百度做的网站,优秀产品设计作品,广告经营许可证,东莞职业技术学院Netty系列整体栏目 内容链接地址【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478【三】深入理解NIO的基本原理和底层…Netty系列整体栏目 内容链接地址【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478【三】深入理解NIO的基本原理和底层实现https://zhenghuisheng.blog.csdn.net/article/details/138451491 深入理解NIO的基本原理和底层实现 一深入理解NIO的底层原理1Reactor反应堆模式1.1通过餐厅描述Bio1.2通过餐厅引入nio 2NIO三大核心组件3NIO通信原理4通过NIO实现简单网络编程 一深入理解NIO的底层原理
在上一篇中讲解了bio的底层原理和具体实现虽然bio在一定场景下也可以进行通信但是随着互联网越来越多业务的场景bio会存在阻塞的弊端被暴露无疑在并发量稍微大点的地方通过bio实现的网络编程会显得略显吃力。于是在jdk1.4之后引入了一个新东西 NIO 由于bio原名叫做 Blocking IO阻塞io因此新网络编程的取名nio有着 NoBlocking IO即不阻塞io当然也有的地方取名为new io。
在讲解nio之前依旧和以前的学习一样不能脱离官网进行学习netty官网地址 用户指南可以参考4.1版本
1Reactor反应堆模式
网络编程从bio废弃到再到nio的崛起跟nio的底层实现有着很大的联系其最主要的设计思想就是这个 reactor 反应堆模式总结这个reactor模式主要有三点注册感兴趣的事件扫描是否有感兴趣的事件发生在事件发生之后做出相应的处理
在讲解这个反应堆模式之前先通过一个生活中的案例来讲述这个事情以我们去线下餐厅点餐为例首先用户扫码点餐然后点餐系统返回一个排队的号码再服务员喊到号码的时候去取餐就以这个案例来说明一下什么是反应堆模式。
1.1通过餐厅描述Bio
首先在上面的这个点餐的案例中bio的实现如下就有点类似于用户直接和厨师直接进行交流告诉厨师要什么菜当没有用户点餐时那么厨师就会一直等待用户来点餐直到有用户点餐为止如果一直没有用户点餐那么厨师就会一直处于阻塞状态进行等待这里就对应了bio服务端没有客户端请求时会长期处于一个阻塞状态
如果已经有了一个用户点餐那么厨师会先炒这个用户的菜当有其他用户来点餐时那么其他用户会处于阻塞状态只有等帮第一个用户炒完菜之后厨师才能和第二个用户进行交流第二个用户才能把自己需要什么菜告诉厨师在如果在上一个用户的菜还没炒玩之前那么下一个用户则会处于一个阻塞等待状态。因此这样效率肯定是非常低下的那么毫无疑问bio的这种方式注定是要被淘汰的。(这里服务端默认为在一个cpu里面就是说一个cpu中只有一个线程去处理请求上面案例对应的就是服务端对应的就是一个厨师厨师就是老板其他顾客就是对应的服务端) 1.2通过餐厅引入nio
由于一个厨师对应多个用户效率会十分的低下而且如果用户量稍微大一点那么每个用户就不用去干其他的事情就一直排队阻塞在那里因此严重的影响整个系统的吞吐量以及严重的影响用户的体验感。随着客户的增加或者午餐这段高峰期为了解决用户长时间等待问题那么就可以做一个点餐系统用户只需扫码点餐即可当用户点餐完成之后可以去做用户自己想做的事情如出去逛逛等此时系统会给用户一个点餐号此时就解决了用户长时间排队阻塞的问题。厨师这边也不需要每次只处理一个请求如多个用户点同一个菜那么厨师可以一次性炒多份菜这样也提高了厨师这边的效率。当厨师将菜炒好之后只需要服务员通过念号或者通过公众号通知订餐的用户即可。 反应堆模式就是不能一直等着客户端去等待服务端的响应而是通过某个中间层客户端先向中间层注册一个事件当服务端有空做出响应的时候再通过定时任务去扫描这个中间层当中间层发现有注册的事件之后再去通知客户端这样就可以减少客户端的等待时间。换句话就是说通过请求响应的模式来说客户端向服务端发送一个请求之后如果服务端长时间没有响应那么客户端可以结束此次请求服务端来不及响应但是服务端得记录这个请求的记录当服务端有空的时候再去扫描这个记录再去响应这个请求再通过通知异步的去响应对应的请求。
2NIO三大核心组件
在nio编程中里面有三大核心组件分别是 Selector、Channel、Buffer 三大组件。
在上面讲解了通过餐厅系统去了解nio的内部实现在这三大组件中扮演的角色分别如下
由于在网络编程中基本是基于tcp协议去实现客户端和服务端之间的通信因此通过socket将tcp协议封装而这里的channel是对这个socket进行了再次的封装。也就是说只需要创建这个channel实例就可以完成双端之间的通信因此点餐系统里面的用户和厨师之间的交流就是通过这个channel去实现的那么channel扮演的角色就是完成客户和厨师之间的最终交流Selector就是一个选择器通过这个餐厅系统可以发现引入了一个新的点餐系统用于注册客户的订单以及在订单完成之后给予响应就是通知下单的客户因此这个Selector选择器扮演的角色就是这个点餐系统也是这个反应堆模式的核心用于注册客户端事件扫描这些注册的事件并对这些事件做出具体的响应而这个Buffer就是nio和bio之间的重大区别因为这个Buffer就是一个Nio的一个重要的特性用于面向缓冲流进行编程这个Buffer指的是应用层之间的buffer就是已经建立好连接之后在服务端内部的一个缓冲区如在这个餐厅系统中在准备食材的时候也是需要大量时间的如果先点餐的用户需要准备的食材要久一些那么厨师可以优先炒后面用户下的单那么这个Buffer就起到重要的作用了。由于这个bio是串行执行那么就不存在这个Buffer的说法但是在这个nio里面通过这个Buffer让整个系统更加的灵活即使先建立的请求也可以后响应从而提高整个系统的吞吐量。还有比如说可以重复的读取数据来不及处理的优先放在这个buffer缓冲区某个buffer缓冲区如果字节数没达到要求可以先去处理其他的缓冲区等主要是让整个系统更加的灵活多变从而提高整个系统的吞吐量和响应。同时也是与BIO最大的差异化之一 3NIO通信原理
通过上面的餐厅事例和讲解NIO内部的三大组件接下来通过一个发送和接收数据的事例讲解NIO底层到底是如何进行网络通信和数据传输的。
首先客户端先向服务端发送一个请求然后服务端在接收到这个请求之后服务端首先会通过这个Selector先向本地注册一个连接事件然后再扫描Channel事件列表查看是否有感兴趣的Channel事件在Channel中找到这个对连接感兴趣的事件之后随后通知这个感兴趣的事件创建一个ServerSocketChannel对象用于服务端和客户端通过三次握手建立可靠的连接完成建立连接之后又会去Selector中扫描是否有对读数据感兴趣的事件如果找到有服务端对读数据感兴趣的事件又会通知对这个事件感兴趣的具体事件用于实例化SocketChannel对象这里的SocketChannel就是建立好连接的Socket对象用于真正的去读取数据以及发送数据socket读取的数据并不是发送给服务端的应用程序而是将数据先存入到Buffer中让应用程序去读取buffer里面的数据从而提高整个架构的吞吐量和效率最后将要响应的数据也存到Buffer中然后通过感兴趣的写事件将数据返回给对应的客户端即可 4通过NIO实现简单网络编程
上面讲解了大量的理论接下来通过具体的编码来讲述NIO的底层到底是怎么实现的。首先创建一个服务端的线程用于接收客户端的请求以及内部做出的响应接下来创建一个 NioServerTask 的任务类并且实现一个 Runnable 方法在该方法中去创建 selectorServerSocketChannelSockerChannel、Buffer等对象
package com.zhs.netty.nio.nio;import com.zhs.netty.nio.Const;
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;/*** 服务端线程具体代码实现*/
Slf4j
public class NioServerTask implements Runnable{private volatile boolean started;private ServerSocketChannel serverSocketChannel;private Selector selector;/*** 构造方法* param port 指定要监听的端口号*/public NioServerTask(int port) {try {//创建一个选择器selector Selector.open();//创建ServerSocketChannel的实例serverSocketChannel ServerSocketChannel.open();//通道实例设置为非阻塞模式serverSocketChannel.configureBlocking(false);//绑定端口serverSocketChannel.socket().bind(new InetSocketAddress(port));//注册事件到selector之上监听客户端连接serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);started true;log.info(服务器已启动端口号 port);} catch (IOException e) {e.printStackTrace();}}Overridepublic void run() {while(started){try {//selector每隔1s被唤醒一次selector.select(1000);//获取全部已经注册的本地事件SetSelectionKey selectionKeys selector.selectedKeys();IteratorSelectionKey iterator selectionKeys.iterator();while(iterator.hasNext()){SelectionKey key iterator.next();//将处理过的本地注册事件给删除iterator.remove();handleInput(key);}} catch (IOException e) {e.printStackTrace();}}}//处理具体的事件private void handleInput(SelectionKey key) throws IOException {if(key.isValid()){//处理新接入的客户端的请求if(key.isAcceptable()){//获取channels全部事件中对此感兴趣的事件ServerSocketChannel ssc (ServerSocketChannel) key.channel();//获取到感兴趣的事件之后创建一个socket实例用于发送和读取数据SocketChannel sc ssc.accept();//设置为非阻塞sc.configureBlocking(false);//注册一个感兴趣的读事件sc.register(selector,SelectionKey.OP_READ);}//处理对端的发送的数据if(key.isReadable()){SocketChannel sc (SocketChannel) key.channel();//创建ByteBuffer开辟一个缓冲区ByteBuffer buffer ByteBuffer.allocate(1024);int readBytes sc.read(buffer);if(readBytes0){//缓冲区中存在指针记录有效位置buffer.flip();//根本有效位置的指针处创建字节数组byte[] bytes new byte[buffer.remaining()];//将缓冲区可读字节数组复制到新建的数组中buffer.get(bytes);String message new String(bytes,UTF-8);log.info(服务器收到消息 message);String result Const.response(message);doWrite(sc,result);}else if(readBytes0){//将channels集合的数据取消key.cancel();sc.close();}}}}/*发送应答消息*/private void doWrite(SocketChannel sc,String response) throws IOException {byte[] bytes response.getBytes();ByteBuffer buffer ByteBuffer.allocate(bytes.length);buffer.put(bytes);buffer.flip();sc.write(buffer);}
}从上面的代码中可以发现在服务端中只关注了读的事件并没有关注写的事件。并且在这个Buffer中存在一个指针用于记录buffer的有效位置这样在读数据时只需要读取到有效的数据即可。
服务端代码写好之后接下来编写客户端的代码代码和客户端基本一样但是由于客户端不需要提供服务因此在客户端这边是不需要 ServerSocketChannel 这个组件的。其他的 SocketChannelSelectorBuffer 还是需要的
package com.zhs.netty.nio.nio;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;/*** author zhenghuisheng* nio客户端请求*/
Slf4j
public class NioClientTask implements Runnable{private String host;private int port;private volatile boolean started;private Selector selector;private SocketChannel socketChannel;public NioClientTask(String ip, int port) {this.host ip;this.port port;try {//创建选择器的实例selector Selector.open();//创建ServerSocketChannel的实例socketChannel SocketChannel.open();//设置通道为非阻塞模式socketChannel.configureBlocking(false);started true;} catch (IOException e) {e.printStackTrace();}}Overridepublic void run() {try{doConnect();}catch(IOException e){e.printStackTrace();System.exit(1);}//循环遍历selectorwhile(started){try{//无论是否有读写事件发生selector每隔1s被唤醒一次selector.select(1000);//获取全部已经注册的本地事件SetSelectionKey keys selector.selectedKeys();//转换为迭代器IteratorSelectionKey it keys.iterator();SelectionKey key null;while(it.hasNext()){key it.next();it.remove();try{handleInput(key);}catch(Exception e){if(key ! null){key.cancel();if(key.channel() ! null){key.channel().close();}}}}}catch(Exception e){e.printStackTrace();System.exit(1);}}//selector关闭后会自动释放里面管理的资源if(selector ! null)try{selector.close();}catch (Exception e) {e.printStackTrace();}}//具体的事件处理方法private void handleInput(SelectionKey key) throws IOException{if(key.isValid()){//获得关心当前事件的channelSocketChannel sc (SocketChannel) key.channel();//连接事件if(key.isConnectable()){if(sc.finishConnect()){socketChannel.register(selector,SelectionKey.OP_READ);}else System.exit(1);}//有数据可读事件if(key.isReadable()){//创建ByteBuffer并开辟一个1M的缓冲区ByteBuffer buffer ByteBuffer.allocate(1024);//读取请求码流返回读取到的字节数int readBytes sc.read(buffer);//读取到字节对字节进行编解码if(readBytes0){//将缓冲区当前的limit设置为position,position0// 用于后续对缓冲区的读取操作buffer.flip();//根据缓冲区可读字节数创建字节数组byte[] bytes new byte[buffer.remaining()];//将缓冲区可读字节数组复制到新建的数组中buffer.get(bytes);String result new String(bytes,UTF-8);log.info(客户端收到消息 result);}//链路已经关闭释放资源else if(readBytes0){key.cancel();sc.close();}}}}private void doWrite(SocketChannel channel,String request)throws IOException {//将消息编码为字节数组byte[] bytes request.getBytes();//根据数组容量创建ByteBufferByteBuffer writeBuffer ByteBuffer.allocate(bytes.length);//将字节数组复制到缓冲区writeBuffer.put(bytes);//flip操作writeBuffer.flip();//发送缓冲区的字节数组channel.write(writeBuffer);}private void doConnect() throws IOException{//非阻塞的连接,这里需要注意因为客户端和服务端都是无阻塞的因此可能在三次握手建立连接之前//这段注册读的代码就已经走完了因此在else中增加一个注册连接的代码if(socketChannel.connect(new InetSocketAddress(host,port))){socketChannel.register(selector,SelectionKey.OP_READ);}else{socketChannel.register(selector,SelectionKey.OP_CONNECT);}}//写数据对外暴露的APIpublic void sendMsg(String msg) throws Exception{doWrite(socketChannel, msg);}
}接下来进行一个数据的测试先创建一个服务端的Main方法然后启动这个Main方法并且设置端口号为8881
public class NioServer {private static NioServerTask nioServerTask;public static void main(String[] args){nioServerTask new NioServerTask(8881);new Thread(nioServerTask,NioServer).start();}
}再创建一个客户端的Main方法ip设置成本地端口号设置成服务端设置的端口号
/*** author zhenghuisheng*/
public class NioClient {private static NioClientTask nioClientTask;public static void main(String[] args) throws Exception {nioClientTask new NioClientTask(127.0.0.1,8881);new Thread(nioClientTask,nioClient).start();//控制台输入Scanner scanner new Scanner(System.in);String message scanner.next();while(!StringUtils.isEmpty(message)){nioClientTask.sendMsg(message);}}
}客户端发送消息
132432
21:58:41.118 [nioClient] INFO com.zhs.netty.nio.nio.NioClientTask - 客户端收到消息Hello,132432,Now is Sat May 04 21:58:41 CST 2024服务端接收到的消息
21:58:30.767 [main] INFO com.zhs.netty.nio.nio.NioServerTask - 服务器已启动端口号8881
21:58:41.114 [NioServer] INFO com.zhs.netty.nio.nio.NioServerTask - 服务器收到消息132432到此为止通过NIO的方式将服务端发送消息和客户端接收消息的代码实现