Netty实战专栏 | BIO详解

在这里插入图片描述

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉
🍎个人主页:Leo的博客
💞当前专栏: Netty实战专栏
✨特色专栏: MySQL学习
🥭本文内容: Netty实战专栏 | BIO详解
🖥️个人小站 :个人博客,欢迎大家访问
📚个人知识库: Leo知识库,欢迎大家访问

目录

    • 1.前言
    • 2.前置知识
      • 2.1阻塞IO和非阻塞IO
      • 2.2同步IO和非同步IO
    • 3.传统的BIO通信方式简介
    • 4.传统的BIO的问题
    • 4.BIO通信方式深入分析
      • 4.1BIO的工作机制
      • 4.2传统的BIO编程实例回顾
      • 4.3多发多收案例
        • 1.客户端编写
        • 2.服务端编写
        • 3.小结
      • 4.4BIO模式下接收多个客户端
        • 1.服务端编写
        • 2.客户端编写
      • 4.4Java BIO模式下的端口转发思想
        • 1.客户端代码编写
        • 2.客户端线程处理类
        • 3.服务端代码编写
        • 4.服务端线程处理类
        • 测试

学习参考 :

  • 讲师:孙帅老师
  • 课程:孙哥说netty

1.前言

大家好,我是Leo哥🫣🫣🫣,上一篇博客我们主要大致了解了一下Java中IO的演变历程,大体分为基础IO–>BIO–>NIO–>AIO这几个阶段,分部逐层递进。下面几个章节我们就要来聊聊这个变化。本节主要讨论关于BIO的知识。好了,话不多说让我们开始吧😎😎😎。

2.前置知识

在学习BIO之前,我们先了解一下IO相关的前置知识。

2.1阻塞IO和非阻塞IO

那什么是阻塞式 IO,什么是非阻塞 IO 呢?

阻塞IO:

在这种模型中,I/O 操作是阻塞的,即执行 I/O 操作时,线程会被阻塞,直到操作完成。在阻塞 I/O 模型中,每个连接都需要一个线程来处理。因此,对于大量并发连接的场景,阻塞 I/O 模型的性能较差。

在阻塞IO模型中,常见的阻塞操作包括从输入流中读取数据和向输出流中写入数据。例如,在读取文件内容时,程序会一直等待文件数据加载到内存中才能继续执行后续代码。同样地,向网络连接发送数据时,程序会一直等待数据发送完毕才能进行下一步操作。

阻塞IO的特点是简单直观,代码易于理解和编写。然而,它也存在一些潜在的问题。首先,如果IO操作时间较长(如读取大文件或网络延迟高),那么程序将会长时间地处于阻塞状态,影响整体的性能和响应性能。其次,阻塞IO模型通常是单线程的,即一次只能处理一个IO请求,这可能造成资源的浪费和效率低下。

image-20231103133810095

非阻塞IO:

为了解决阻塞IO模型的问题,Java引入了非阻塞IO(NIO)机制,使得程序能够在等待IO操作完成时继续执行其他任务,提高了系统的并发性能和响应速度。非阻塞IO模型使用选择器**(Selector)来管理多个通道(Channel)**,并通过轮询的方式检查通道上是否有可以进行IO操作的事件,从而实现高效的IO操作。

在这种模型中,I/O 操作不会阻塞线程。当数据尚未准备好时,I/O 调用会立即返回。线程可以继续执行其他任务,然后在适当的时候再次尝试执行 I/O 操作。非阻塞 I/O 模型允许单个线程同时处理多个连接,但可能需要在应用程序级别进行复杂的调度和管理。这种模型通常需要一个专门的线程来处理轮询操作。

**Java NIO(New IO)库提供了非阻塞IO支持。在NIO中,数据传输通过缓冲区(Buffer)进行,而通道(Channel)**则负责管理缓冲区和底层IO操作。其中,**选择器(Selector)**是实现非阻塞IO的关键组件之一。选择器可以注册多个通道,并监视它们上的IO事件,如读取、写入、连接等。通过轮询选择器上的事件,程序可以快速地响应IO事件,而不会被单个IO操作所阻塞。

与阻塞IO相比,非阻塞IO具有更高的并发性能和响应速度。非阻塞IO模型允许程序同时处理多个IO请求,并且可以在等待IO操作完成时继续执行其他任务,从而充分利用系统资源。然而,使用非阻塞IO也面临着更高的复杂性和编程难度,因为程序需要显式地处理缓冲区和事件轮询等细节。

image-20231103134101127

