一、socket 和 fd(file descriptor)是什么?
Unix/Linux 基本哲学之一就是"一切皆文件",即一切都可以用 "open -> read/write -> close" 来操作,socket 也可以理解成是一种特殊的文件。
fd(file descriptor):文件描述符,非负整数,是内核为了高效的管理已经被打开的文件所创建的索引,内核(kernel)利用文件描述符来访问文件。
需要明确的是,每个 tcp 连接的两端都会关联一个套接字和该套接字指向的文件描述符。
二、tcp 连接过程
要通过 TCP 连接发送出去的数据都先拷贝到 send buffer,可能是从用户空间进程的 app buffer 拷入的,也可能是从内核的 kernel buffer 拷入的,拷入的过程是通过 send() 函数完成的,由于也可以使用 write() 函数写入数据,所以也把这个过程称为写数据,相应的send buffer 也就有了别称 write buffer。
最终数据是通过网卡流出去的,所以 send buffer 中的数据需要拷贝到网卡中。由于一端是内存,一端是网卡设备,可以直接使用 DMA 的方式进行拷贝,无需 CPU 的参与。也就是说,send buffer 中的数据通过 DMA 的方式拷贝到网卡中并通过网络传输给 TCP 连接的另一端。
当通过 TCP 连接接收数据时,数据肯定是先通过网卡流入的,然后同样通过 DMA 的方式拷贝到 recv buffer 中,再通过 recv() 函数将数据从 recv buffer 拷入到用户空间进程的 app buffer 中。
三、tcp 连接细节
?总体流程如下:服务端使用 socket 函数创建套接字,调用 bind、listen 函数进入等待状态。客户端通过调用 connect 函数发起连接请求。需要注意的是,客户端只能等到服务器端调用 listen 函数后才能调 connect 函数。同时要清楚,客户端调用 connect 函数之前,服务器端有可能率先调用 accept 函数。当然,此时服务器端在调用 accept 函数是进入阻塞状态,知道客户端调用 connect 函数为止。
下面我们要逐一对这些步骤进行解释。
- socket() 函数
socket 函数的作用就是生成一个用于通信的套接字文件描述符 sockfd,这个文件描述符可以作为稍后 bind() 函数的绑定对象 - bind() 函数
服务程序通过分许配置文件,从中解析出象牙监听的地址和端口,再加上可以通过 socket() 函数生成的套接字 sockfd,就可以使用 bind() 函数将这个套接字绑定到要监听的地址和端口组合 "addr:port" 上,绑定了端口号的套接字可以作为 listen() 函数的监听对象。 - listen() 函数
listen() 函数就是监听已经通过 bind() 绑定了 "addr:port" 的套接字的。监听之后,套接字就从 CLOSE 状态转变为 LISTEN 状态,于是这个套接字就可以对外提供 TCP 连接的窗口了。
listen() 函数维护了两个队列:连接未完成队列 (syn queue) 和连接已完成队列 (accept queue),用来配合内核完成 TCP 三次握手和四次挥手过程(注意,这时还不涉及用户线程),当监听的 sockfd? 接收到某个客户端发来的 SYN 并回复了 SYN + ACK 之后,就会在连接未完成队列(syn queue)的尾部创建一个关于这个客户端的条目,并设置他的状态为 SYN_RECV,显然,这个条目中必须包含客户端的地址和端口相关信息,当监听的该条目再次受到这个客户端发送的 ACK 信息之后,就会把这个条目移入到连接已完成队列(accept queue),并设置它的状态为 ESTABLISHED。 - connect() 函数
connect() 函数是用于向某个已监听的套接字发起连接请求,也就是发起 TCP 三次握手过程。可以看出,连接请求方(如客户端)才会使用 connect 函数,当然,在发起connect之前,连接发起方也需要生成一个 sockfd,且使用的很可能是绑定了随机端口的套接字。既然 connect 函数是向某个套接字发起连接的,自然在使用 connect 函数时需要带上连接的目的地,即目标地址和目标端口,这正是服务端的监听套接字上绑定的地址和端口。同时,它还要带上自己的地址和端口,对于服务端来说,这就是连接请求的源地址和源端口。于真,TCP 连接的两端的套接字都已经成了五元组的完整格式。 - accept() 函数
listen 函数的连接已完成队列中维护着已经完成三次握手的连接,accept 函数的作用是读取已经完成连接队列的第一项(读完就从队列中移除),并对此项生成一个用于后续连接的套接字描述符(暂且用connfd来表示),有了新的连接套接字,用户进程/线程就可以通过这个连接套接字和客户端进行数据传输,而前文所说的监听套接字(sockfd)则仍然被监听者监听。
accept 函数是由用户空间进程发起,由内核空间操作,只要经过 accept 过的连接,连接将从已完成队列中移除,也就表示 TCP 已经建立完成了,两端的用户空间进程可以通过这个连接进行真正的数据传输了。
经过accept函数后,tcp连接的套接字从 sockfd 变成了 connfd,也就是说,经过 accept 之后,这个连接和 sockfd 套接字已经没有任何关系了。
|