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对应的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();
  • 获取就绪事件并得到对应的通道,然后进行处理

// 获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件
Iterator<SelectionKey> 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);// 获取所有事件Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件Iterator<SelectionKey> 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基础 - Nyima's Blog

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# 友元程序集

1.友元程序集 使用友元程序集可以将internal成员提供给其他的友元程序集访问。 程序集FriendTest1.dll [assembly:InternalsVisibleTo("FriendTest2")] namespace FriendTest1 {internal class Friend{string name;public string Name > name;public Friend(str…

手敲myarraylist,深入了解其运行逻辑

1、自定义MyArrayList类 该类里面基本有两个属性&#xff0c;一个是用来存放数据的数组&#xff0c;另外一个是用来描述已经存放数据的数量。同时设置arraylist表的默认长度为10&#xff1b;代码如下&#xff1a; public class MyArrayList {private int[] elem;private int u…

kubernetes七层负载Ingress搭建(K8S1.23.5)

首先附上K8S版本及Ingress版本对照 Ingress介绍 NotePort&#xff1a;该方式的缺点是会占用很多集群机器的端口&#xff0c;当集群服务变多时&#xff0c;这个缺点就愈发的明显(srevice变多&#xff0c;需要的端口就需要多) LoadBalancer&#xff1a;该方式的缺点是每个servi…

Retrofit中的注解

一、Retrofit中的注解有那些&#xff1f; 方法注解&#xff1a;GET ,POST,PUT,DELETE,PATH,HEAD,OPTIONS,HTTP标记注解&#xff1a;FormUrlEncoded&#xff0c;Multpart&#xff0c;Streaming参数注解&#xff1a;Query&#xff0c;QueryMap&#xff0c;Body&#xff0c;Field…

JSP+servlet实现高校社团管理系统

JSPservlet实现的高校社团管理系统 &#xff0c;前后台都有&#xff0c;前台演示地址:高校社团管理系统 后台演示地址:登录 用户名:sys,密码:123456 前台功能&#xff1a;首页&#xff0c;社团列表&#xff0c;社团风采&#xff0c;社团活动&#xff0c;新闻列表&#xff0c…

阿里云新版公共实例从注册账号到创建设备生成参数教程

1 注册阿里云 打开阿里云官网&#xff0c;点击右上角的登录/注册 打开的界面按照图片输入手机号注册 注册成功后&#xff0c;登录返回第一次打开的界面&#xff0c;点击控制台 点击控制台后界面如下 点击左上角的菜单&#xff0c;弹出新窗口&#xff0c;搜索物联网平台 开通物…

springmvc(基础学习整合)

SpringMVC是Spring框架提供的构建Web应用程序的全功能MVC模块。 在SpringMVC的各个组件中&#xff0c;处理器映射器、处理器适配器、视图解析器称为SpringMVC的三大组件。 springMVC基本介绍&#xff1a; http://t.csdnimg.cn/TOzw9 MVC是一种设计思想&#xff0c;将一个应…

华为云cce容器管理中的调度策略作用

研究不深&#xff0c;但是这个还是挺重要的&#xff0c;在这里记录一下。 在cce节点集群中&#xff0c;有时候会发现有的节点实例过于饱满&#xff0c;有的又有些空&#xff0c;导致部分节点由于压力过大&#xff0c;存在崩溃的危险&#xff0c;这时候调度策略就有用了。 我这…

图扑参展高交会-全球清洁能源创新博览会

“相聚鹏城深圳&#xff0c;共享能源盛宴” 第二十五届中国国际高新技术成果交易会(简称“高交会”)于 11 月 15-18 日在深圳盛大开幕。高交会由商务部、科学技术部、工业和信息化部、国家发展改革委、农业农村部、国家知识产权局、中国科学院、中国工程院和深圳市人民政府共同…

nvm for windows使用与node/npm/yarn的配置

1 下载 nvm for windows download – github 下拉到Assets, 下载.exe文件 2 安装 安装到如下文件夹中 目录可以自己选, 可以换别的名字, 自己记住即可 新手建议全部看完再进行个人配置, 或者使用与博主一致的路径 D:\DevelopEnvironment\nvm3 配置nvm使用的镜像 node_mir…

Carbonyl ,一个可以在终端里运行的浏览器

浏览器对于我们的日常来说是使用频率比较高的一个东西。 一般来说&#xff0c;对于桌面的发行版的linux的浏览器&#xff0c;用的比较多的是Firefox浏览器。对于我们日常windows、mac等。常用的有chrome、edge等。 但是&#xff01;在终端里运行一个浏览器&#xff0c;我想大多…

SaaS模式C/S检验科LIS系统源码

适用于医院检验科实际需要的管理系统, 实现检验业务全流程的计算机管理。从检验申请、标本编号、联机采集、中文报告单的生成与打印、质控图的绘制和数据的检索与备份。通过将所有仪器自身提供的端口与科室LIS系统中的工作站点连接,实现与医院HIS系统的对接。 通过门诊医生和住…

HTML-CSS知识速查

HTML/CSS知识速查 文章目录 HTML/CSS知识速查[toc]网页的组成浏览器**为什么需要Web标准&#xff1a;** **web标准的构成&#xff1a;**HTMLHTML语法导读**1.1 HTML语法规则&#xff1a;**1.2 基本结构标签**1.3 标签的关系&#xff1a;**1. **包含关系&#xff08;Parent-Chil…

岩土工程监测新利器——振弦采集仪

岩土工程监测新利器——振弦采集仪 振弦采集仪是一种常用的岩土工程监测仪器&#xff0c;主要用于测量岩土体的振动和应变情况。它采用先进的数字信号处理技术&#xff0c;可以实时采集和处理振弦信号&#xff0c;快速准确地获取岩土体的振动和应变信息。 振弦采集仪具有以下优…

数据结构---树

树概念及结构 1.树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的 有一个特殊的结点&#xff0c…

在很多nlp数据集上超越tinybert 的新架构nlp神经网络模型

在很多nlp数据集上超越tinybert 的新架构nlp神经网络模型 网络结构图测试代码网络结构图 测试代码 import paddle import numpy as np import pandas as pd from tqdm import tqdmclass FeedFroward(paddle.nn.Layer):

TCP 基本认识

1&#xff1a;TCP 头格式有哪些&#xff1f; 序列号&#xff1a;用来解决网络包乱序问题。 确认应答号&#xff1a;用来解决丢包的问题。 2&#xff1a;为什么需要 TCP 协议&#xff1f; TCP 工作在哪一层&#xff1f; IP 层是「不可靠」的&#xff0c;它不保证网络包的交付…

SourceInsight - Relation Windows

磨刀不误砍柴工&#xff0c;你使用的工具决定了你的下限。我平时使用较多的代码编辑工具就是SourceInsight&#xff0c;这个工具速度快&#xff0c;操作方便&#xff0c;但处理非常大的项目的性能不是很理想&#xff0c;比如你要是添加整个Linux Kernel的源代码的话。 在使用SI…

什么是requestIdleCallback?和requestAnimationFrame有什么区别?

什么是requestIdleCallback? 我们都知道React 16实现了新的调度策略(Fiber), 新的调度策略提到的异步、可中断&#xff0c;其实就是基于浏览器的 requestIdleCallback和requestAnimationFrame两个API。 在 JavaScript 中&#xff0c;requestIdleCallback 是一个用于执行回调函…

Linguistic Steganalysis in Few-Shot Scenario论文阅读笔记

TIFS期刊 A类期刊 新知识点 Introduction Linguistic Steganalysis in Few-Shot Scenario模型是个预训练方法。 评估了四种文本加密分析方法&#xff0c;TS-CSW、TS-RNN、Zou、SeSy&#xff0c;用于分析和训练的样本都由VAE-Stego生产(编码方式使用AC编码)。 实验是对比在少样…