2.2同步IO和非同步IO

同步IO的概念是操作系统级别的IO了,若Java程序发起IO请求,若操作系统IO资源未准备好,同步IO不做出任何响应。而非同步IO则会返回一个标记,当IO资源准备完成后,再通过事件机制通知程序。

如下图所示,同步IO会因为IO资源未准备好而进入阻塞状态。

非同步IO则会通过事件机制,避免和操作系统级别的阻塞。

3.传统的BIO通信方式简介

以前大多数网络通信方式都是阻塞模式的,即:

  • 客户端向服务器端发出请求后,客户端会一直等待(不会再做其他事情),直到服务器端返回结果或者网络出现问题。
  • 服务器端同样的,当在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的这个处理线程完成上一个处理。

image-20231103135551587

4.传统的BIO的问题

  1. 低效率:在等待IO操作时,线程会一直处于阻塞状态,占用系统资源,无法响应其他请求。如果有大量的并发请求,那么线程将被频繁地切换,导致系统性能下降。
  2. 编程复杂度高:由于每个连接都需要一个独立的线程来处理,因此需要管理大量的线程。而线程管理是一项非常复杂的任务,容易出错。
  3. 可伸缩性差:由于每个连接都需要一个独立的线程来处理,线程数量可能会随着连接数的增加而急剧增加,导致系统崩溃或者性能急剧下降。
  4. 不支持异步IO:BIO只支持同步阻塞IO模型,无法实现异步IO,而异步IO在处理高并发和大吞吐量的场景中具有重要意义。

4.BIO通信方式深入分析

4.1BIO的工作机制

image-20231103140520774

对 BIO 编程流程的梳理

  1. 服务器端启动一个 ServerSocket,注册端口,调用accpet方法监听客户端的Socket连接。
  2. 客户端启动 Socket对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯

4.2传统的BIO编程实例回顾

网络编程的基本模型是Client/Server模型,也就是两个进程之
间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),
客户端通过连接操作向服务端监听的端口地址发起连接请
求,基于TCP协议下进行三次握手连接,连接成功后,双方
通过网络套接字(Socket)进行通信。

传统的同步阻塞模型开发中,服务端ServerSocket负责绑定
IP地址,启动监听端口;客户端Socket负责发起连接操作。连
接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的。

4.3多发多收案例

