Java 中的 IO 与 socket 编程 [ 复习 ]
一、Unix IO 与 IPC
Unix IO:Open-Read or Write-Close
IPC:open socket - receive and send to socket - close socket
IPC 全称是 InterProcess Communication。
当消息发出后,消息进入 SendQ队列 一直等待 sending socket 处理,才真正发出(一直等待是阻塞的)。当消息到达时,消息进入RecvQ队列 一直等待 receiving socket 处理(同前)。
底层的 TCP 协议关联的 RecvQ 或者 SendQ 队列就是一个操作系统缓冲区。
1 2 3 4 5 6 7 8 9 | public static void simpleIo() throws IOException { InputStream input = new BufferedInputStream( new FileInputStream( "/home/lg/Pictures/wallpapers/2047.png" )); OutputStream out = new BufferedOutputStream( new FileOutputStream( "dem.png" )); byte [] buf = new byte [ 1024 ]; while (input.read(buf) > 0 ) { out.write(buf); } out.flush(); } |
基于字符的
1 2 3 4 5 6 7 8 9 10 | public static void simpleIo() throws IOException { BufferedReader input = new BufferedReader( new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(System.out, true ); String myLine; while ((myLine = input.readLine()) != null ) { out.println(myLine); } } // new String(char[] chars, "utf-8") 字节序列到字符串的转换 |
* PrintWriter 与 BufferedWriter 区别
1. PrintWriter的print、println方法可以接受任意类型的参数,而BufferedWriter的write方法只能接受字符、字符数组和字符串;
2. PrintWriter的println方法自动添加换行,BufferedWriter需要显示调用newLine方法;
3. PrintWriter的方法不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生;
4. PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);
5. PrintWriter的构造方法更广。
三、Java 中的 NIO 编程
读写文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public static void readAndWriteFile() throws IOException { FileInputStream fileInputStream = null ; FileOutputStream fileOutputStream = null ; try { File file = new File( "test.txt" ); fileInputStream = new FileInputStream(file); FileChannel inputFileChannel = fileInputStream.getChannel(); fileOutputStream = new FileOutputStream( "out.txt" ); FileChannel outFileChannel = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate( 5 ); while ( true ) { // 重置上一次的写位置以及限制。开始重新读 byteBuffer.clear(); // 读了多少内容:0 表示读不了,-1 表示已读完。 int read = inputFileChannel.read(byteBuffer); if (read == - 1 ) { break ; } // 下一次写,限制为读的位置 byteBuffer.flip(); outFileChannel.write(byteBuffer); } } finally { if (fileInputStream != null ) { fileInputStream.close(); } if (fileOutputStream != null ) { fileOutputStream.close(); } } } |
拷贝文件
1 2 3 4 5 6 7 8 9 10 | public static void copyFile() throws Exception { try (FileInputStream fis = new FileInputStream( "test.txt" ); FileOutputStream fos = new FileOutputStream( "test_copy.txt" ); FileChannel fisChannel = fis.getChannel(); FileChannel fosChannel = fos.getChannel()) { fosChannel.transferFrom(fisChannel, 0 , fisChannel.size()); } catch (IOException e) { e.printStackTrace(); } } |
buffer 的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // MappedByteBuffer public static void mappedByteBufferTest(String[] args) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile( "test.txt" , "rw" ); FileChannel channel = randomAccessFile.getChannel(); //参数说明:0 表示可以修改的位置,5表示映射到内存的大小(字节数) MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0 , 5 ); mappedByteBuffer.put( 0 , ( byte ) 'H' ); mappedByteBuffer.put( 3 , ( byte ) '9' ); randomAccessFile.close(); } // readOnlyBuffer public static void ReadOnlyBufferTest(String[] args) { ByteBuffer byteBuffer = ByteBuffer.allocate( 64 ); for ( int i = 0 ; i < 64 ; i++) { byteBuffer.put(( byte ) i); } byteBuffer.flip(); ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer(); while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } } |
四、BIO 中的 socket 编程
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class Server { public static void main(String[] args) { { ServerSocket myServer = null ; Socket acceptSocket = null ; BufferedReader input = null ; PrintWriter output = null ; try { myServer = new ServerSocket( 4000 ); while ( true ) { acceptSocket = myServer.accept(); input = new BufferedReader( new InputStreamReader(acceptSocket.getInputStream())); output = new PrintWriter(acceptSocket.getOutputStream(), true ); String rLine; while ((rLine = input.readLine()) != null ) { System.out.println( "服务器接受到一条消息:\n" + rLine + "\n---------------\n\n" ); if (rLine.equals( "hello server" )) { System.out.println( "然后打了个招呼\n---------------\n\n" ); output.println( "hello client" ); } else { System.out.println( "然后嘲讽了一波\n---------------\n\n" ); output.println( "233333" ); } } } } catch (IOException e) { e.printStackTrace(); } } } } |
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public class Client { private static Socket myClient; //静态初始化时处理异常 static { try { myClient = new Socket( "localhost" , 4000 ); } catch (IOException e) { e.printStackTrace(); } } private static void sendMessage() { try { PrintWriter output = new PrintWriter(myClient.getOutputStream(), true ); BufferedReader readConsole = new BufferedReader( new InputStreamReader(System.in)); String cline; while ((cline = readConsole.readLine()) != null ) { output.println(cline); System.out.println( "消息发送成功!\n---------------\n\n" ); } } catch (IOException e) { e.printStackTrace(); } } private static void receiveMessage() { try { BufferedReader input = new BufferedReader( new InputStreamReader(myClient.getInputStream())); // 这里省略掉了 OutputStream(System.out), 直接使用打印替代 String rLine; while ((rLine = input.readLine()) != null ) { System.out.println( "客户端收到来自服务器的消息:\n" + rLine + "\n---------------\n\n" ); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(Client::sendMessage).start(); new Thread(Client::receiveMessage).start(); } } |
说明:
客户端使用了两个线程:一个用于发送消息,另一个用于接受消息。服务端只考虑了一个 socket 连接,因此并没有用多线程。实际上,服务端通常需要同时处理多个 socket 连接,因此可考虑多线程。建立一个线程池,对于每个 socket 连接,分派一个线程去处理。
五、NIO 中 socket 编程
1. 处理流程对比
普通的 BIO,消息要等待处理,需要先放入操作系统的 Socket 缓冲区(阻塞的SendQ与RecvQ队列),这样一直等待,直到 sending socket 或 receiving socket 就位后,才进行处理。
而 NIO,是 消息进入到操作系统的 Socket 缓冲区,直接复制到 Buffer (ByteBuffer.allocate)中,抑或直接进入与系统底层关联的缓冲区(ByteBuffer.allocateDirector)。不用一直等待 sending socket 或 receiving socket 就续就进行处理,是非阻塞的。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
下图是几种常见I/O模型的对比:(图片来自:UNIX网络编程 -- I/O复用:select和poll函数)
(摘自 Linux IO模式及 select、poll、epoll详解 )
- BIO,同步阻塞IO,在IO执行的两个阶段都被block了。如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
- NIO,同步非阻塞IO,用户进程需要不断的主动询问kernel数据好了没有。
- 多路复用IO,就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。
当用户进程调用了select,那么整个进程会被block
,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。注意与 nio 的关系:在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。 - 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
- 异步IO,用户进程发起read操作之后,立刻就可以开始去做其它的事,当数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
2.线程模型对比
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。
NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
3.事件模型
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。Selector 主要围绕这几个事件做分发。注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
4.socket 编程原理
5. 示例
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | public class Server { private static Selector selector; static { try { selector = Selector.open(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { sscRegister(); } private static void sscRegister() { try { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking( false ); ssc.socket().bind( new InetSocketAddress( "localhost" , 8000 )); ssc.register(selector, SelectionKey.OP_ACCEPT); dispatcher(selector); } catch (IOException e) { e.printStackTrace(); } } private static void dispatcher(Selector selector) { try { while ( true ) { int select = selector.select(); if (select == 0 ) { System.out.println( "返回 0 表示 SelectedKeys 未更新" ); continue ; } Iterator<SelectionKey> selectedIterator = selector.selectedKeys().iterator(); while (selectedIterator.hasNext()) { SelectionKey key = selectedIterator.next(); selectedIterator.remove(); // 只是从 SelectedKeys 中移除,而并不是从整个选择器的键集中移除。 if (!key.isValid()) { continue ; } if (key.isConnectable()) { System.out.println( 233 ); } if (key.isAcceptable()) { accept(key); } if (key.isReadable()) { read(key); } } } } catch (IOException e) { e.printStackTrace(); } } private static void accept(SelectionKey key) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); try { SocketChannel socketChannel; if ((socketChannel = serverSocketChannel.accept()) != null ) { // 服务端非阻塞时,直接返回 null socketChannel.configureBlocking( false ); socketChannel.register(selector, SelectionKey.OP_READ); } } catch (IOException e) { e.printStackTrace(); } } private static void read(SelectionKey key) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); try { if (channel.read(buffer) > 0 ) { System.out.println( "客户端:" + new String(buffer.array())); buffer.clear(); } } catch (IOException e) { e.printStackTrace(); } finally { buffer.clear(); } } } |
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class Client { private static void clientSendMsg() { try { SocketChannel client = SocketChannel.open( new InetSocketAddress( "localhost" , 8000 )); System.out.println( "已向服务器发出请求!" ); String msg = "hello" + Thread.currentThread().getName(); ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); client.write(buffer); buffer.flip(); Thread.sleep( 5 ); client.close(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(Client::clientSendMsg, "Thread-A" ).start(); } } |
参考文章
https://tech.meituan.com/nio.html?utm_source=tool.lu
https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html
https://www.bysocket.com/?p=615 (大小端模式)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix