骗子会利用钓鱼网站做啥,重庆网站推广效果,如何做色流量网站,微信公众号小程序有哪些功能作者简介#xff1a;☕️大家好#xff0c;我是Aomsir#xff0c;一个爱折腾的开发者#xff01; 个人主页#xff1a;Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客 当前专栏#xff1a;Netty应用专栏_Aomsir的博客-CSDN博客 文章目录 参考文献前言问题解… 作者简介☕️大家好我是Aomsir一个爱折腾的开发者 个人主页Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客 当前专栏Netty应用专栏_Aomsir的博客-CSDN博客 文章目录 参考文献前言问题解决如何解决实战编码 Selector详解keysselectedKeysselect()方法 代码问题及改进问题解决代码演示 拓展知识总结 参考文献 孙哥suns说NettyNetty官方文档 前言
在我们的上一篇文章中我们详细讲解了如何使用NIO进行网络通信并成功解决了服务端的两次阻塞问题。这种解决方案有效地改善了通信效率。然而引入非阻塞机制后又产生了一个新的问题。我们注意到在没有客户端请求和IO通信的情况下上篇文章中的while循环会持续运行导致CPU资源的浪费。更为复杂的是我们的程序是单线程运行的所有的请求接收和IO通信都由这一个线程处理这无疑进一步拉低了CPU的利用率。
问题解决
如何解决
为了解决这个问题我们可以引入一个“监管者”负责监控客户端的请求和IO通信。这个“监管者”会专注于监控ServerSocketChannel的ACCEPT状态以及SocketChannel的READ和WRITE状态。只有当这些状态被触发时监管者才会进行处理。在NIO中我们有一个名为Selector的组件它可以承担这个监管者的角色。
实战编码
现在让我们通过实战编码来看看如何实现这个解决方案。通过引入Selector我们成功地解决了while循环空转的问题将阻塞的责任转交给了selector。这样我们的程序就不会再发生阻塞了。我们的selector会监控ServerSocketChannel的ACCEPT事件监控到了ACCEPT以后就会去获取对应的客户端SocketChannel监控它的READ和WRITE事件。 请参考以下代码和相关注释进行理解。在接下来的内容中我们会逐步详细解释这个过程。
注意⚠️它是一个单线程
public class MyServer2 {public static void main(String[] args) throws Exception{ServerSocketChannel serverSocketChannel ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8000));// Selector只在非阻塞下可用serverSocketChannel.configureBlocking(false);// 引入监管者Selector selector Selector.open();// 将ssc注册到selector上,返回一个SelectionKey,用于设置监控ACCEPT状态// SelectionKey: 将来事件发生后,通过它可以知道来自哪个Channel和哪个事件SelectionKey selectionKey serverSocketChannel.register(selector, 0, null);selectionKey.interestOps(SelectionKey.OP_ACCEPT);// 监控while (true) {// 开始监控,此处会阻塞,直到监控到有客户端请求和实际的连接或读写操作才会继续往下执行// 监控到以后会将实际的ssc或者sc保存至 SelectionKeys(HashSet)里,然后放行selector.select();// 从监控到的SelectionKeys中获取到实际的IteratorSelectionKey iterator selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();// 获取到后移除,防止重复处理iterator.remove();// 判断SelectionKey事件类型if (key.isAcceptable()) {// 获取到ssc从而获取到scServerSocketChannel channel (ServerSocketChannel) key.channel();SocketChannel sc channel.accept();sc.configureBlocking(false);// 将获取的sc注册到selector上,返回一个SelectionKey,用于设置监控READ状态SelectionKey scKey sc.register(selector, 0, null);scKey.interestOps(SelectionKey.OP_READ);System.out.println(accept sc);} else if (key.isReadable()) {try {// 通过SelectionKey获取到sc,然后读取数据SocketChannel sc (SocketChannel) key.channel();ByteBuffer buffer ByteBuffer.allocate(10);int read sc.read(buffer);if (-1 read) {// 客户端处理完毕key.cancel();} else {buffer.flip();System.out.println(Charset.defaultCharset().decode(buffer).toString() Charset.defaultCharset().decode(buffer));}} catch (IOException e) {e.printStackTrace();key.cancel();}}}}}
}Selector详解
Selector里面有很多的细节我会带着一点点的去剖析方便对整个程序有一个清晰的认知。
keysselectedKeys
在Selector中我们主要关注两种keys。
第一种是keys这是我们在将channel注册到selector的时候获取到的SelectionKey。这个key是在注册过程中获取的而不是由selector在监控特定事件后获取。一旦channel注册成功这个key就会被添加到keys列表中。这个key的主要作用是为特定的channel设置需要监控的事件。
第二种是selectedKeys这是我们在事件触发后通过调用selectedKeys()方法获取的key它是在事件触发后从keys列表中复制到selectedKeys列表中去的。这些selectedKeys对应的channel都是实际要发生事件的例如ACCEPT、READ、WRITE等。所以说当我们从selectedKeys中取出一个key后要将其移出以免出现异常
总的来说keys列表包含了所有注册的channel和其事件信息这是一个较大的范围。而selectedKeys列表则是一个较小的范围它来自于keys只包含当前实际发生事件的channel。比如我开了个直播课有100个人报名这100个人在keys里实际直播的时候有80人这80人同时在selectedKeys里
select()方法
Selector的select()方法是一个会产生阻塞的方法。它会定期轮询在Selector中注册的所有SelectionKey也就是keys并监控与这些key关联的Channel的状态如果有对应事件发生例如有新的连接请求或者有数据可读/可写则将对应的key添加到selectedKeys列表中并放行让程序处理这些事件。
如果在调用select()方法时没有任何事件发生那么该方法会阻塞直到有事件发生为止。这样可以避免程序在没有任何事件发生时不断轮询浪费CPU资源。
如果服务端的buffer设置得太小可能会导致服务端一次无法处理所有的数据。在这种情况下当buffer被填满后服务端会处理这第一部分数据然后结束因为这些未处理的数据会被视为新的事件。简言之就是说如果buffer需要两次才能读完客户端发送的一条数据那这个channel会被selector监控到两次read事件
代码问题及改进
问题
未处理半包与粘包问题处理的过程中一段数据被分成了几个事件但是每个buffer是独属某一个事件的新的事件就是一个新的buffer怎么解决解决半包粘包后如果buffer设置的小从SocketChannel中读取的数据还没遇到\n那buffer切换写模式压缩去等剩余数据写进来等于白干程序会被空转调用怎么解决服务端从SocketChannel已经读取完数据了后续没有通信了服务端没有去主动断开连接那select岂不是每次轮询都得带着这些不会产生通信的keys服务端没有处理异常
解决
对于第一个问题我们可以用先前的doLineSpilt方法处理半包粘包然后我们可以给每一个SocketChannel设置一个附件(att)在注册到selector的时候进行绑定在处理其读写事件的时候取出来使用这样粘包粘包压缩的数据就会一直都在了(只要key没有被删除即channel没有断开那就是同一个Channel)对于第二个问题我们可以在处理半包粘包后检查一下buffer的limit和position是否相等如果在处理半包粘包后两者相等说明buffer里是满的这时我们创建新的buffer进行扩容将新buffer作为附件绑定即可对于第三个问题客户端和服务端达成协议比如客户端不发数据代表通信结束那服务端从channel读不出来数据(返回值为-1)时则调用SelectionKey的cancle方法从keys中删除对于第四个问题处理异常就可以了
代码演示
public class MyServer4 {public static void main(String[] args) throws Exception{ServerSocketChannel serverSocketChannel ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8000));// Selector只在非阻塞下可用serverSocketChannel.configureBlocking(false);// 引入监管者Selector selector Selector.open();// 让serverSocketChannel被selector管理,它只处理accept,所以附件为nullSelectionKey selectionKey serverSocketChannel.register(selector, 0, null);// 监控acceptselectionKey.interestOps(SelectionKey.OP_ACCEPT);System.out.println(MyServer.main);// 监控while (true) {selector.select();System.out.println(------------111-------------);IteratorSelectionKey iterator selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();// 取出来就从selectedKeys中删除iterator.remove();if (key.isAcceptable()) {// ServerSocketChannel、获取的是最开始创建的,可以直接使用上面创建的ServerSocketChannel channel (ServerSocketChannel) key.channel();SocketChannel sc channel.accept();sc.configureBlocking(false);// 给每个SocketChannel绑定一个buffer,监控sc状态ByteBuffer buffer ByteBuffer.allocate(7);SelectionKey scKey sc.register(selector, 0, buffer);scKey.interestOps(SelectionKey.OP_READ);System.out.println(accept sc);} else if (key.isReadable()) {try {// 监控到key是读时间,获取到SocketChannel和bufferSocketChannel sc (SocketChannel) key.channel();ByteBuffer buffer (ByteBuffer) key.attachment(); // 获取附件中的bufferint read sc.read(buffer);if (-1 read) {// 客户端处理完毕key.cancel();} else {doLineSplit(buffer);// 没有压缩动,需要扩容if (buffer.position() buffer.limit()) {// 1、空间扩大ByteBuffer newBuffer ByteBuffer.allocate(buffer.capacity() * 2);// 2、老缓冲区数据复制进新缓冲区buffer.flip();newBuffer.put(buffer);// 3、绑定channel// buffer newBuffer;key.attach(newBuffer);}}} catch (IOException e) {e.printStackTrace();key.cancel();}}}}}private static void doLineSplit(ByteBuffer buffer) {buffer.flip(); // 读模式for (int i 0; i buffer.limit(); i) {if (buffer.get(i) \n) {int length i 1 - buffer.position(); // 以免出现一行里面有多个\nByteBuffer target ByteBuffer.allocate(length);for (int j 0; j length; j) {target.put(buffer.get());}// 截取工作完成target.flip();System.out.println(StandardCharsets.UTF_8.decode(target) StandardCharsets.UTF_8.decode(target));target.clear();}}// 写模式(压缩)buffer.compact();}
}拓展知识
对于网络编程中常见的半包和粘包问题我们有多种解决策略。一种简单且常用的方法是添加特定的标识符如换行符\n用于区分数据包的边界。另一种更为复杂但也更为精确的方法是采用类似HTTP协议的头体分离策略。在这种策略中我们将数据分为头部和体部两部分。头部包含元数据信息例如体部数据的大小等关键信息。体部则包含实际的数据内容。通过这种方式我们可以清晰地区分每个数据包从而有效解决半包和粘包问题。
总结
在今天的学习中我们深入探讨了如何利用Java NIO的Selector来高效地监控我们的服务器端程序从而避免无意义的空转。我们对Selector进行了深入剖析透彻理解了其工作原理。进一步地我们逐步优化了我们的程序提高了其性能和效率。这一系列的学习和实践为我们接下来的Netty学习铺设了坚实的基础。Netty作为一个基于Java NIO的网络应用框架我们对其的掌握将在未来的编程道路上发挥重要作用。