1.客户端编写
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**目标: Socket网络编程。Java提供了一个包:java.net下的类都是用于网络通信。Java提供了基于套接字(端口)Socket的网络通信模式,我们基于这种模式就可以直接实现TCP通信。只要用Socket通信,那么就是基于TCP可靠传输通信。功能1:客户端发送一个消息,服务端接口一个消息,通信结束!!创建客户端对象:(1)创建一个Socket的通信管道,请求与服务端的端口连接。(2)从Socket管道中得到一个字节输出流。(3)把字节流改装成自己需要的流进行数据的发送创建服务端对象:(1)注册端口(2)开始等待接收客户端的连接,得到一个端到端的Socket管道(3)从Socket管道中得到一个字节输入流。(4)把字节输入流包装成自己需要的流进行数据的读取。Socket的使用:构造器:public Socket(String host, int port)方法:  public OutputStream getOutputStream():获取字节输出流public InputStream getInputStream() :获取字节输入流ServerSocket的使用:构造器:public ServerSocket(int port)小结:通信是很严格的,对方怎么发你就怎么收,对方发多少你就只能收多少!!*/
public class ClientDemo {public static void main(String[] args) throws Exception {System.out.println("==客户端的启动==");// (1)创建一个Socket的通信管道,请求与服务端的端口连接。Socket socket = new Socket("127.0.0.1",8888);// (2)从Socket通信管道中得到一个字节输出流。OutputStream os = socket.getOutputStream();// (3)把字节流改装成自己需要的流进行数据的发送PrintStream ps = new PrintStream(os);// (4)开始发送消息ps.println("我是客户端,我想约吃烤肉!!!");ps.flush();}
}
2.服务端编写
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 服务端*/
public class ServerDemo {public static void main(String[] args) throws Exception {System.out.println("==服务器的启动==");// (1)注册端口ServerSocket serverSocket = new ServerSocket(8888);//(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道Socket socket = serverSocket.accept();//(3)从Socket管道中得到一个字节输入流。InputStream is = socket.getInputStream();//(4)把字节输入流包装成自己需要的流进行数据的读取。BufferedReader br = new BufferedReader(new InputStreamReader(is));//(5)读取数据String line ;while((line = br.readLine())!=null){System.out.println("服务端收到:"+line);}}
}
3.小结

在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!

4.4BIO模式下接收多个客户端

在上面的案例中,一个服务端只能接收到一个客户端传递过来的请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型。

image-20231103141359894

下面我们通过编写代码案例进一步深入。

1.服务端编写
/**服务端*/
public class ServerDemo {public static void main(String[] args) throws Exception {System.out.println("==服务器的启动==");// (1)注册端口ServerSocket serverSocket = new ServerSocket(7777);while(true){//(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道Socket socket = serverSocket.accept();new ServerReadThread(socket).start();System.out.println(socket.getRemoteSocketAddress()+"上线了!");}}
}
class ServerReadThread extends Thread{private Socket socket;public ServerReadThread(Socket socket){this.socket = socket;}@Overridepublic void run() {try{//(3)从Socket管道中得到一个字节输入流。InputStream is = socket.getInputStream();//(4)把字节输入流包装成自己需要的流进行数据的读取。BufferedReader br = new BufferedReader(new InputStreamReader(is));//(5)读取数据String line ;while((line = br.readLine())!=null){System.out.println("服务端收到:"+socket.getRemoteSocketAddress()+":"+line);}}catch (Exception e){System.out.println(socket.getRemoteSocketAddress()+"下线了!");}}
}
2.客户端编写
/**目标: Socket网络编程。功能1:客户端可以反复发,一个服务端可以接收无数个客户端的消息!!小结:服务器如果想要接收多个客户端,那么必须引入线程,一个客户端一个线程处理!!*/
public class ClientDemo {public static void main(String[] args) throws Exception {System.out.println("==客户端的启动==");// (1)创建一个Socket的通信管道,请求与服务端的端口连接。Socket socket = new Socket("127.0.0.1",7777);// (2)从Socket通信管道中得到一个字节输出流。OutputStream os = socket.getOutputStream();// (3)把字节流改装成自己需要的流进行数据的发送PrintStream ps = new PrintStream(os);// (4)开始发送消息Scanner sc = new Scanner(System.in);while(true){System.out.print("请说:");String msg = sc.nextLine();ps.println(msg);ps.flush();}}
}

注意:

