Netty权威指南——基础篇1(同步阻塞IO-BIO)

1 Linux网络I/O模型简介

1.1 简述

        Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。根据UNIX网络编程对I/O模型的分类,UNIX提供了五种I/O模型。

1.2  五种I/O模型介绍

        阻塞I/O模型:

        最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才返回,再次期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此成为阻塞I/O模型模型。如下图所示:

        非阻塞 I/O模型:

        recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞模型进行轮询检查这个状态,看内核是否有数据到来,如下图:

        I/O复用模型:

        Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态状态。select/poll是顺序扫描fd是否就绪,而且支持df数量有限,因此它的使用受到了一些限制。Linux还提供了一个epoll系统调用,epoll使用基于时间驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数,如下图:

        信号驱动I/O模型

        首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通过主循环函数处理数据,如下图:

         异步I/O

        告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已完成,如下图:

1.3 I/O多路复用技术

        在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O 多路复用技术通过把多个I/O 的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型相比,I/O 多路复用技术的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源,I/O 的主要应用场景如下:

        1、服务器需要同时处理多个处于监听状态或者多个连接状态的套接字

        2、服务器需要同时处理多种网络协议的套接字。

        目前支持I/O 多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到很大限制,最终Linux选择了epoll。epoll与select的原理比较类型,为了克服select的缺点,epoll做了很多重大改进。总结如下:

        1、支持一个进程打开的socket描述符不受限制(仅受限于操作系统的最大文件句柄数)。

        select最大缺陷就是单个进程能够打开的fd是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接发大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降,也可以通过选择多进程的方案来解决这个问题,不过虽然在Linux上创建进程的代价比较大,但仍旧是不可忽视的。另外,进程间的数据交换非常麻烦,对于Java来说,由于没有共享内存,需要通过Socket通信或其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。而epoll没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024.例如,在1GB内存的机器上大约有10万个句柄左右,这个值跟系统的内存关系比较大。

        2、I/O 效率不会随着FD数量的增加而线性下降

        传统select/poll的另一个致命弱点,就是当你拥有一个很大的socket集合时,由于网络延时造成链路空闲,任一时刻只有少部分的socket是活跃的,但是select/poll每次调用都会线性扫描全部集合,导致效率呈线性下降。epoll不存在这个问题,它只会对活跃的socket进行操作——这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。那么,只有活跃的socket才会去主动调用callback函数,其他idle状态的socket则不会。在这点上,epoll实现了一个伪AIO。针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于一个活跃态——例如一个高速LAN环境,epoll并不比select/poll效果高太多;相反,如果过多使用epoll_ctl,效率相比还有稍微的降低。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/epoll之上了。

        3、使用mmap加速内核与用户空间的消息传递

        无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存来实现。

        4、epoll的API更简单

        包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符等。

2 传统的BIO编程

        网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端位置信息(绑定的IP地址和监听端口),客户端通过连接操作箱服务端监听的地址发起连接请求,通过的三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

        在基于传统同步阻塞模型并发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

        下面以时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉BIO编程。

2.1 BIO通信模型图

        首先,通过下图所示的通信模型来熟悉BIO的服务端通信模型

        采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。就是代行的一请求一应答通信模型。

        该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

2.2 同步阻塞式I/O创建的TimeServer源码分析

package BIO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;public class TimeServer {public static void main(String[] args) throws IOException {int port = 8080;if(args != null && args.length > 0){try {port = Integer.valueOf(args[0]);}catch (NumberFormatException e){e.printStackTrace();}}ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(port);System.out.println("时间服务器开始启动,端口号是:"+port);Socket socket = null;while(true){socket = serverSocket.accept();new Thread(new TimeServerHandler(socket)).start();}} catch (IOException e) {e.printStackTrace();}finally {if(serverSocket != null){System.out.println("时间服务器关闭");serverSocket.close();serverSocket = null;}}}
}
TimeServerHandler.java
package com.jay.BIO;import org.ietf.jgss.Oid;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;public class TimeServerHandler implements Runnable{private  Socket socket;public TimeServerHandler(Socket socket) {this.socket = socket;}@Overridepublic void run(){BufferedReader in = null;PrintWriter out = null;try {in = new BufferedReader(new InputStreamReader((this.socket.getInputStream())));out = new PrintWriter(this.socket.getOutputStream(),true);String currentTime = null;String body = null;while(true){body = in.readLine();if(body == null){break;}System.out.println("TimeServer收到信息:"+body);currentTime = "query".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "bad";out.println(currentTime);}}catch (Exception e){if(in != null){try {in.close();}catch (IOException e1){e1.printStackTrace();}}if(out != null){out.close();out = null;}if(this.socket != null){try {this.socket.close();}catch (IOException e2){e2.printStackTrace();}this.socket = null;}}}
}

       TimeServer.java 分析

        TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080.然后通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。然后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。启动TimeServer,发现主线程阻塞在accept上。当有新的客户端接入的时候,执行这行代码

