从操作系统层面理解Linux下的网络IO模型,这么讲你还不懂?( 二 )

怎么优化呢?
对于一次I/O访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间 。
所以说,当一个read操作发生时,它会经历两个阶段:

  • 等待数据准备 (Waiting for the data to be ready) 。
  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 。
正是因为这两个阶段,Linux系统升级迭代中出现了下面三种网络模式的解决方案 。
02
I/O模型
2.1 阻塞 I/O - Blocking I/O
从操作系统层面理解Linux下的网络IO模型,这么讲你还不懂?

文章插图
 
简介:最原始的网络I/O模型 。进程会一直阻塞,直到数据拷贝完成 。
缺点:高并发时,服务端与客户端对等连接,线程多带来的问题:
  • CPU资源浪费,上下文切换 。
  • 内存成本几何上升,JVM一个线程的成本约1MB 。
public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT)); int idx =0; while (true) { final Socket socket = ss.accept();//阻塞方法 new Thread(() -> { handle(socket); },"线程["+idx+"]" ).start(); } } static void handle(Socket socket) { byte[] bytes = new byte[1024]; try { String serverMsg = " server sss[ 线程:"+ Thread.currentThread().getName() +"]"; socket.getOutputStream().write(serverMsg.getBytes());//阻塞方法 socket.getOutputStream().flush(); } catch (Exception e) { e.printStackTrace(); } }2.2 非阻塞 I/O - Non Blocking IO
从操作系统层面理解Linux下的网络IO模型,这么讲你还不懂?

文章插图
 
简介: 进程反复系统调用,并马上返回结果 。
缺点: 当进程有1000fds,代表用户进程轮询发生系统调用1000次kernel,来回的用户态和内核态的切换,成本几何上升 。
public static void main(String[] args) throws IOException { ServerSocketChannel ss = ServerSocketChannel.open(); ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT)); System.out.println(" NIO server started ... "); ss.configureBlocking(false); int idx =0; while (true) { final SocketChannel socket = ss.accept();//阻塞方法 new Thread(() -> { handle(socket); },"线程["+idx+"]" ).start(); } } static void handle(SocketChannel socket) { try { socket.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socket.read(byteBuffer); byteBuffer.flip(); System.out.println("请求:" + new String(byteBuffer.array())); String resp = "服务器响应"; byteBuffer.get(resp.getBytes()); socket.write(byteBuffer); } catch (IOException e) { e.printStackTrace(); } }2.3 I/O 多路复用 - IO multiplexing
从操作系统层面理解Linux下的网络IO模型,这么讲你还不懂?

文章插图
 
简介: 单个线程就可以同时处理多个网络连接 。内核负责轮询所有socket,当某个socket有数据到达了,就通知用户进程 。多路复用在Linux内核代码迭代过程中依次支持了三种调用,即SELECT、POLL、EPOLL三种多路复用的网络I/O模型 。下文将画图结合Java代码解释 。
2.3.1 I/O 多路复用- select
从操作系统层面理解Linux下的网络IO模型,这么讲你还不懂?

文章插图
 
简介: 有连接请求抵达了再检查处理 。
缺点:
  • 句柄上限- 默认打开的FD有限制,1024个 。
  • 重复初始化-每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,内核进行遍历 。
  • 逐个排查所有FD状态效率不高 。
服务端的select 就像一块布满插口的插排,client端的连接连上其中一个插口,建立了一个通道,然后再在通道依次注册读写事件 。一个就绪、读或写事件处理时一定记得删除,要不下次还能处理 。
public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open();//管道型ServerSocket ssc.socket().bind(new InetSocketAddress(Constant.HOST, Constant.PORT)); ssc.configureBlocking(false);//设置非阻塞 System.out.println(" NIO single server started, listening on :" + ssc.getLocalAddress()); Selector selector = Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT);//在建立好的管道上,注册关心的事件 就绪 while(true) { selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while(it.hasNext()) { SelectionKey key = it.next(); it.remove();//处理的事件,必须删除 handle(key); } } } private static void handle(SelectionKey key) throws IOException { if(key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false);//设置非阻塞 sc.register(key.selector(), SelectionKey.OP_READ );//在建立好的管道上,注册关心的事件 可读 } else if (key.isReadable()) { //flip SocketChannel sc = null; sc = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(512); buffer.clear(); int len = sc.read(buffer); if(len != -1) { System.out.println("[" +Thread.currentThread().getName()+"] recv :"+ new String(buffer.array(), 0, len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); } }


推荐阅读