百度趋势搜索大数据,上海百度推广优化排名,房屋装修设计图,专做polo衫的网站Netty学习之NIO基础 - Nyimas Blog
1、阻塞
阻塞模式下#xff0c;相关方法都会导致线程暂停 ServerSocketChannel.accept 会在没有连接建立时让线程暂停SocketChannel.read 会在通道中没有数据可读时让线程暂停阻塞的表现其实就是线程暂停了#xff0c;暂停期间不会占用 c… Netty学习之NIO基础 - Nyimas Blog
1、阻塞
阻塞模式下相关方法都会导致线程暂停 ServerSocketChannel.accept 会在没有连接建立时让线程暂停SocketChannel.read 会在通道中没有数据可读时让线程暂停阻塞的表现其实就是线程暂停了暂停期间不会占用 cpu但线程相当于闲置单线程下阻塞方法之间相互影响几乎不能正常工作需要多线程支持但多线程下有新的问题体现在以下方面 32 位 jvm 一个线程 320k64 位 jvm 一个线程 1024k如果连接数过多必然导致 OOM并且线程太多反而会因为频繁上下文切换导致性能降低可以采用线程池技术来减少线程数和线程上下文切换但治标不治本如果有很多连接建立但长时间 inactive会阻塞线程池中所有线程因此不适合长连接只适合短连接
服务端代码
public class Server {public static void main(String[] args) {// 创建缓冲区ByteBuffer buffer ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server ServerSocketChannel.open()) {// 为服务器通道绑定端口server.bind(new InetSocketAddress(8080));// 用户存放连接的集合ArrayListSocketChannel channels new ArrayList();// 循环接收连接while (true) {System.out.println(before connecting...);//建立与客户端连接, SocketChannel 与客户端进行通信//没有连接时会阻塞线程SocketChannel socketChannel server.accept();System.out.println(after connecting...);channels.add(socketChannel);// 循环遍历集合中的连接for(SocketChannel channel : channels) {System.out.println(before reading);// 接/处理通道中的数据// 当通道中没有数据可读时会阻塞线程channel.read(buffer);buffer.flip();//调试打印出来ByteBufferUtil.debugRead(buffer);buffer.clear();System.out.println(after reading);}}} catch (IOException e) {e.printStackTrace();}}
}
客户端代码
public class Client {public static void main(String[] args) {try (SocketChannel socketChannel SocketChannel.open()) {// 建立连接socketChannel.connect(new InetSocketAddress(localhost, 8080));System.out.println(waiting...);} catch (IOException e) {e.printStackTrace();}}
}
运行结果
客户端-服务器建立连接前服务器端因accept阻塞客户端-服务器建立连接后客户端发送消息前服务器端因通道为空被阻塞 当通道中没有数据可读时会阻塞线程 channel.read(buffer); 客户端发送数据后服务器处理通道中的数据。再次进入循环时再次被accept阻塞 之前的客户端再次发送消息服务器端因为被accept阻塞无法处理之前客户端发送到通道中的信息,accept只有建立新的连接才会继续执行,当有一个新的连接时,才会接收到之前客户端发送的消息,如果没有新的连接,线程会一直阻塞在accept 2、非阻塞 可以通过ServerSocketChannel的configureBlocking(false)方法将获得连接设置为非阻塞的。此时若没有连接accept会返回null 可以通过SocketChannel的configureBlocking(false)方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读read会返回-1
服务器代码如下
public class Server {public static void main(String[] args) {// 创建缓冲区ByteBuffer buffer ByteBuffer.allocate(16);// 获得服务器通道try {ServerSocketChannel server ServerSocketChannel.open()// 为服务器通道绑定端口server.bind(new InetSocketAddress(8080));// 用户存放连接的集合ArrayListSocketChannel channels new ArrayList();// 循环接收连接while (true) {// 设置为非阻塞模式没有连接时返回null不会阻塞线程server.configureBlocking(false);SocketChannel socketChannel server.accept();// 通道不为空时才将连接放入到集合中if (socketChannel ! null) {System.out.println(after connecting...);channels.add(socketChannel);}// 循环遍历集合中的连接for(SocketChannel channel : channels) {// 处理通道中的数据// 设置为非阻塞模式若通道中没有数据会返回0不会阻塞线程channel.configureBlocking(false);int read channel.read(buffer);//没有数据会返回0不会阻塞线程if(read 0) {buffer.flip();ByteBufferUtil.debugRead(buffer);buffer.clear();System.out.println(after reading);}}}} catch (IOException e) {e.printStackTrace();}}
}
这样写存在一个问题因为设置为了非阻塞会一直执行while(true)中的代码CPU一直处于忙碌状态会使得性能变低所以实际情况中不使用这种方法处理请求
3、Selector
多路复用
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控这称之为多路复用
多路复用仅针对网络 IO普通文件 IO 无法利用多路复用如果不用 Selector 的非阻塞模式线程大部分时间都在做无用功而 Selector 能够保证 有可连接事件时才去连接有可读事件才去读取有可写事件才去写入 限于网络传输能力Channel 未必时时可写一旦 Channel 可写会触发 Selector 的可写事件
4、使用及Accept事件 要使用Selector实现多路复用服务端代码如下改进 public class SelectServer {public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel serverChannel ServerSocketChannel.open()) {serverChannel .bind(new InetSocketAddress(8080));// 创建选择器Selector selector Selector.open();// 通道必须设置为非阻塞模式serverChannel.configureBlocking(false);// 将通道注册到选择器中并设置感兴趣的事件//返回值是当前是事件 下面的 iterator.next();serverChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {// 若没有事件就绪线程会被阻塞反之不会被阻塞。从而避免了CPU空转// 返回值为就绪的事件个数int ready selector.select();System.out.println(selector ready counts : ready);// 获取所有事件SetSelectionKey selectionKeys selector.selectedKeys();// 使用迭代器遍历事件IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();// 判断key的类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel (ServerSocketChannel) key.channel();System.out.println(before accepting...);// 获取连接并处理而且是必须处理否则需要取消,如果不处理 会一直循环SocketChannel socketChannel channel.accept();System.out.println(after accepting...);// 处理完毕后移除iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}
} 步骤解析
获得选择器Selector Selector selector Selector.open(); 将通道设置为非阻塞模式并注册到选择器中并设置感兴趣的事件 channel 必须工作在非阻塞模式FileChannel 没有非阻塞模式因此不能配合 selector 一起使用绑定的事件类型可以有 connect - 客户端连接成功时触发accept - 服务器端成功接受连接时触发read - 数据可读入时触发有因为接收能力弱数据暂不能读入的情况write - 数据可写出时触发有因为发送能力弱数据暂不能写出的情况 // 通道必须设置为非阻塞模式 server.configureBlocking(false); // 将通道注册到选择器中并设置感兴趣的实践 server.register(selector, SelectionKey.OP_ACCEPT); 通过Selector监听事件并获得就绪的通道个数若没有通道就绪线程会被阻塞 阻塞直到绑定事件发生 int count selector.select();Copy 阻塞直到绑定事件发生或是超时时间单位为 ms int count selector.select(long timeout);Copy 不会阻塞也就是不管有没有事件立刻返回自己根据返回值检查是否有事件 int count selector.selectNow(); 获取就绪事件并得到对应的通道然后进行处理 // 获取所有事件
SetSelectionKey selectionKeys selector.selectedKeys();// 使用迭代器遍历事件
IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();// 判断key的类型此处为Accept类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel (ServerSocketChannel) key.channel();// 获取连接并处理而且是必须处理否则需要取消SocketChannel socketChannel channel.accept();// 处理完毕后移除iterator.remove();}
}
事件发生后能否不处理
事件发生后要么处理要么取消cancel不能什么都不做否则下次该事件仍会触发这是因为 nio 底层使用的是水平触发 5、Read事件
在Accept事件中若有客户端与服务器端建立了连接需要将其对应的SocketChannel设置为非阻塞并注册到选择其中添加Read事件触发后进行读取操作 public class SelectServer {public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));// 创建选择器Selector selector Selector.open();// 通道必须设置为非阻塞模式server.configureBlocking(false);// 将通道注册到选择器中并设置感兴趣的实践server.register(selector, SelectionKey.OP_ACCEPT);// 为serverKey设置感兴趣的事件while (true) {// 若没有事件就绪线程会被阻塞反之不会被阻塞。从而避免了CPU空转// 返回值为就绪的事件个数int ready selector.select();System.out.println(selector ready counts : ready);// 获取所有事件SetSelectionKey selectionKeys selector.selectedKeys();// 使用迭代器遍历事件IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();// 判断key的类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel (ServerSocketChannel) key.channel();System.out.println(before accepting...);// 获取连接SocketChannel socketChannel channel.accept();System.out.println(after accepting...);// 设置为非阻塞模式同时将连接的通道也注册到选择其中socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);// 处理完毕后移除iterator.remove();} else if (key.isReadable()) {SocketChannel channel (SocketChannel) key.channel();System.out.println(before reading...);channel.read(buffer);System.out.println(after reading...);buffer.flip();ByteBufferUtil.debugRead(buffer);buffer.clear();// 处理完毕后移除iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}
}
删除事件
当处理完一个事件后一定要调用迭代器的remove方法移除对应事件否则会出现错误。原因如下
以我们上面的 Read事件 的代码为例 当调用了 server.register(selector, SelectionKey.OP_ACCEPT)后Selector中维护了一个集合用于存放SelectionKey以及其对应的通道 // WindowsSelectorImpl 中的 SelectionKeyImpl数组
private SelectionKeyImpl[] channelArray new SelectionKeyImpl[8];public class SelectionKeyImpl extends AbstractSelectionKey {// Key对应的通道final SelChImpl channel;...
}当选择器中的通道对应的事件发生后selecionKey会被放到另一个集合中但是selecionKey不会自动移除所以需要我们在处理完一个事件后通过迭代器手动移除其中的selecionKey。否则会导致已被处理过的事件再次被处理就会引发错误 断开处理
当客户端与服务器之间的连接断开时会给服务器端发送一个读事件对异常断开和正常断开需要加以不同的方式进行处理 正常断开 正常断开时服务器端的channel.read(buffer)方法的返回值为-1所以当结束到返回值为-1时需要调用key的cancel方法取消此事件并在取消后移除该事件 int read channel.read(buffer);
// 断开连接时客户端会向服务器发送一个写事件此时read的返回值为-1
if(read -1) {// 取消该事件的处理key.cancel();channel.close();
} else {...
}
// 取消或者处理都需要移除key
iterator.remove(); 异常断开 异常断开时会抛出IOException异常 在try-catch的catch块中捕获异常并调用key的cancel方法即可
消息边界 不处理消息边界存在的问题
将缓冲区的大小设置为4个字节发送2个汉字你好通过decode解码并打印时会出现乱码
ByteBuffer buffer ByteBuffer.allocate(4);
// 解码并打印
System.out.println(StandardCharsets.UTF_8.decode(buffer));你这是因为UTF-8字符集下1个汉字占用3个字节此时缓冲区大小为4个字节一次读时间无法处理完通道中的所有数据所以一共会触发两次读事件。这就导致 你好 的 好 字被拆分为了前半部分和后半部分发送解码时就会出现问题
处理消息边界
传输的文本可能有以下三种情况
文本大于缓冲区大小 此时需要将缓冲区进行扩容发生半包现象发生粘包现象 解决思路大致有以下三种
固定消息长度数据包大小一样服务器按预定长度读取当发送的数据较少时需要将数据进行填充直到长度与消息规定长度一致。缺点是浪费带宽另一种思路是按分隔符拆分缺点是效率低需要一个一个字符地去匹配分隔符TLV 格式即 Type 类型、Length 长度、Value 数据也就是在消息开头用一些空间存放后面数据的长度如HTTP请求头中的Content-Type与Content-Length。类型和长度已知的情况下就可以方便获取消息大小分配合适的 buffer缺点是 buffer 需要提前分配如果内容过大则影响 server 吞吐量 Http 1.1 是 TLV 格式Http 2.0 是 LTV 格式 剩下文档直接跳网站 Netty学习之NIO基础 - Nyimas Blog