new Thread(new TimeServerHandler(socket)).start();

        以Socket为参数构造TimeServerHandler对象,  TimeServerHandler是一个Runnable,使用它它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。

         TimeServerHandler.java 分析

        通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到非空值,则对内容进行判断,如果请求消息为查询时间的指令“query”,则获取当前最新的系统时间,如果PrintWrite的println函数发送给客户端,最后退出循环。最后面为释放输入流、输出流和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。

2.3 同步阻塞式I/O创建的TimeClient源码分析

        客户端通过Socket创建,发送查询时间服务器的“query”指令,然后读取服务端的相应并把结果打印出来,随后关闭连接,释放资源,程序退出执行。

package BIO;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;public class TimeClient {public static void main(String[] args) {int port = 8080;if(args != null && args.length > 0){try {port = Integer.valueOf(args[0]);}catch (NumberFormatException e){e.printStackTrace();}}Socket socket = null;BufferedReader in = null;PrintWriter out = null;try {socket = new Socket("127.0.0.1",port);in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new PrintWriter(socket.getOutputStream(),true);out.println("query");System.out.println("成功发送到服务器");String resp = in.readLine();System.out.println("现在时间是:" + resp);}catch (Exception e){}finally {if(out != null){out.close();out = null;}if(in != null){try {in.close();}catch (IOException e1){e1.printStackTrace();}in = null;}if(socket != null){try {socket.close();}catch (IOException e2){e2.printStackTrace();}socket = null;}}}
}

        客户端通过PrintWrite想服务端发送“query”指令,然后通过BufferedReader的readLine读取响应并打印。分别执行服务端和客户端,执行结果如下。

        服务器运行结果如下:

       客户端执行结果如下:

  

        为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”。下面对伪异步代码进行分析。

3 伪异步I/O编程

        为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化——后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

3.1 伪异步I/O模型图

        采用线程池和任务队列可以实现一种伪异步的I/O 通信框架,它的模型如下图:

        当有新的客户端接入时,将客户端的Socket封装成一个Task(该任务实现Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

3.2 伪异步I/O创建TimeServer源码分析

TimeServerAsyn.java代码如下:
public class TimeServerAsync {public static void main(String[] args) throws IOException {int port = 8080;if(args != null && args.length > 0){try {port = Integer.valueOf(args[0]);}catch (NumberFormatException e){e.printStackTrace();}}ServerSocket server = null;try {server = new ServerSocket(port);System.out.println("TimeServer 启动,端口号是:"+port);Socket socket = null;TimeServerHandlerExecutePool singleExecute = new TimeServerHandlerExecutePool(50, 10000);while(true){socket = server.accept();singleExecute.execute(new TimeServerHandler(socket));}}finally {if(server != null){System.out.println("关闭TimeServer ");server.close();server = null;}}}
}

        伪异步I/O的主函数发生了变化,首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。

TimeServerHandlerExecutePool.java 源码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class TimeServerHandlerExecutePool {private ExecutorService executorService;public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize) {executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxPoolSize,120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));}public void execute(Runnable task){executorService.execute(task);}
}

        客户端代码没有改变,因此直接运行服务端和客户端,执行结果如下:

        伪异步I/O虽然避免了每个请求都创建一个独立线程造成的线程资源耗尽问题。但由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

3.3 伪异步I/O弊端分析

        要对伪异步I/O的弊端进行深入分析,首先看两个java同步I/O的API说明,随后结合代码进行详细分析,先看InputStream.java的read()方法:

/**This method blocks until input data is* available, end of file is detected, or an exception is thrown.*/public int read(byte b[]) throws IOException {return read(b, 0, b.length);}

         请看注释,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三件事情:

        1、有数据可读。2、可用数据已经读取完毕。3、发生异常。

        这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60秒才能将数据发送完成,读取一方的I/O线程也将会被同步阻塞60秒,在此期间,其他接入消息只能在消息队列中排队。

        同样,当调用OutPutStream的write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP Window size不断减少,直到为0,双方处理Keep-Alive状态,消息发送方将不能再想TCP缓冲区写入消息,这是如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP Window size大于0或者发生I/O异常。

        伪异步I/O实际上仅仅是对之前I/O线程模型的一个简单优化,无法从根本上解决同步I/O导致的通信线程阻塞问题。

        

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

IT廉连看——C语言——概述

IT廉连看——C语言概述 一、什么是c语言 C语言是一门通用计算机编程语言&#xff0c;广泛应用于底层开发。C语言的设计目标是提供一种能以简易 的方式编译、处理低级存储器、产 生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。 尽管C语言提供了许多低级处理的功…

【C语言】linux内核__netdev_start_xmit函数

一、中文注释 // 这是一个内联函数&#xff0c;用于启动网络设备的数据包发送流程。 // 它通过网络设备操作集&#xff08;net_device_ops&#xff09;指定的特定函数 // 来启动给定数据包的发送。// ops: 指向包含网络设备操作函数的结构体的指针 // skb: 指向要发送的套接字…

Matlab自学笔记二十七:详解格式化文本sprintf各参数设置方法

1.一个程序引例 上篇文章已经介绍了格式化文本的初步应用&#xff0c;程序示例如下&#xff1a; sprintf(|%f\n|%.2f\n|%8.2f,pi*ones(1,3)) 2.格式化操作符各字段的含义解析 格式化操作符可以有六个字段&#xff0c;只有主字符%和转换格式是必需的&#xff0c;其他都是可选…

跨境支付介绍

1、跨境电商定义和分类&#xff1b; 2、国际贸易清结算&#xff1b; 3、跨境支付&#xff1b; 1、跨境电商定义和分类 跨境电商业务简单说就是指不同国家地域的主体通过电子商务进行交易的一种业务模式。同传统的电商不同&#xff0c;交易双方属于不同的国家。因此&#xff0…

【漏洞复现】若依系统默认弱口令漏洞

Nx01 产品简介 若依系统&#xff08;RuoYi&#xff09;是一套基于SpringBoot的权限管理系统&#xff0c;核心技术采用Spring、MyBatis、Shiro&#xff0c;众多政府、企业采用它作为某些系统的权限管理后台&#xff0c;使用率较高。 Nx02 漏洞描述 若依系统存在默认弱口令漏洞&…

Hikvision SPON IP网络对讲广播系统命令执行漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 1.漏洞描述 Hikvision Intercom Broadcasting System是中国海康威视&a…

分布式架构(分布式ID+分布式事务)

分布式架构 分布式事务产生的场景&#xff1a; 跨JVM进程产生的分布式事务 单体系统访问多个数据库实例 多服务访问同一个数据库实例 CAP理论 C&#xff1a;一致性&#xff0c;指写操作后的读操作可以读取到最新的数据状态&#xff0c;当数据分布在多个节点上&#xff0…

【力扣白嫖日记】178.分数排名

前言 练习sql语句&#xff0c;所有题目来自于力扣&#xff08;https://leetcode.cn/problemset/database/&#xff09;的免费数据库练习题。 今日题目&#xff1a; 178.分数排名 表&#xff1a;Scores 列名类型idintscoredecimal 在 SQL 中&#xff0c;id 是该表的主键。 …

普中51单片机学习(8*8LED点阵)

8*8LED点阵 实验代码 #include "reg52.h" #include "intrins.h"typedef unsigned int u16; typedef unsigned char u8; u8 lednum0x80;sbit SHCPP3^6; sbit SERP3^4; sbit STCPP3^5;void HC595SENDBYTE(u8 dat) {u8 a;SHCP1;STCP1;for(a0;a<8;a){SERd…

stable-diffusion-webui+sadTalker开启GFPGAN as Face enhancer