  1. 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能。
  2. 每个线程都会占用栈空间和CPU资源。
  3. 并不是每个socket都进行IO操作,无意义的线程处理。
  4. 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

4.4Java BIO模式下的端口转发思想

需要实现一个客户端的消息可以发送给所有的客户

image-20231103142118923

1.客户端代码编写
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {public static void main(String[] args) {try {//1.请求与服务端的Socket对象连接Socket socket = new Socket("127.0.0.1", 9999);//收消息Thread clientThread = new ClientReaderThread(socket);clientThread.start();while (true) {//发消息OutputStream os = socket.getOutputStream();PrintStream ps = new PrintStream(os);//3. 使用循环不断的发送消息给服务端接收Scanner sc = new Scanner(System.in);//System.out.print("client send message:");String msg = sc.nextLine();ps.println(msg);ps.flush();}} catch (Exception e) {e.printStackTrace();}}
}
2.客户端线程处理类
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ClientReaderThread extends Thread {private Socket socket;public ClientReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {while (true) {InputStream is = socket.getInputStream();//4.把字节输入流包装成一个缓存字符输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;if ((msg = br.readLine()) != null) {System.out.println(msg);}}} catch (Exception e) {}}
}
3.服务端代码编写
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/*** 目标:BIO模式下的端口转发思想-服务端实现* 服务端实现需求:* 1.注册端口* 2.接收客户端的socket连接,交给一个独立的线程来处理* 3.把当前连接的客户端socket存入到一个所谓的在线socket集合中保存* 4.接收客户端的消息,然后推动给当前所有的在线socket接收*/
public class Server {//定义一个静态集合public static List<Socket> allSocketOnLine = new ArrayList<>();public static void main(String[] args) {try {ServerSocket ss = new ServerSocket(9999);while (true) {Socket socket = ss.accept();//把登录的客户端socket存入到一个在线集合中去allSocketOnLine.add(socket);//为当前登录成功的socket分配一个独立的线程来处理与之通信new ServerReaderThread(socket).start();}} catch (Exception e) {e.printStackTrace();}}
}
4.服务端线程处理类
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReaderThread extends Thread {private Socket socket;public ServerReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {//1.从socket中去获取当前客户端的输入流BufferedReader br = new BufferedReader(newInputStreamReader(socket.getInputStream()));String msg;while ((msg = br.readLine()) != null) {System.out.println("服务器收到消息:" + msg);//2.服务端接收到了客户端的消息后,需要推送给所有的当前在线的socketsendMsgToAllClient(msg, socket);}} catch (Exception e) {e.printStackTrace();System.out.println("当前有人下线了!");//从在线socket集合中移除本socketServer.allSocketOnLine.remove(socket);}}/*** 把当前客户端发送来的消息推送给全部在线的socket** @param msg*/private void sendMsgToAllClient(String msg, Socket socket) throws Exception {for (Socket sk : Server.allSocketOnLine) {//只发送给除自己以外的其他客户端if (socket != sk) {PrintStream ps = new PrintStream(sk.getOutputStream());ps.println(msg);ps.flush();}}}
}
测试

先启动服务端再启动客户端

服务器收到消息:大家好,我是客户端一
服务器收到消息:哈哈哈哈
服务器收到消息:大家好,我是client2
服务器收到消息:嘻嘻嘻嘻
服务器收到消息:hello everyone
服务器收到消息:i am client3#客户端一
大家好,我是客户端一 --发送
哈哈哈哈 --发送
大家好,我是client2 --接收
嘻嘻嘻嘻 --接收
hello everyone --接收
i am client3 --接收#客户端二
大家好,我是客户端一 --接收
哈哈哈哈 --接收
大家好,我是client2 --发送
嘻嘻嘻嘻 --发送
hello everyone --接收
i am client3 --接收#客户端三
大家好,我是客户端一 --接收
哈哈哈哈 --接收
大家好,我是client2 --接收
嘻嘻嘻嘻 --接收
hello everyone --发送
i am client3 --发送

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

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

相关文章

Fourier分析导论——第3章——Fourier级数的收敛性(E.M. Stein R. Shakarchi)

第 3 章 Fourier级数的收敛性(Convergence of Fourier Series) The sine and cosine series, by which one can represent an arbitrary function in a given interval, enjoy among other remarkable properties that of being convergent. This property did not escape…

Selenium元素定位之页面检测技巧

在进行web自动化测试的时候进行XPath或者CSS定位&#xff0c;需要检测页面元素定位是否正确&#xff0c;如果用脚本去检测&#xff0c;那么效率是极低的。 一般网上推选装额外的插件来实现页面元素定位检测 如&#xff1a;firebug。 其实F12开发者工具就能直接在页面上检测元…

云安全—K8S API Server 未授权访问

0x00 前言 master节点的核心就是api服务&#xff0c;k8s通过REST API来进行控制&#xff0c;在k8s中的一切都可以抽象成api对象&#xff0c;通过api的调用来进行资源调整&#xff0c;分配和操作。 通常情况下k8s的默认api服务是开启在8080端口&#xff0c;如果此接口存在未授…

SpringCloud Alibaba 【四】Openfeign

Openfeign配置与使用 前言介绍openfeign使用openfeign导入依赖启动类正式使用测试结果 前言 在springcloud中消费者项目需要调用提供者项目的接口&#xff0c;一开始用的是RestTemplate中的方法。但是RestTemplate进行远程调用时&#xff0c;直接调用controller层的接口&#…

图论08-图的建模-状态的表达与理解 - 倒水问题为例

文章目录 状态的表达例题1题解1 终止条件&#xff1a;有一个数位为42 状态的改变&#xff1a;a表示十位数&#xff0c;b表示个位数3 其他设置 例题2 力扣773 滑动谜题JavaC 状态的表达 例题1 从初始的(x&#xff0c;y)状态&#xff0c;到最后变成&#xff08;4&#xff0c;&am…

JAVA提取嵌套夹带文件之Apache Tika

目录结构 前言tika简介Tika支持的文件格式MAVEN依赖JAVA程序JAVA测试程序测试文件测试结果部分文件提取失败参考连接 前言 Apache Tika提取文件整理如下&#xff0c;如有特定的文件需要提取可以先参照【部分文件提取失败】章节对照&#xff0c;以免浪费您的宝贵时间&#xff0c…

3.Docker的客户端指令学习与实战

1.Docker的命令 1.1 启动Docker&#xff08;systemctl start docker&#xff09; systemctl start docker1.2 查看docker的版本信息&#xff08;docker version&#xff09; docker version1.3 显示docker系统范围的信息&#xff08;docker info&#xff09; docker info1.4…

夯实c语言基础(2)

夯实c语言基础&#xff08;2&#xff09; 题干执行以下程序段执行后的输出结果分别为&#xff08;  b &#xff09;、&#xff08; a  &#xff09;、&#xff08;  a &#xff09;、&#xff08;  d &#xff09;。 程序段一&#xff1a; int i1,a0; while(i<…

亚马逊云科技:让生成式AI真正走向普惠

伴随着ChatGPT的横空出世&#xff0c;生成式AI&#xff08;Artificial Intelligence Generated Content&#xff0c;也称AIGC&#xff09;大潮也以锐不可当之势席卷全球。从各行各业的商业领袖&#xff0c;到千千万万的程序员和开发者&#xff0c;都在思考如何借助生成式AI技术…

Git https方式拉的代码IDEA推送代码报错

报错信息 fatal: could not read Username for ‘https://codehub-cn-south-1.devcloud.huaweicloud.com’: No such file or directory 18:18:39.885: [recovery_pattern] git -c credential.helper -c core.quotepathfalse -c log.showSignaturefalse push --progress --porc…

Portraiture4.0介绍与插件安装包下载

相信有很多需要经常进行图像处理的小伙伴的电脑上都有一款PS软件吧&#xff0c;PS的功能非常强大&#xff0c;各种细节处理都非常细致&#xff0c;但还是需要一些插件来帮我们快速处理图片&#xff0c;能够省去很多时间和精力。今天给大家介绍一款PS磨皮插件&#xff0c;能够快…

路由器基础(十一):ACL 配置

访问控制列表 (Access Control List,ACL) 是目前使用最多的访问控制实现技术。访问控制列表是路由器接口的指令列表&#xff0c;用来控制端口进出的数据包。ACL适用于所有的被路由协议&#xff0c;如IP、IPX、AppleTalk 等。访问控制列表可以分为基本访问控制列表和高级访问控制…

【IDEA】在工具栏设置快速创建包和类的图表

页面效果&#xff1a; 操作步骤&#xff1a; 设置 --> 外观与行为 --> 菜单与工具栏 --> 点击 主工具栏 --> 点击 ---- --> 点击 号 --> 添加操作 主菜单 --> 文件 --> 文件打开操作 --> 打开项目操作 --> 新建 --> 往下找 找到 clas…

【漏洞库】XXL-JOB 默认accessToken权限绕过导致RCE

文章目录 漏洞描述漏洞编号漏洞评级影响版本漏洞复现- EXP 编写 漏洞挖掘修复建议 漏洞描述 XXL-JOB 是一款开源的分布式任务调度平台&#xff0c;用于实现大规模任务的调度和执行。 XXL-JOB 默认配置下&#xff0c;用于调度通讯的 accessToken 不是随机生成的&#xff0c;而…

一个使用uniapp+vue3+ts+pinia+uview-plus开发小程序的基础模板

uniappuviewPlusvue3tspiniavite 开发基础模板 使用 uniapp vue3 ts pinia vite 开发基础模板&#xff0c;拿来即可使用&#xff0c;不要删除 yarn.lock 文件&#xff0c;否则会启动报错&#xff0c;这个可能和 pinia 的版本有关&#xff0c;所以不要随意修改。 拉取代码…

银行账单转换beancount

用了beancount来记账后&#xff0c;发现每月的账单手动记是一件极其麻烦的事情。 然后再github搜索一通后&#xff0c;有double-entry-generator&#xff08;https://github.com/deb-sig/double-entry-generator&#xff09;能转换支付宝/微信的账单&#xff0c;但是没有自己用…

Android工具栏ToolBar

主流APP除了底部有一排标签栏外&#xff0c;通常顶部还有一排导航栏。在Android5.0之前&#xff0c;这个顶部导航栏以ActionBar控件的形式出现&#xff0c;但AcionBar存在不灵活、难以扩展等毛病&#xff0c;所以Android5.0之后推出了ToolBar工具栏控件&#xff0c;意在取代Aci…

绝地求生msvcp140.dll丢失报错怎么办,这四个方法都可以解决

在回答这个问题之前&#xff0c;我们先来了解一下什么是msvcp140.dll。msvcp140.dll是微软Visual C 2015 Redistributable的一个组件&#xff0c;它包含了许多运行库文件&#xff0c;用于支持各种应用程序的正常运行。当你在玩《绝地求生》&#xff08;俗称“吃鸡”&#xff09…

【vscode远程开发】使用内网穿透实现在公网环境下远程访问

文章目录 前言1、安装OpenSSH2、vscode配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 前言 远程…