套接字简介
位于应用层的应用程序在基于 TCP 协议或 UDP 协议进行通信时,需要用到操作系统提供的类库,这种类库一般称为 API(Application Programming Interface,应用编程接口)。
使用 TCP 或 UDP 时,又会广泛使用到 Socket(套接字)API,Socket 原本是由 BSD UNIX 开发的,但是后来被移植到 Windows 的 Winsock 以及嵌入式系统中。应用程序利用 Socket,可以设置对端的 IP 地址、端口号,并实现数据的接收和发送:
下面我们分别以 TCP 和 UDP 为例,详细介绍 Socket 的底层原理和相关 API 函数。我们先从较为复杂的 TCP 开始。
TCP套接字编程
TCP 的服务端要先监听一个端口,一般是先调用 bind 函数,给这个 Socket 赋予一个 IP 地址和端口。
当服务端有了 IP 和端口号,就可以调用 listen 函数进行监听。在 TCP 的状态图里面,有一个 listen 状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。
在操作系统内核中,为每个 Socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。
接下来,服务端调用 accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。
在服务端等待的时候,客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手,操作系统会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 Socket 用于传输数据。
这里需要注意的是,负责监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作监听 Socket,一个叫作已连接 Socket。
连接建立成功之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
下面这个图就是基于 TCP 协议的 Socket API 函数调用过程:
说 TCP 的 Socket 就是一个文件流,是非常准确的。因为,Socket 在 Linux 中就是以文件的形式存在的。除此之外,还存在文件描述符。写入和读出,也是通过文件描述符。
如果你留心过 Nginx 里面 PHP-FPM 的配置,就会发现有两种方式将 PHP 动态请求转发给 PHP-FPM,一种是 IP 地址+端口号,例如:127.0.0.1:9000,一种是 Socket 文件,例如:unix:/run/php/php7.1-fpm.sock。这里也可以表明,Socket 在 Linux 中确实以文件形式存在,由于不需要建立额外的网络请求,所以后者效率更高,但是由于是本地文件,所以不能跨机器访问,如果 Nginx 和 PHP-FPM 部署在不同的机器,只能通过前一种方式转发请求。