接上一篇&#xff1a;在autodl搭建stable-diffusion-webuisadTalker-CSDN博客 要开启sadTalker gfpgan as face enhancer&#xff0c; 需要将 1. stable-diffusion-webui/extensions/SadTalker/gfpgan/weights 目录下的文件拷贝到 :~/autodl-tmp/models/GFPGAN/目录下 2.将G…

有趣的CSS - 弹跳的圆

大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是用css写一个好玩的不停弹跳变形的圆。 《有趣的css》系列最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码 完整代码如下html 页面css 样式页…

C++ 学习(1)---- 左值 右值和右值引用

这里写目录标题 左值右值左值引用和右值引用右值引用和移动构造函数std::move 移动语义返回值优化移动操作要保证安全 万能引用std::forward 完美转发传入左值传入右值 左值 左值是指可以使用 & 符号获取到内存地址的表达式&#xff0c;一般出现在赋值语句的左边&#xff…

我的NPI项目之Android 安全系列 -- Android Strongbox 使能(一)

这里借用Android14高通相关的技术文档作为基础文档&#xff0c;该文档描述的是基于NFC的secure element. NFC型号为SN220. 有些概念的说明&#xff1a; 1. RoT 在我们目前的这个上下文中&#xff0c;首先RoT下几个内容&#xff0c;Bootinfo/ Additonal params(images hash) /…

洛谷C++简单题小练习day21—梦境数数小程序

day21--梦境数数--2.25 习题概述 题目背景 Bessie 处于半梦半醒的状态。过了一会儿&#xff0c;她意识到她在数数&#xff0c;不能入睡。 题目描述 Bessie 的大脑反应灵敏&#xff0c;仿佛真实地看到了她数过的一个又一个数。她开始注意每一个数码&#xff08;0…9&#x…

✅技术社区项目—Session/Cookie身份验证识别

session实现原理 SpringBoot提供了一套非常简单的session机制&#xff0c;那么它又是怎么工作的呢? 特别是它是怎么识别用户身份的呢? session又是存在什么地方的呢? 核心工作原理 借助cookie中的 JESSIONID 来作为用户身份标识&#xff0c;这个数据相同的&#xff0c;认…

【DAY04 软考中级备考笔记】数据结构基本结构和算法

数据结构基本结构和算法 2月25日 – 天气&#xff1a;晴 周六玩了一天&#xff0c;周天学习。 1. 什么是数据结构 数据结构研究的内容是一下两点&#xff1a; 如何使用程序代码把现实世界的问题信息化如何用计算机高效地处理这些信息从创造价值 2. 什么是数据 数据是信息的…

第十四章 Linux面试题

第十四章 Linux面试题 日志t.log(访问量)&#xff0c; 将各个ip地址截取&#xff0c;并统计出现次数&#xff0c;并按从大到小排序(腾 讯) http://192. 168200.10/index1.html http://192. 168.200. 10/index2.html http:/192. 168 200.20/index1 html http://192. 168 200.30/…

测试C#使用ViewFaceCore实现图片中的人脸遮挡

基于ViewFaceCore和DlibDotNet都能实现人脸识别&#xff0c;准备做个遮挡图片中人脸的程序&#xff0c;由于暂时不清楚DlibDotNet返回的人脸尺寸与像素的转换关系&#xff0c;最终决定使用ViewFaceCore实现图片中的人脸遮挡。   新建Winform项目&#xff0c;在Nuget包管理器中…

基于深度学习的子图计数方法

背景介绍 子图计数&#xff08;Subgraph Counting&#xff09;是图分析中重要的研究课题。给定一个查询图 和数据图 , 子图计数需要计算 在 中子图匹配的&#xff08;近似&#xff09;数目 。一般我们取子图匹配为子图同构语义&#xff0c;即从查询图顶点集 到数据图顶点集 的…

Excel的中高级用法

单元格格式&#xff0c;根据数值的正负分配不同的颜色和↑ ↓ 根据数值正负分配颜色 2-7 [蓝色]#,##0;[红色]-#,##0 分配颜色的基础上&#xff0c;根据正负加↑和↓ 2↑-7↓ 其实就是在上面颜色的代码基础上加个 向上的符号↑&#xff0c;或向下的符号↓ [蓝色]#,##0↑;[红色…