Redis,Nginx,Netty为什么这么香?( 二 )


Redis,Nginx,Netty为什么这么香?

文章插图
 
服务端处理网络请求的过程如上图:
  • 连接建立后 。
  • 等待数据准备好(CPU 闲置) 。
  • 将数据从内核拷贝到进程中(CPU 闲置) 。
怎么优化呢?对于一次 I/O 访问(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间 。
所以说,当一个 read 操作发生时,它会经历两个阶段:
  • 等待数据准备 (Waiting for the data to be ready) 。
  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 。
正是因为这两个阶段,Linux 系统升级迭代中出现了下面三种网络模式的解决方案 。
I/O 模型阻塞 I/O:Blocking I/O
Redis,Nginx,Netty为什么这么香?

文章插图
 
简介:最原始的网络 I/O 模型 。进程会一直阻塞,直到数据拷贝完成 。
缺点:高并发时,服务端与客户端对等连接 。
线程多带来的问题:
  • CPU 资源浪费,上下文切换 。
  • 内存成本几何上升,JVM 一个线程的成本约 1MB 。
publicstaticvoidmain(String[]args)throwsIOException{ServerSocketss=newServerSocket();ss.bind(newInetSocketAddress(Constant.HOST,Constant.PORT));intidx=0;while(true){finalSocketsocket=ss.accept();//阻塞方法newThread(()->{handle(socket);},"线程["+idx+"]").start();}}staticvoidhandle(Socketsocket){byte[]bytes=newbyte[1024];try{StringserverMsg="serversss[线程:"+Thread.currentThread().getName()+"]";socket.getOutputStream().write(serverMsg.getBytes());//阻塞方法socket.getOutputStream().flush();}catch(Exceptione){e.printStackTrace();}}非阻塞 I/O:Non Blocking IO
Redis,Nginx,Netty为什么这么香?

文章插图
 
简介:进程反复系统调用,并马上返回结果 。
缺点:当进程有 1000fds,代表用户进程轮询发生系统调用 1000 次 kernel,来回的用户态和内核态的切换,成本几何上升 。
publicstaticvoidmain(String[]args)throwsIOException{ServerSocketChannelss=ServerSocketChannel.open();ss.bind(newInetSocketAddress(Constant.HOST,Constant.PORT));System.out.println("NIOServerstarted...");ss.configureBlocking(false);intidx=0;while(true){finalSocketChannelsocket=ss.accept();//阻塞方法newThread(()->{handle(socket);},"线程["+idx+"]").start();}}staticvoidhandle(SocketChannelsocket){try{socket.configureBlocking(false);ByteBufferbyteBuffer=ByteBuffer.allocate(1024);socket.read(byteBuffer);byteBuffer.flip();System.out.println("请求:"+newString(byteBuffer.array()));Stringresp="服务器响应";byteBuffer.get(resp.getBytes());socket.write(byteBuffer);}catch(IOExceptione){e.printStackTrace();}}I/O 多路复用:IO multiplexing
Redis,Nginx,Netty为什么这么香?

文章插图
 
简介:单个线程就可以同时处理多个网络连接 。内核负责轮询所有 Socket,当某个 Socket 有数据到达了,就通知用户进程 。
多路复用在 Linux 内核代码迭代过程中依次支持了三种调用,即 Select、Poll、Epoll 三种多路复用的网络 I/O 模型 。下文将画图结合 Java 代码解释 。
①I/O 多路复用:Select
Redis,Nginx,Netty为什么这么香?

文章插图
 
简介:有连接请求抵达了再检查处理 。
缺点如下:
  • 句柄上限:默认打开的 FD 有限制,1024 个 。
  • 重复初始化:每次调用 select(),需要把 FD 集合从用户态拷贝到内核态,内核进行遍历 。
  • 逐个排查所有 FD 状态效率不高 。
服务端的 Select 就像一块布满插口的插排,Client 端的连接连上其中一个插口,建立了一个通道,然后再在通道依次注册读写事件 。
一个就绪、读或写事件处理时一定记得删除,要不下次还能处理 。
publicstaticvoidmain(String[]args)throwsIOException{ServerSocketChannelssc=ServerSocketChannel.open();//管道型ServerSocketssc.socket().bind(newInetSocketAddress(Constant.HOST,Constant.PORT));ssc.configureBlocking(false);//设置非阻塞System.out.println("NIOsingleserverstarted,listeningon:"+ssc.getLocalAddress());Selectorselector=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()){SelectionKeykey=it.next();it.remove();//处理的事件,必须删除handle(key);}}}privatestaticvoidhandle(SelectionKeykey)throwsIOException{if(key.isAcceptable()){ServerSocketChannelssc=(ServerSocketChannel)key.channel();SocketChannelsc=ssc.accept();sc.configureBlocking(false);//设置非阻塞sc.register(key.selector(),SelectionKey.OP_READ);//在建立好的管道上,注册关心的事件可读}elseif(key.isReadable()){//flipSocketChannelsc=null;sc=(SocketChannel)key.channel();ByteBufferbuffer=ByteBuffer.allocate(512);buffer.clear();intlen=sc.read(buffer);if(len!=-1){System.out.println("["+Thread.currentThread().getName()+"]recv:"+newString(buffer.array(),0,len));}ByteBufferbufferToWrite=ByteBuffer.wrap("HelloClient".getBytes());sc.write(bufferToWrite);}}


推荐阅读