当前位置: 首页 > news >正文

nio和bio的原理_深入剖析BIO和NIO底层原理

在学习IO之前需要理解四个基础概念: 同步,异步,阻塞,非阻塞

(1)同步/异步:同步是指一个时间点只能有一个程序在占用CPU,而异步是指可以有多个程序并行(可以很好的利用操作系统的多核)

(2)阻塞/非阻塞:阻塞是指操作系统发出一个调用/操作之后,必须等到此调用返回结果后才返回。而非阻塞是指在操作系统发出一个调用/操作后,不用等到执行完毕得到结果才返回,而是立即返回,然后可以执行其他的调用/操作(例如:在遇到比较耗时的IO操作时,无需等待此IO操作完成再返回)。

BIO: 同步阻塞模型

BIO会话可以理解为由三个socket组成: 一个是客户端的socket, 一个是服务端用于监听的socket,一个是用于服务端接收数据的socket。

在BIO中用于监听的API是ServerSocket类。

BIO中两个阻塞: (1)等待socket连接时,accept方法会阻塞

(2)等待客户端发送数据时,read方法会阻塞

由于这两个阻塞方法的存在,BIO时无法在单线程下处理并发的。如果硬要在程序中使用BIO来处理多个连接请求,则需要开多线程(将read操作以及后续的数据处理操作放到Thread中去执行,从而避免主线程的阻塞),但是多线程的弊端是非常大的:因为可能有一些线程其实不是活跃的,然后还是专门开了一个线程来处理它,会发生CPU资源浪费的问题。

手写一个BIO:

客户端:

public class Client {

public static void main(String[] args) {

try {

Socket socket = new Socket("127.0.0.1", 8080);

// socket.connect(new InetSocketAddress(8080));

//下面的代码也会阻塞 Scanner scanner = new Scanner(System.in);

String txt = scanner.next();

socket.getOutputStream().write(txt.getBytes());

//socket.getOutputStream().write("111".getBytes()); socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

服务端代码:

public class QQServer {

static byte[] bytes = new byte[1024];

public static void main(String[] args) {

try {

//用于监听 ServerSocket serverSocket = new ServerSocket();

//绑定服务器的ip和端口,ip为本机,所以省略 serverSocket.bind(new InetSocketAddress(8080));

while(true) {

System.out.println("wait conn");

//监听,会阻塞->当前线程回放弃CPU线程,就意味着不会再向下执行了 //这个socket是专门用于与客户端通信的socket Socket socket = serverSocket.accept();

System.out.println("conn success");

System.out.println("wait data");

//专门用于接受客户端发来的byte数组,返回一个int类型的值,用于表示返回多少字节 int read = socket.getInputStream().read(bytes);

System.out.println("data success");

String content = new String(bytes);

System.out.println(content);

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

由于阻塞的原因,无法处理批量线程。所以引出NIO模型:

NIO的逻辑比较复杂,涉及到操作系统的内核调用,先来个简单的,手写一个通过应用程序进行轮训的:

客户端:

public class Client {

public static void main(String[] args) {

try {

Socket socket = new Socket("127.0.0.1", 8080);

// socket.connect(new InetSocketAddress(8080));

//下面的代码也会阻塞 Scanner scanner = new Scanner(System.in);

String txt = scanner.next();

socket.getOutputStream().write(txt.getBytes());

//socket.getOutputStream().write("111".getBytes()); socket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}服务端:

public class QQServerNIO {

static byte[] bytes = new byte[1024];

static List list = new ArrayList();

//申请对外内存 static ByteBuffer byteBuffer = ByteBuffer.allocate(512);

public static void main(String[] args) {

try {

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.bind(new InetSocketAddress(8080));

//设置为serversocketchannel为非阻塞 serverSocketChannel.configureBlocking(false);

while(true){

//非阻塞 SocketChannel socketChannel = serverSocketChannel.accept();

if(socketChannel == null){

Thread.sleep(500);

System.out.println("no conn");

for(SocketChannel client : list) {

int k = client.read(byteBuffer);

if (k > 0) {

byteBuffer.flip();

System.out.println(byteBuffer.toString());

}

}

}else{

System.out.println("conn----");

socketChannel.configureBlocking(false);

list.add(socketChannel);

for(SocketChannel client : list){

int i = client.read(byteBuffer);

if(i > 0){

byteBuffer.flip();

System.out.println(byteBuffer.toString());

}

}

}

}

} catch (IOException | InterruptedException e) {

e.printStackTrace();

}

}

}

注意理解BIO和NIO的实现区别!

NIO是将read和accept都设置为非阻塞了!

但是这样做存在一个问题:如果现在list中有100000个socket,那么每次循环都要用for去轮训这么多元素吗? so stupid!

所以最好不要程序去主动轮训,而是能够主动感知哪个socket有新的数据发送过来了!

所以就进入到了真实使用的NIO,它是不将轮训交给应用程序,而是交给操作系统。linux内核会调用epoll方法。

接下来介绍IO多路复用的三种机制:

(1)select

(2) poll

(3) epoll

select、poll、epoll本质上也都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

在接下来深入介绍之前,先回顾一下Linux操作系统中的几个基本概念:

(1)内核态/用户态:

电脑启动后,第一个程序是操作系统内核(kernel),内核时负责管理计算机的硬件(eg:网卡),应用程序是不能直接与硬件交互的。

当启动之后,os内核进入内存中之后,会注册一个GDT(全局描述符表)—》 它会把内核进程所在的内存空间划分出来,剩余的予以划分。

而如果应用程序想去操作硬件的话,可以调用内核提供的”系统调用”函数(system call),但是又没法直接调,因为处于保护模式(这个方法是在内核的内存空间内),所以有个中断系统。(中断分为软中断和硬中断,而系统调用是软中断),外设例如鼠标的移动,属于硬中断),CPU会根据终端号会有例如网卡的驱动

结论: 若一个程序想要调IO,则必须要经过操作系统内核。

(2)文件描述符:

多路复用实际就是一个进城可以监视多个文件描述符(fd),一旦某个描述符就绪(无论读就绪还是写就绪),能够通知程序进行相应读或写的处理。

select,poll和epoll的本质都是同步I/O(异步IO无需自己负责读写)

Select详解:

看下面程序:

sockfd = socket(AF_INET, SOCK_STREAM, 0);

memset(&addr, 0, sizeof (addr));

addr.sin_family = AF_INET;

addr.sin_port = htons(2000);

addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));

listen (sockfd, 5);

for (i=0;i<5;i++)

{

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);

if(fds[i] > max)

max = fds[i];

}

while(1){

FD_ZERO(&rset);

for (i = 0; i< 5; i++ ) {

FD_SET(fds[i],&rset);

}

puts("round again");

select(max+1, &rset, NULL, NULL, NULL);

for(i=0;i<5;i++) {

if (FD_ISSET(fds[i], &rset)){

memset(buffer,0,MAXBUF);

read(fds[i], buffer, MAXBUF);

puts(buffer);

}

}

}

select函数的参数:

第一个参数n = max + 1,即为最大文件描述符+ 1 (代表总共的bitmap连接的数量),

第二个参数是读文件描述符集合,

第三个参数是写文件描述符集合,

第四个参数是异常文件描述符集合,

第五个参数是超时时间。

上面程序的逻辑:在while(True)循环之前是建立socket连接,while循环中主要做了以下几个事情:

(1)每次重新循环的时候,都将rset清0

(2)每次将socket连接通过for-loop来放到rset中

(3)循环遍历,看看那个链接中有值

注: &rset其实是一个bitmap,即使1024位的0,1序列

这里的FD置位是指rset中对应的那一位,而不是真正的fds中的元素

这种方式有一定的缺点:

fd_set是利用数组实现的

1.fd_size 有限制 1024 bitmap

fd【i】 = accept()

2.fdset不可重用,新的fd进来,重新创建

3.用户态和内核态拷贝产生开销

4.O(n)时间复杂度的轮询

成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0,具有超时时间

(2)poll函数:

poll中定义了一个结构体pollfd

结构体中包括:(1)fd (2)event:即poll的事件 (pollin表示读,pollout表示写)

(3)revents:当有数据过来的时候 ,会将pollfd中的revents置位(而不是像select中将bitmap置位),revents默认是0,有读的事件进来,则置位为pollin,然后poll返回。

看一段C ++代码:

for (i=0;i<5;i++)

{

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);

pollfds[i].events = POLLIN;

}

sleep(1);

while(1){

puts("round again");

poll(pollfds, 5, 50000);

for(i=0;i<5;i++) {

if (pollfds[i].revents & POLLIN){

pollfds[i].revents = 0;

memset(buffer,0,MAXBUF);

read(pollfds[i].fd, buffer, MAXBUF);

puts(buffer);

}

}

}

程序继续往下,if revents == pollin,则读数据,将revents置位为0,所以每次pollfds都是一样的,可以进行重用!!!

从而解决了select中的前两个缺点!!

(3)epoll函数:

不需要轮询,时间复杂度为O(1)

看一段C++代码理解一下:

struct epoll_event events[5];

int epfd = epoll_create(10);

...

...

for (i=0;i<5;i++)

{

static struct epoll_event ev;

memset(&client, 0, sizeof (client));

addrlen = sizeof(client);

ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);

ev.events = EPOLLIN;

epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);

}

while(1){

puts("round again");

nfds = epoll_wait(epfd, events, 5, 10000);

for(i=0;i

memset(buffer,0,MAXBUF);

read(events[i].data.fd, buffer, MAXBUF);

puts(buffer);

}

}

epoll_create 创建一个白板 存放fd_events

epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上

epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。

总而言之,epoll解决了所有select的缺点。

相关文章:

  • 【codeforces 789B】Masha and geometric depression
  • redis 缓存预热_Redis中缓存预热、击穿、雪崩等问题解决方案
  • Ubuntu环境下IPython的搭建和使用
  • 或是独体字吗_什么是独体字?
  • 【7005】二叉树的遍历问题2
  • eslint 无法格式化ts_vscode-eslint的踩坑实践--typescript没法格式化
  • 【2030】排队打水问题
  • vue入门到启动_Vue入门:Vue项目创建及启动
  • 【2012】建立二维矩阵
  • idle显示出错信息 python_python小课堂05 - 基本数据类型字符串篇(重要)
  • POJ3468(线段树 区间修改 lazy-tag)
  • html radio 默认图片替换_怎么修改单选框radio默认样式
  • ubuntu 16.04 主题美化及终端美化
  • android怎么监听多点触摸_android 多点触控
  • c#webservice接口調用_用.net发布一个简单的webservice
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • iOS编译提示和导航提示
  • Java基本数据类型之Number
  • Java-详解HashMap
  • js继承的实现方法
  • Python3爬取英雄联盟英雄皮肤大图
  • react 代码优化(一) ——事件处理
  • Spring技术内幕笔记(2):Spring MVC 与 Web
  • VuePress 静态网站生成
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 基于Dubbo+ZooKeeper的分布式服务的实现
  • 名企6年Java程序员的工作总结,写给在迷茫中的你!
  • 前端
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 算法---两个栈实现一个队列
  • 原生Ajax
  • 在Docker Swarm上部署Apache Storm:第1部分
  • elasticsearch-head插件安装
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • 正则表达式-基础知识Review
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #define 用法
  • #HarmonyOS:Web组件的使用
  • #pragam once 和 #ifndef 预编译头
  • #pragma data_seg 共享数据区(转)
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • (2)STL算法之元素计数
  • (SpringBoot)第七章:SpringBoot日志文件
  • (超详细)语音信号处理之特征提取
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (转)Mysql的优化设置
  • ***linux下安装xampp,XAMPP目录结构(阿里云安装xampp)
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .netcore 如何获取系统中所有session_如何把百度推广中获取的线索(基木鱼,电话,百度商桥等)同步到企业微信或者企业CRM等企业营销系统中...
  • .NET分布式缓存Memcached从入门到实战
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth