网络通信和IO(3):IO模型 / 什么是BIO / 什么是NIO /什么是多路复用 / epoll实现IO多路复用 / socket详解

本文详细介绍了网络通信中的IO模型,包括Socket、BIO(阻塞I/O)、NIO(非阻塞I/O)以及多路复用IO,特别强调了Linux下的epoll实现。通过分析各模型的工作原理和优缺点,阐述了它们在不同场景下的适用性,指出epoll在处理大量连接时的高效性。
摘要由CSDN通过智能技术生成

Socket

在第一文网络与IO基本概念中说了一下对网络和IO的理解,第二文OSI七层详细的讲解了网络通信的过程,在过程当中感受到了会有一系列的系统调用,也就是IO的过程

换句话说,通信就是IO的过程,我们可以通过socket来完成通信。

我们来通过帮助文档详细的看一下Linux内核中的socket

man 2 socket

可以看到,如果得到socket会给我们返回了一个文件描述符
在这里插入图片描述

以及关于socket的一些命令方法
在这里插入图片描述

在得到socket过程当中,还有一些属性和选项,SOCK_NONBLOC:非阻塞,也就是说socket可以是阻塞或者非阻塞
在这里插入图片描述

再继续看,我们看socket的bind()方法

man 2 bind

可以看到文档有一个example例子,我们大概看一下干的什么
在这里插入图片描述
在这里插入图片描述

我们来梳理一下逻辑,我们现在是一个服务端,上来调用socket得到的文件描述符是服务端文件描述符,然后绑定地址端口号之后去监听它,如果有客户端连进来,会得到一个新的客户端描述符,也就是说每个客户端连进来都是新的描述符,也就是独立的单独的文件描述符,相当于是不同的连接了。

我们再来看Java中使用Socket
在这里插入图片描述

得到一个ServerSocket对象并绑定端口号,实例化以后相当于建立了连接,调用了listen,开启了监听状态,调用accept能拿到Socket,有别人连接进来,就可以拿到不为空的socket,也就是得到了一个新的客户端文件描述符。和linux中的socket基本一致的的。然后我们再继续看,拿到一个连接之后抛出了一个线程去处理这个连接,也就是每个连接让单独的线程去处理。
在这里插入图片描述

得到连接之后,可以获取到socket的一个流,得到它的输入流,就可以readerLine(),读取它的数据。得到输出流就可以发送数据。

在这里插入图片描述

然后我们回到Linux中来,一切皆文件,我们再来看socket的read方法

man 2 read

可以看到需要传入文件描述符,就可以读取文件描述符

在这里插入图片描述

read可以读取文件描述符,而socket的文件描述符可以是阻塞或者非阻塞的,那么阻塞与否是什么意思?

如果读取的是一个阻塞的文件描述符,那么我们这个read方法调起以后,就会在这卡住了,对方什么时候把数据发来,read才能读到东西,也就说我们这个线程可以被阻塞住。这就是BIO。

如果读取的是一个非阻塞的文件描述符,在read方法调起的时候,因为没有数据到达,它可以立刻返回一个没有数据,然后就可以去干别事,所以read就不会被阻塞住,等过一会再去read,就有可能读到了,也就是数据到了有可能需要等一会,但无论如何程序不会阻塞住。这就是NIO。

BIO

上面说的socket的bind,accept,read方法都是来自于kernel也就是系统内核,我们把上面的点连起来,如下图我们假如说要在服务器上跑一个java项目。
在这里插入图片描述

我们把项目放到tomcat中,启动tomcat,这时tomcat会先去访问内核,listen开启监听,比如说监听8080,就可以得到一个服务端文件描述符,比如说是6。

然后客户端想访问tomcat中的项目的话,客户端会先和服务器内核通信,在内核中,建立三次握手,握手的时候访问的就是8080,6这个文件描述符就可以监听到,然后会为这个客户端建立一个新文件描述符比如说8。

然后客户端和kernel连接成功以后,tomcat会开启一个线程去从kernel中读文件描述符8,也就是读客户端发过来的数据,完成通信。

如果又一个新的socket连接连进来,会再分配一个新的文件描述符比如说9,这样就可以把socket区分出来了。

