为什么网络I/O会被阻塞?

时间:2023-01-10 20:08:18
摘要:I/O 其实就是 input 和 output 的缩写,即输入/输出。

本文分享自华为云社区《为啥网络IO会被阻塞呢》,作者: 龙哥手记。

我们应该都知道 socket(套接字),你可以认为我们的通信都要基于这个玩意,而常说的网络通信又分为 TCP 与 UDP 两种,下面我会以 TCP 通信为例来阐述下 socket 的通信流程。

不过在此之前,我先来说说什么叫 I/O 。

I/O到底是什么?

I/O 其实就是 input 和 output 的缩写,即输入/输出。

那输入输出啥呢?

比如我们用键盘来敲代码其实就是输入,那显示器显示图案就是输出,这其实就是 I/O。

而我们时常关心的磁盘 I/O 指的是硬盘和内存之间的输入输出。

读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中。

网络 I/O 指的是网卡与内存之间的输入输出。

当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。

那为什么都要跟内存交互呢?

我们的指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度。

因此都是和内存交互,当然假设没有内存,让 CPU 直接和外部设备交互,那也算 I/O。

总结下:I/O 就是指内存与外部设备之间的交互(数据拷贝)。

好了,明确什么是 I/O 之后,让我们来揭一揭 socket 通信内幕~

创建 socket

首先服务端需要先创建一个 socket。在 Linux 中一切都是文件,那么创建的 socket 也是文件,每个文件都有一个整型的文件描述符(fd)来指代这个文件。

int socket(int domain, int type, int protocol);

  • domain:这个参数用于选择通信的协议族,比如选择 IPv4 通信,还是 IPv6 通信等等
  • type:选择套接字类型,可选字节流套接字、数据报套接字等等。
  • protocol:指定使用的协议。

这个 protocol 通常可以设为 0 ,因为由前面两个参数可以推断出所要使用的协议。

比如socket(AF_INET, SOCK_STREAM, 0);,表明使用 IPv4 ,且使用字节流套接字,可以判断使用的协议为 TCP 协议。

这个方法的返回值为 int ,其实就是创建的 socket 的 fd。

bind

现在我们已经创建了一个 socket,但现在还没有地址指向这个 socket。

众所周知,服务器应用需要指明 IP 和端口,这样客户端才好找上门来要服务,所以此时我们需要指定一个地址和端口来与这个 socket 绑定一下。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数里的 sockfd 就是我们创建的 socket 的文件描述符,执行了 bind 参数之后我们的 socket 距离可以被访问又更近了一步。

listen

执行了 socket、bind 之后,此时的 socket 还处于 closed 的状态,也就是不对外监听的,然后我们需要调用 listen 方法,让 socket 进入被动监听状态,这样的 socket 才能够监听到客户端的连接请求。

int listen(int sockfd, int backlog);

传入创建的 socket 的 fd,并且指明一下 backlog 的大小。

这个 backlog 我查阅资料的时候,看到了三种解释:

  1. socket 有一个队列,同时存放已完成的连接和半连接,backlog为这个队列的大小。
  2. socket 有两个队列,分别为已完成的连接队列和半连接队列,backlog为这个两个队列的大小之和。
  3. socket 有两个队列,分别为已完成的连接队列和半连接队列,backlog仅为已完成的连接队列大小。
解释下什么叫半连接

我们都知道 TCP 建立连接需要三次握手,当接收方收到请求方的建连请求后会返回 ack,此时这个连接在接收方就处于半连接状态,当接收方再收到请求方的 ack 时,这个连接就处于已完成状态:

为什么网络I/O会被阻塞?