总结一下近日所学的关于 TCP 建立连接
三次握手
的过程中, TCP 的状态变化
, 以及在这个过程中所用的socket 函数
, 对于各种情况会如何处理涉及到
connect
listen
accept
函数
三次握手过程中 TCP 的状态变化
三次握手的过程和本质
如下图, 是一个 TCP 建立和断开连接的过程, 我们今天只讨论建立连接的三次握手过程
- 主动申请连接的一方(通常为客户端), 发送一个 SYN 报文段,
- 服务器在收到客户端发来的 SYN 报文段后, 回复一个 SYN 报文段, 并发送一个 ACK 以确认客户端的 SYN,
- 为了确认服务器的 SYN, 客户端也会回复一个 ACK
经过上面三个步骤交换三个报文段之后, 就建立起了一个 TCP 连接, 这也被称为 三次握手
, 三次握手的重点在于每次报文段的交换过程中, 都在数据包里包含了特殊的信息 :各自的初始序列号
, 简单来说, 在经历了:
的过程, 完成了 TCP 的建立, 完成了三次握手, 而三次握手的本质, 握的是各自的序列号
序列号的初始化
半随机
, Linux 下采用一个复杂的过程来选择他的初始序列号, 是一个基于时钟的方案, 以此来确保每一个连接的序列号唯一, 且不会轻易被猜出从而抵御攻击
三种状态
SYN_SENT
在服务端监听后,客户端 socket 执行 connect 连接时,客户端发送 SYN 报文,此时客户端就进入 SYN_SENT 状态,等待服务端的确认。
SYN_RCVD
表示服务端接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 socket 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,因为一般来说会立即回复一个 ACK ,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。
ESTABLISHED
表示连接已经建立了。
socket 函数
listen 函数
函数原型:
#include <sys.socket.h>
int listen(int sockfd, int backlog);
//成功返回 0, 出错返回 -1
socket 分为主动 / 被动套接字, 通常服务器在调用 socket 函数后, 调用 listen 函数, 将 sockfd 变为一个被动套接字
, 调用 listen 后, 也会导致 TCP 状态从 CLOSED 装换到 LISTEN (上图未标出该状态转换),
我们今天主要讨论其第二个参数 backlog
(未完成连接)
backlog 规定了内核应该为相应套接字排队的最大连接个数
在 Linux 下 (内核版本大于 2.2), 实现了两个队列, 一个 SYN 队列
, 一个 accept 队列
由上面的图得知, 一个新的连接完成, 即进入 ESTABLISHED 状态之前, 要经过一个中间状态 SYN_RCVD, 未经过最终 ACK 确认的连接在 SYN 队列存储着, 经过了最终 ACK 确认的连接进入 accept 队列等待应用调用 accept 函数取走就绪连接
SYN 队列存放着接收到连接请求的处于 SYN_RCVD 状态的连接
accept 队列存放着完成了三次握手处于 ESTABLISHED 状态的连接(但还未被调用 accept 取走)
当一个 SYN 到达时, 这条连接存储的大部分信息都被编码保存在 SYN + ACK 报文段的序列号字段, 只有当 SYN + ACK 报文段本身被确认后(并且已返回初始序列号), 才会分配真正的内存
服务端未处理连接队列
满的时候,它就会丢掉 client 端发送的三次握手中的最后一个 ACK 包,这就会导致,client 端以为自己已经建立连接了,但是实际在 server 端没有连接
connect 函数
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
//成功返回0, 出错返回 -1
- connect 何时返回呢
- TCP 客户端没有收到自己发出去的
SYN
的ACK
响应, 先尝试重发SYN
, 一段时间后仍未收到回应, 则返回ETIMEDOUT
- 客户端收到服务器返回的
RST
(复位报文), 返回ECONNREFUSED
- 客户端发送的 SYN 在某一个路由器发生了
不可达错误
, 客户端先尝试重发 SYN , 在一段时间仍未收到回应后, 返回EHOSTUNREACH
或ENETUNREACH
connect 会让当前状态从 CLOSED
转换到 SYN_SENT
, 如果成功返回 (意味着收到服务器的 SYN + ACK)
如果 connect 失败, 那么当前的套接字不再可用, 必须 close 关闭, 并重新获取一个新的套接字, 不能再次对其调用 connect 函数
accept 函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen* addlen);
//成功返回非负描述符, 出错返回 -1
accept 函数在服务器端调用, 会从 accept 队列中取出一个完成三次握手的连接
所以说如果长期不调用 accept 函数, 会导致 accept 队列满, 而这导致的情况我们下面讨论
accept 的后两个参数会保存客户端的地址结构, 如果不感兴趣, 可以置为 NULL
#值-结果参数
我们注意到, 有的函数调用时要求的结构体长度是一个值, 而有的要求为一个指针类型, 比如, bind 就是要求一个整数大小, 而 accept 要求一个指向该整数大小的指针
这是因为: 这些函数的数据传递方向可以分为 从内核到进程
和 从进程到内核
.
- 从进程到内核, 如 bind 函数, 内核需要知道他要从进程拷贝的数据量, 这个
值
类型的参数告诉内核这个数据结构有多大, 从而不至于越界 - 从内核到进程, 如 accept 函数, 第二个和第三个参数是作为了
返回结果
,
connect 函数在三次握手中的返回情况
前文提到了 listen 函数的参数 backlog, 会有两个队列, 那么 socket 函数针对这两个队列的不同情况会有什么反应呢?
- 如果 SYN 队列和 accept 队列都没有满, 那 connect 函数自然正常返回
- 如果 SYN 队列未满, accept 队列满了(
意味着没有及时调用 accept 函数
), connect 也会正常返回 - 但是如果 SYN 队列满了, 那么不管 accept 队列是什么情况, connect 都会返回 timeout 错误, 因为你服务器无法响应客户端的第一次连接请求, 这时就会像我们前面说的, TCP 客户端会尝试重发 SYN, 直到一段时间后返回错误
SYN 队列满时, 新的连接申请会被忽略丢弃
connect 何时正常返回呢?
connect 函数 ESTABLISHED 状态返回, 也就是说在客户端收到服务器返回的 ACK 时, conne 就已经正常返回, 这里一般不会出问题, 因为客户端一般都会立即回复 ACK 给服务器, 但是也有可能出现服务器没有收到客户端回复的最后一次 ACK, 即客户端进入 ESTABLISHED 状态, 但是服务器因为没有收到 ACK, 还是 SYN_RCVD 状态,
那么这种客户端认为连接已经建立, 服务器却没有这个新连接的通知, 这时, 服务器的 TCP 模块会将客户端到来的数据存入队列中