在早期linux的kernel的文件描述符或者说socket,是阻塞的,那么read8这个线程读不到数据就会被阻塞了,再监听到fd9,又会开启一个线程去read9,fd9有数据了,T2就开始工作了,T1就得等有数据才会工作否则会一直阻塞着。这就是BIO模型。

它的弊端如下:

  • 线程越多,Context Switch就越多,而Context Switch是一个比较重的操作,会无谓浪费大量的CPU。

  • 每个线程会占用一定的内存作为线程的栈。比如有1000个线程同时运行,每个占用1MB内存,就占用了1个G的内存。

由于线程阻塞会引起以上问题,那么要是操作IO接口时,操作系统能够总是直接告诉有没有数据,而不是Block去等就好了。于是,NIO登场。

NIO

NIO被翻译为No-Block或者是New-Bolck,让socket能变成非阻塞,那么read就只需要开辟一个线程就够了,比如说它读fd8的时候,没有数据,它不会阻塞住,它会再去读fd9,伪代码就是循环一直监听读取新的连接就可以了。

在这里插入图片描述

但NIO虽然可以不阻塞了,我们通过轮询,不断尝试数据是否到达,这比之前BIO好多了,起码程序不会被卡死了。但这样会带来两个新问题:

  • 如果有大量文件描述符都要等,那么就得一个一个的read。这会带来大量的Context Switch,因为read是系统调用,每调用一次就得在用户态和核心态切换一次

  • 休息一会的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU。

要是操作系统能一口气告诉程序,哪些连接有数据到达了就好了。那么read能否只读一次就把所有连接读一遍看看是否有数据呢?

答案是不行的,read一次只能读一个连接,处理不了多个连接,所以内核就要发生变化,开辟一个新的系统调用来弥补资源浪费的情况

这时内核多了一个系统调用,即select,在Java中的NIO中叫Selector选择器。这种一次查询多个连接是否有数据到达的方式就叫多路复用。

多路复用IO

再说多路复用IO之前先来说一下它和NIO的关系,以免很多人会搞混

IO多路复用是要和NIO一起使用的。尽管在操作系统级别,NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回,不会被阻塞。

而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制必须得一起用,你可以用NIO,但不用IO多路复用。也可以使用IO多路复用 + BIO,但这时效果还是当前线程被卡住。

所以IO多路复用和NIO是要配合一起使用才有实际意义。因此,在使用IO多路复用之前,总是先把fd设为SOCK_NONBLOC。

铺垫完以后我们先在Linux系统上来看select

man 2 select

在这里插入图片描述

它接受3个文件描述符的数组,分别监听读取(readfds),写入(writefds)和异常(expectfds)事件,所以select允许监控更多的文件描述符,可以把所有的连接放进去,然后内核会推演哪些连接有数据然后返回给程序,程序再去处理。

详细来说就是为select构造一个fd数组,然后用select监听了read_fds中的多个socket的读取时间。调用select后,程序会Block住,直到一个事件发生了,或者等到最大1秒钟(tv定义了这个时间长度)就返回。之后,需要遍历所有注册的fd,挨个检查哪个fd有事件到达(FD_ISSET返回true)。如果true,就说明数据已经到达了,可以读取fd了。读取后就可以进行数据的处理。

在Java中的NIO也是相似的,JAVA NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。

传统IO是基于流的形式,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件,比如:连接打开,数据到达。因此,单个线程可以监听多个数据通道。

Channel

首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream流是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream。而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作,Channel的主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,通过看名字就可以猜出个所以然来:分别可以对应文件IO、UDP、TCP的Server和Client

Buffer

Buffer是一个缓冲区,是一个用于存储特定基本类型数据的容器,例如它的实现有:ByteBuffer、CharBuffer、 FloatBuffer、IntBuffer分别对应基本数据类型: byte、char、 float、int。可以把它理解成和Channel配合使用的,在进行读操作时,需要使用Buffer分配空间,然后将数据从Channel中读入Buffer中,对于Channel的写操作,也需要先将数据写入Buffer,然后将Buffer写入Channel中。

Selector

Selector 是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,也就是说Selector可以监听多个通道的事件,比如连接打开,数据到达。

通过代码可以看到java怎么使用他们,如下:

//创建选择器
S
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值