NIO网络编程

 Netty学习之NIO基础 - Nyima's Blog

1、阻塞

  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在通道中没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32 位 jvm 一个线程 320k,64 位 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));
            // 用户存放连接的集合
            ArrayList<SocketChannel> 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));
            // 用户存放连接的集合
            ArrayList<SocketChannel> 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);
                
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                
                // 使用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    
                    // 判断key的类型
                    if(key.isAcceptable()) {
                        // 获得key对应的channel
                        ServerSocketChannel 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();
  • 获取就绪事件并得到对应的通道,然后进行处理

// 获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
                
// 使用迭代器遍历事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();

while (iterator.hasNext()) {
	SelectionKey key = iterator.next();
                    
	// 判断key的类型,此处为Accept类型
	if(key.isAcceptable()) {
        // 获得key对应的channel
        ServerSocketChannel 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);
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 使用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 判断key的类型
                    if(key.isAcceptable()) {
                        // 获得key对应的channel
                        ServerSocketChannel 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基础 - Nyima's Blog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/211622.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开源软件license介绍与检测

开源License介绍 通俗来讲&#xff0c;开源许可证就是一种允许软件使用者在一定条件内按照需要自由使用和修改软件及其源代码的的法律条款。借此条款&#xff0c;软件作者可以将这些权利许可给使用者&#xff0c;并告知使用限制。这些许可条款可以由个人、商业公司或非赢利组织…

mediapipe+opencv实现保存图像中的人脸,抹去其他信息

mediapipeopencv MediaPipe本身不提供图像处理功能&#xff0c;它主要用于检测和跟踪人脸、手势、姿势等。如果您想要从图像中仅提取人脸主要信息并去除其他信息. # codingutf-8 """project: teatAuthor&#xff1a;念卿 刘file&#xff1a; test.pydate&…

2022年8月2日 Go生态洞察:Go 1.19版本发布深度解析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Tomcat目录介绍

目录 1 Tomcat主目录介绍 2 webapps目录介绍 3 Tomcat配置文件目录介绍&#xff08;conf&#xff09; 1 Tomcat主目录介绍 进入Tomcat目录下&#xff0c;我的目录是/application/tomcat/ cd /application/tomcat/ 安装tree命令 yum -y install tree tree -L 1 tree&…

AD使用交互式BOM插件时应该注意到的一个问题

交互式BOM脚本 插件下载&#xff1a;GitHub - lianlian33/InteractiveHtmlBomForAD 或&#xff1a;GitHub - yukaichao/InteractiveHtmlBom-plugin-for-AD 或&#xff1a;InteractiveHtmlBomForAD-master.zip - 蓝奏云 问题&#xff1a;当PCB文件中出现MARK点时&#xff0c;…

MMseqs2蛋白质序列快速高效比对工具

先看仓库&#xff1a;soedinglab/MMseqs2: MMseqs2: ultra fast and sensitive search and clustering suite (github.com) 无论哪个工具软件&#xff0c;无论你是否熟悉&#xff0c;都推荐你看一下作者原文&#xff0c;这样后面的步骤以及怎么使用头脑里会更清晰。 Fast an…

探索意义的深度:自然语言处理中的语义相似性

一、说明 语义相似度&#xff0c;反应出计算机对相同内容&#xff0c;不同表达的识别能力。因而识别范围至少是个句子&#xff0c;最大范围就是文章&#xff0c;其研究方法有所区别。本文将按照目前高手的研究成绩&#xff0c;作为谈资介绍给诸位。 二、语义相似度简介 自然语言…

Build task failed. Open the Run window to view details.

问题描述 在使用deveco-studio 开发工具进行HarmonyOS第一个应用构建开发时&#xff0c;通过Previewer预览页面时报错&#xff0c;报错信息为&#xff1a;Build task failed. Open the Run window to view details. 如图所示&#xff1a; 问题解决 在设置中&#xff0c;找到…

文件操作-IO

文件操作-IO 1.认识文件1.什么是文件2.文件路径 2.java中操作文件2.1 File类的用法 3.InputStream和OutputStream的用法 1.认识文件 1.什么是文件 文件是计算机系统中用来存储数据的基本单位。它是一种用于持久性存储数据的命名、有序的数据集合.计算机系统通过文件系统来组织…

Simple_SSTI_1-WEB-bugku-解题步骤

——CTF解题专栏—— 声明&#xff1a;文章由作者weoptions学习或练习过程中的步骤及思路&#xff0c;非正式答案&#xff0c;仅供学习和参考。 题目信息&#xff1a; 题目&#xff1a;Simple_SSTI_1 作者&#xff1a;valecalida 提示&#xff1a;无 场景&#xff1a; 解题…

Centos图形化界面封装OpenStack Ubuntu镜像

目录 背景 环境 搭建kvm环境 安装ubuntu虚机 虚机设置 系统安装 登录虚机 安装cloud-init 安装cloud-utils-growpart 关闭实例 删除细节信息 删除网卡细节 使虚机脱离libvirt纳管 结束与验证 压缩与转移 验证是否能够正常运行 背景 一般的镜像文件在上传OpenSt…

HttpServletRequest/Response视频笔记

学习地址&#xff1a;144-尚硅谷-Servlet-HttpServletRequest类的介绍_哔哩哔哩_bilibili 目录 1.HttpServletRequest 类 a.HttpServletRequest类有什么作用 b.HttpServletRequest类的常用方法 c.如何获取请求参数 d.解决post请求中文乱码问题 获取请求的参数值相关问题 …

docker搭建nginx实现负载均衡

安装nginx 查询安装 [rootlocalhost ~]# docker search nginx [rootlocalhost ~]# docker pull nginx准备 创建一个什么都不配置的nginx拿到一个nginx.conf文件和conf.d文件夹 创建文件、文件夹&#xff08;只需创建logs文件夹即可&#xff0c;log文件是运行后自动挂载的&a…

2023.12.2 做一个后台管理网页(左侧边栏实现手风琴和隐藏/出现效果)

2023.12.2 做一个后台管理网页&#xff08;左侧边栏实现手风琴和隐藏/出现效果&#xff09; 网页源码见附件&#xff0c;比较简单&#xff0c;之前用很多种方法实现过该效果&#xff0c;这次的效果相对更好。 实现功能&#xff1a; &#xff08;1&#xff09;实现左侧边栏的手…

node.js express路由和中间件

目录 路由 解释 使用方式 中间件 解释 使用方式 中间件类型 路由注册和中间件注册 代码 app全局路由接口请求以及代码解析 示例1 示例2 示例3 示例4 中间件req继承 嵌套子路由 解释 代码 示例1 路由 解释 在 Express 中&#xff0c;路由&#xff08;Route&…

【大模型】更强的 ChatGLM3-6B 来了,开源可商用

【大模型】更强的 ChatGLM3-6B 来了&#xff0c;开源可商用 简介ChatGLM3-6B 环境配置环境搭建安装依赖 代码及模型权重拉取拉取 ChatGLM3-6B拉取 ChatGLM3-6B 模型权重及代码 终端测试网页测试安装 gradio加载模型并启动服务 参考 简介 ChatGLM3-6B ChatGLM3-6B 是 ChatGLM …

Python逐步打造惊艳的折线图

大家好&#xff0c;Matplotlib可以快速轻松地使用现成的函数绘制图表&#xff0c;但是微调步骤需要花费更多精力。今天本文将介绍如何使用Matplotlib绘制吸引人的图表&#xff0c;实现折线图的惊艳变身。 1.数据 为了说明方法&#xff0c;本文使用了包含过去50年各国GDP信息的…

短线买入卖出有哪些交易技巧?

前面两节课&#xff0c;我们认识了短线交易&#xff0c;知道了短线交易常见的买入卖出时机&#xff0c;这节课&#xff0c;我们来讲解一下短线买入卖出的一些交易技巧。话不多时&#xff0c;直接进入重点&#xff01; 一、短线交易要果断 短线波动快&#xff0c;在出现买卖信号…

Redis集群详解

1.1 什么是Redis集群 Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许Redis在不同节点上同时提供服务&#xff0c;提高整体性能和可靠性。根据搭建的方式和集群的特性&#xff0c;Redis集群主要有三种模式&#xff1a;主从复制…

Vue基础知识点梳理

在Vue中&#xff0c;被用来响应地更新HTML属性的指令是v-model页面挂载成功之后会触发哪一个钩子函数mounted挂载之后会进行页面的渲染v-on是动作元素不属于条件渲染指令 在Vue中&#xff0c;下列关于Vue实例对象说法不正确的是&#xff08;&#xff09;。A.Vue实例对象是通过n…