Netty Review - 从BIO到NIO的进化推演

文章目录

  • BIO
    • DEMO 1
    • DEMO 2
    • 小结论
    • 单线程BIO的缺陷
    • BIO如何处理并发
    • 多线程BIO服务器的弊端
  • NIO
    • NIO要解决的问题
    • 模拟NIO
      • 方案一: (等待连接时和等待数据时不阻塞)
      • 方案二(缓存Socket,轮询数据是否准备好)
      • 方案二存在的问题
    • NIO是如何解决这些问题的
    • 使用select/poll/epoll和直接在应用层做轮询的区别
      • select底层逻辑
      • poll的底层逻辑
      • epoll的底层逻辑

在这里插入图片描述


BIO

要讲明白BIO和NIO,首先我们应该自己实现一个简易的服务器,单线程即可。

DEMO 1

package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Server {public static void main(String[] args) {// BIO 面向字节byte[] bytes = new byte[1024];ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(1234);System.out.println("服务端已开启端口");while (true) {System.out.println();System.out.println("服务端等待连接......");Socket socket = serverSocket.accept();System.out.println("服务端已经收到连接请求");System.out.println("服务端等待数据.....");socket.getInputStream().read(bytes);System.out.println("服务端等已收到数据.....");String msg = new String(bytes);System.out.println("接收到了数据: " + msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

创建了一个服务端类,在类中实现实例化了一个SocketServer并绑定了1234端口。之后调用accept方法来接收连接请求,并且调用read方法来接收客户端发送的数据。最后将接收到的数据打印。

package com.artisan.bio;import java.io.IOException;
import java.net.Socket; /*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Client {public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1",1234);socket.getOutputStream().write("数据数据数据".getBytes());socket.close();}
}

客户端,首先实例化Socket对象,并且绑定ip为127.0.0.1(本机),端口号为1234,调用write方法向服务器发送数据

运行测试会发现

在服务器启动后,客户端还没有连接服务器时,服务器由于调用了accept方法,将一直阻塞,直到有客户端请求连接服务器


DEMO 2

客户端的逻辑主要是:建立Socket –> 连接服务器 –> 发送数据,我们的数据是在连接服务器之后就立即发送的,现在我们来对客户端进行一次扩展,当我们连接服务器后,不立即发送数据,而是等待控制台手动输入数据后,再发送给服务端

客户端代码如下

 try {Socket socket = new Socket("127.0.0.1",1234);String message = null;Scanner sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());socket.close();sc.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}

小结论

在这里插入图片描述

从上面的运行结果中我们可以看到,服务器端在启动后:

1)首先需要等待客户端的连接请求(第一次阻塞)

2)如果没有客户端连接,服务端将一直阻塞等待;

3)然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞)

4)如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。


服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程:

  • 1)第一次在等待连接时阻塞;
  • 2)第二次在等待数据时阻塞。

BIO会产生两次阻塞,这就是BIO的非常重要的一个特点


单线程BIO的缺陷

当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之:在不考虑多线程的情况下,BIO是无法处理多个客户端请求的


BIO如何处理并发

单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢?

我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。

【多线程BIO版本简易实现】

 package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ServerMultThread {public static void main(String[] args) {byte[] buffer = new byte[1024];try {ServerSocket serverSocket = new ServerSocket(1234);System.out.println("服务器已启动并监听8080端口");while (true) {System.out.println();System.out.println("服务器正在等待连接...");Socket socket = serverSocket.accept();new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 服务器已接收到连接请求...");System.out.println();System.out.println(Thread.currentThread().getName() + "服务器正在等待数据...");try {socket.getInputStream().read(buffer);} catch (IOException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "服务器已经接收到数据");System.out.println();String content = new String(buffer);System.out.println(Thread.currentThread().getName() + "接收到的数据:" + content);}).start();}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}

多启动几个客户端,测试结果如下

在这里插入图片描述


多线程BIO服务器的弊端

多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力

所以:如果这种不活跃的线程比较多,我们应该采取单线程的一个解决方案,但是单线程又无法处理并发,这就陷入了一种很矛盾的状态,于是就有了NIO


NIO

NIO要解决的问题

我们先来看看单线程模式下BIO服务器的代码,其实NIO需要解决的最根本的问题就是存在于BIO中的两个阻塞,分别是等待连接时的阻塞和等待数据时的阻塞

package com.artisan.bio;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class Server {public static void main(String[] args) {// BIO 面向字节byte[] bytes = new byte[1024];ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(1234);System.out.println("服务端已开启端口");while (true) {System.out.println();System.out.println("服务端等待连接......");Socket socket = serverSocket.accept();System.out.println("服务端已经收到连接请求");System.out.println("服务端等待数据.....");socket.getInputStream().read(bytes);System.out.println("服务端等已收到数据.....");String msg = new String(bytes);System.out.println("接收到了数据: " + msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

如果单线程服务器在等待数据时阻塞,那么第二个连接请求到来时,服务器是无法响应的。如果是多线程服务器,那么又会有为大量空闲请求产生新线程从而造成线程占用系统资源,线程浪费的情况。

那么我们的问题就转移到,如何让单线程服务器在等待客户端数据到来时,依旧可以接收新的客户端连接请求


模拟NIO

如果要解决上文中提到的单线程服务器接收数据时阻塞,而无法接收新请求的问题,那么其实可以让服务器在等待数据时不进入阻塞状态,问题不就迎刃而解了吗?

方案一: (等待连接时和等待数据时不阻塞)

package com.artisan.bio;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;public class MyServer {public static void main(String[] args) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);try {//Java为非阻塞设置的类ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(1234));//设置为非阻塞serverSocketChannel.configureBlocking(false);while (true) {SocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {//表示没人连接System.out.println(Thread.currentThread().getName() + " 正在等待客户端请求连接...");Thread.sleep(5000);} else {System.out.println(Thread.currentThread().getName() + "当前接收到客户端请求连接...");}if (socketChannel != null) {//设置为非阻塞socketChannel.configureBlocking(false);byteBuffer.flip();//切换模式  写-->读int effective = socketChannel.read(byteBuffer);if (effective != 0) {String content = Charset.forName("utf-8").decode(byteBuffer).toString();System.out.println(content);} else {System.out.println(Thread.currentThread().getName() + "当前未收到客户端消息");}}}} catch (IOException e) {e.printStackTrace();}}
}

客户端测试代码

package com.artisan.bio;import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ClientWithInput {private static Socket socket;private static Scanner sc;public static void main(String[] args) throws IOException {try {while (true) {socket = new Socket("127.0.0.1", 1234);String message = null;sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());}} catch (IOException e) {e.printStackTrace();} finally {socket.close();sc.close();}}
}

在这里插入图片描述

在这里插入图片描述

在这种解决方案下,虽然在接收客户端消息时不会阻塞,但是又开始重新接收服务器请求,用户根本来不及输入消息,服务器就转向接收别的客户端请求了,换言之,服务器弄丢了当前客户端的请求


方案二(缓存Socket,轮询数据是否准备好)

package com.artisan.bio;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;public class MyServer {public static void main(String[] args) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);List<SocketChannel> socketList = new ArrayList<SocketChannel>();try {//Java为非阻塞设置的类ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(1234));//设置为非阻塞serverSocketChannel.configureBlocking(false);while (true) {SocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {//表示没人连接System.out.println(Thread.currentThread().getName() + "正在等待客户端请求连接...");Thread.sleep(5000);} else {System.out.println(socketChannel.getRemoteAddress() + "当前接收到客户端请求连接...");socketList.add(socketChannel);}for (SocketChannel socket : socketList) {socket.configureBlocking(false);int effective = socket.read(byteBuffer);if (effective != 0) {byteBuffer.flip();//切换模式  写-->读String content = Charset.forName("UTF-8").decode(byteBuffer).toString();System.out.println(socket.getRemoteAddress() + "接收到消息:" + content);System.out.println();byteBuffer.clear();} else {// System.out.println(socket.getRemoteAddress() + "当前未收到客户端消息");}}}} catch (IOException e) {e.printStackTrace();}}
}

客户端我们使用如下代码去模拟

package com.artisan.bio;import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class ClientWithInput {private static Socket socket;private static Scanner sc;public static void main(String[] args) throws IOException {try {while (true) {socket = new Socket("127.0.0.1", 1234);String message = null;sc = new Scanner(System.in);message = sc.next();socket.getOutputStream().write(message.getBytes());}} catch (IOException e) {e.printStackTrace();} finally {socket.close();sc.close();}}
}

在这里插入图片描述

在这里插入图片描述

我们可以发现
1. 消息并没有丢失
2. server端并没有开启多线程来处理消息,均是在main线程

在解决方案一中,我们采用了非阻塞方式,但是发现一旦非阻塞,等待客户端发送消息时就不会再阻塞了,而是直接重新去获取新客户端的连接请求,这就会造成客户端连接丢失。

而在解决方案二中,我们将连接存储在一个list集合中,每次等待客户端消息时都去轮询,看看消息是否准备好,如果准备好则直接打印消息。

可以看到,从头到尾我们一直没有开启第二个线程,而是一直采用单线程来处理多个客户端的连接,这样的一个模式可以很完美地解决BIO在单线程模式下无法处理多客户端请求的问题,并且解决了非阻塞状态下连接丢失的问题。


方案二存在的问题

从刚才的运行结果中其实可以看出,消息没有丢失,程序也没有阻塞。

但是,在接收消息的方式上可能有些许不妥,我们采用了一个轮询的方式来接收消息,每次都轮询所有的连接,看消息是否准备好,测试用例中只是三个连接,所以看不出什么问题来,但是我们假设有1000万连接,甚至更多,采用这种轮询的方式效率是极低的

另外,1000万连接中,我们可能只会有100万会有消息,剩下的900万并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的


NIO是如何解决这些问题的

在真实NIO中,并不会在Java层上来进行一个轮询,而是将轮询的这个步骤交给我们的操作系统来进行,他将轮询的那部分代码改为操作系统级别的系统调用(select函数,在linux环境中为epoll),在操作系统级别上调用select函数,主动地去感知有数据的socket


使用select/poll/epoll和直接在应用层做轮询的区别

NIO使用了操作系统底层的轮询系统调用 select/epoll(windows:select,linux:epoll),那么为什么不直接实现而要去调用系统来做轮询呢?


select底层逻辑

假设有A、B、C、D、E五个连接同时连接服务器,那么根据我们上文中的设计,程序将会遍历这五个连接,轮询每个连接,获取各自数据准备情况,那么和我们自己写的程序有什么区别呢?

首先:我们写的Java程序其本质在轮询每个Socket的时候也需要去调用系统函数,那么轮询一次调用一次,会造成不必要的上下文切换开销

而:Select会将五个请求从用户态空间全量复制一份到内核态空间,在内核态空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层写轮询要高的。

如果:select没有查询到到有数据的请求,那么将会一直阻塞(是的,select是一个阻塞函数)。如果有一个或者多个请求已经准备好数据了,那么select将会先将有数据的文件描述符置位,然后select返回。返回后通过遍历查看哪个请求有数据。


select的缺点

  • 1)底层存储依赖bitmap,处理的请求是有上限的,为1024;
  • 2)文件描述符是会置位的,所以如果当被置位的文件描述符需要重新使用时,是需要重新赋空值的;
  • 3)fd(文件描述符)从用户态拷贝到内核态仍然有一笔开销;
  • 4)select返回后还要再次遍历,来获知是哪一个请求有数据。

poll的底层逻辑

poll的工作原理和select很像,先来看一段poll内部使用的一个结构体

struct pollfd{int fd;short events;short revents;
}
  • poll同样会将所有的请求拷贝到内核态,和select一样,poll同样是一个阻塞函数,当一个或多个请求有数据的时候,也同样会进行置位,但是它置位的是结构体pollfd中的events或者revents置位,而不是对fd本身进行置位,所以在下一次使用的时候不需要再进行重新赋空值的操作。

  • poll内部存储不依赖bitmap,而是使用pollfd数组的这样一个数据结构,数组的大小肯定是大于1024的。

解决了select 1、2两点的缺点。


epoll的底层逻辑

epoll是最新的一种多路IO复用的函数。这里只说说它的特点。

  • epoll和上述两个函数最大的不同是,它的fd是共享在用户态和内核态之间的,所以可以不必进行从用户态到内核态的一个拷贝,这样可以节约系统资源。

  • 另外,在select和poll中,如果某个请求的数据已经准备好,它们会将所有的请求都返回,供程序去遍历查看哪个请求存在数据,但是epoll只会返回存在数据的请求,这是因为epoll在发现某个请求存在数据时,首先会进行一个重排操作,将所有有数据的fd放到最前面的位置,然后返回(返回值为存在数据请求的个数N),那么我们的上层程序就可以不必将所有请求都轮询,而是直接遍历epoll返回的前N个请求,这些请求都是有数据的请求。

高性能网络编程 - select、 poll 、epoll 、libevent

在这里插入图片描述

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

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

相关文章

STM32F103C8 PC13端口无输出原因

如果开启了RTC功能&#xff0c;就要注意PC13端口的设置。要把RTC OUT 由“Disable”改成“No RTC Output”&#xff0c;才行。

docker-compose 部署 MySQL 8

前言 Windows 系统通过 docker-compose 部署 MySQL8.0。 MySQL 配置文件(my.cnf) # 服务端参数配置 [mysqld] usermysql # MySQL启动用户 default-storage-engineINNODB # 创建新表时将使用的默认存储引擎 character-set-serverutf8mb4 # 设置mysql服…

Spring-Spring之AOP底层原理解析---实践(动态代理)

动态代理 代理模式的解释&#xff1a;为其他对象提供一种代理以控制对这个对象的访问&#xff0c;增强一个类中的某个方法&#xff0c;对程序进行扩展。 cglib动态代理 方式一&#xff1a; public class UserService {public void test() {System.out.println("test..…

智能穿戴AR眼镜主板方案定制_MTK平台AR智能眼镜PCB板开发

AR智能眼镜&#xff0c;是采用了多种技术实现增强现实效果&#xff0c;是将虚拟信息和现实场景相结合的智能设备。 AR智能眼镜硬件上&#xff0c;包括多个传感器、显示装置和处理器等。其中&#xff0c;传感器用于捕捉用户的动作和环境信息&#xff0c;如摄像头、陀螺仪、加速…

【Python】 Python 使用 Pillow 处理图像:几何变换

Python 使用 Pillow 处理图像&#xff1a;几何变换 pillow库操作切片、旋转、滤镜、输出文字、调色板等功能一应俱全。 1. 几何变换 Image 包含调整图像大小 resize() 和旋转 rotate() 的方法。前者采用元组给出新的大小&#xff0c;后者采用逆时针方向的角度。 调整大小并…

将VS工程转为Qt的pro工程及VS安装Qt插件后没有create basic .pro file菜单问题解决

目录 1. 前言 2. VS工程转为pro工程 3. 没有create basic .pro file菜单 1. 前言 很多小伙伴包括本人&#xff0c;如果是在Windows下开发Qt程序&#xff0c;偏好用Visual Studio外加装个Qt插件进行Qt开发&#xff0c;毕竟Visual Studio确实是功能强大的IDE&#xff0c;但有时…

SpringCloud微服务:Ribbon负载均衡

目录 负载均衡策略&#xff1a; 负载均衡的两种方式&#xff1a; 饥饿加载 1. Ribbon负载均衡规则 规则接口是IRule 默认实现是ZoneAvoidanceRule&#xff0c;根据zone选择服务列表&#xff0c;然后轮询 2&#xff0e;负载均衡自定义方式 代码方式:配置灵活&#xff0c;但修…

Linux--makefile

一、makefile的作用 makefile是一个文件&#xff0c;是围绕依赖关系和依赖方法的自动化编译工具 一个工程中的源文件有很多&#xff0c;按照不同的类型、功能、模块放在不同的目录中。而makefile定义了一系列的规则来指定&#xff0c;那些文件需要先编译&#xff0c;那些文件…

【算法与数据结构】93、LeetCode复原 IP 地址

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;参照【算法与数据结构】131、LeetCode分割回文串的思路&#xff0c;需要将IP字符串进行分割&#xff0…

JVM虚拟机:垃圾回收之三色标记

本文重点 在前面的课程中我们已经学习了垃圾回收器CMS和G1,其中CMS和G1中的mixedGC都存在四个过程,这四个过程中有一个过程叫做并发标记,也就是说程序一边运行,一边标记垃圾。这个过程最困难的是:如果在标记垃圾的时候,如果对象的引用关系发生了改变,此时应该如何处理?…

【分布式】CAP理论详解

一、CAP理论概述 在分布式系统中&#xff0c;CAP是指一组原则&#xff0c;它们描述了在网络分区&#xff08;Partition&#xff09;时&#xff0c;分布式系统能够提供的保证。CAP代表Consistency&#xff08;一致性&#xff09;、Availability&#xff08;可用性&#xff09;和…

【Python】上市公司数据进行经典OLS回归实操

一、题目二、数据合并、清洗、描述性统计1、数据获取2、数据合并3、选择董监高薪酬作为解释变量的理论逻辑分析 三、多元回归模型的参数估计、结果展示与分析1、描述性统计分析2、剔除金融类上市公司3、对所有变量进行1%缩尾处理4、0-1标准化&#xff0c;所有解释变量5、绘制热…