一、TCP协议网络开发API
1、传输控制块(TCB)
传输控制块(TCB)是TCP协议的核心数据结构之一,它用于维护TCP连接状态和处理TCP数据传输。每个TCP连接都有一个对应的TCB,其中包含了该连接的相关信息,如序列号、确认号、窗口大小等。
在TCP连接过程中,TCB扮演着非常重要的角色。例如,在建立连接时,客户端和服务器端会相互发送SYN和ACK消息来协商初始序列号等信息,在这个过程中就需要使用到TCB;在数据传输阶段,每次发送或接收数据都需要更新TCB中的相关字段,以便双方能够正确地维护连接状态。
2、TCP服务端调用的API
// 1、socket 在文件系统中分配一个fd,并创建TCB数据结构。 int socket(int domain, int type, int protocol) // 2、bind 将TCP的socket绑定本地IP地址和端口。 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 3、listen 将TCP置于LISTEN状态。 int listen(int sockfd, int backlog); // 4、accept 从全连接队列中取出一个节点,并分配一个fd。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 5、recv 在对应fd中,从读缓冲区中拷贝出数据。 ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 6、send 把fd对应的TCB数据拷贝到写缓冲区中。 ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 7、close 准备一个FIN包,放到写缓冲区,是否关闭连接。 int close(int fd);
3、TCP客户端调用的API有:
// 1、socket int socket(int domain, int type, int protocol) // 2、bind int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 3、connect int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 4、recv ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 5、send ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 6、close int close(int fd);
二、TCP 建立连接
1、TCP首部行说明
TCP是一个面向连接的协议。无论哪一方向另一方发送数据之前,都必须先在双方之间
建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。同时由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式,所以需要四次挥手关闭连接。
下面的图是TCP首部的数据格式,它定义了TCP协议如何读取和解析数据:
1.TCP端口号
每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加
上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。
(五元组:源端IP + 源端端口号 + 目的端IP + 目的端端口号 + 协议)
2.TCP的序号
32位序号 seq:Sequence number 缩写seq ,用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
3.TCP的确认号
32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
4.TCP的标志位
SYN:同步标志位,用于建立会话连接,同步序列号;
ACK: 确认标志位,对已接收的数据包进行确认;
FIN: 完成标志位,表示发端完成发送任务,即将关闭连接;
PSH:推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;
RST:重置标志位,用于连接复位、拒绝错误和非法的数据包;
URG:紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;
2、TCP三次握手
一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。
第一次握手
客户端将TCP首部标志位的SYN置1,表示SYN报文。并随即产生一个TCP首部的序号值seq=x。接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据。之后,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手
服务端收到客户端的SYN报文后,首先服务端也随机产生一个TCP首部的序号值seq=y,其次把TCP首部的确认号置为ack=x+1,接着把TCP首部标志位的SYN和ACK置为1。最后把该报文发给客户端,该报文也不包含应用层数据,服务器端进入SYN_RCVD状态。
第三次握手
客户端收到服务端报文后,还要向服务端回应最后一个应答报文。首先该应答报文 TCP 首部标志位的ACK置为 1 ,其次TCP首部的确认号置为ack=y+1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
3、为何只用三次握手
(来自小林图解网络)
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
1.防止历史连接的建立:
在两次握手的情况下,假设服务端在收到历史连接的SYN 报文后,就会进入 ESTABLISHED 状态,并给客户端发送ACK确认报文。客户端收到ACK确认报文,判断到此次连接为历史连接,那么就会回 RST 报文来断开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,浪费了服务端的资源。
2.帮助双方同步初始化序列号
TCP 协议的通信双方, 都必须维护一个「序列号」seq, 序列号是可靠传输的一个关键因素,它的作用:
·接收方可以去除重复的数据;
·接收方可以根据数据包的序列号按序接收;
·可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
举个例子,现实生活中的人与人打电话
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你“
经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。
3、全连接和半连接
全连接和半连接是TCP协议中的两种连接状态。
半连接
半连接也称为不完全连接,是指在进行TCP三次握手后只建立了一部分的连接状态。在半连接状态下,仅有一方可以向对方发送数据,而对方不能回复。当接收到请求端发送的SYN报文时,被请求端会返回一个ACK报文确认该SYN已收到,并同时发送自己的SYN报文作为回应。但此时并没有完整建立起TCP链接。
全连接
全连接也称为完全连接,是指在进行TCP三次握手后建立的一种稳定的、可靠的连接状态。在全连接状态下,双方可以互相发送数据,并且保证数据传输的可靠性和顺序性。
三、TCP传输数据
使用send()函数发送数据时,返回正数不一定代表发送成功,仅仅表示成功将数据拷贝到内核协议栈的tcb,再由协议栈发送;发送过程中会经过N个网关,可能存在丢包或链路断开导致未能发送到目的地。如果要知道数据是否发送成功,需要加上确认机制(ACK)。
四、TCP 断开连接
1、四次挥手
第一次挥手
客户端发起关闭请求,向服务端发送TCP首部标志位FIN = 1、ACK = 1的报文段,之后客户端进入FIN_WAIT_1状态。
第二次挥手
服务端收到该报文后,向客户端发送标志位ACK = 1的确认报文段,接着服务端进入CLOSE_WAIT状态。
客户端收到服务吨的ACK确认报文段后,进入FIN_WAIT_2状态。
第三次挥手
服务端处理完数据之后,也向客户端发送TCP首部标志位FIN = 1、ACK = 1的报文段,之后服务端进入LAST_ACK状态。
第四次挥手
客户端收到服务端发送的FIN报文后,回复标志位是ACK的报文段,之后入客户端TIME_WAIT状态。
服务端收到客户端的ACK确认报文后,就进入CLOSE状态,至此服务端已经完成连接的关闭。
客户端在等待2MSL的时间后没有收到回复,自动进入CLOSE状态,至此服务端也已经完成连接的关闭
2、问题
(以下来自小林图解网络)
1)为什么需要四次挥手
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
2)为什么会出现三次挥手
当被动关闭方在 TCP 挥手过程中,没有数据要发送并且开启了 TCP 延迟确认机制,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:
-当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
-当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
-如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
3)为什么需要 TIME_WAIT 状态?
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
服务端在关闭连接之前发送的 seq= x 报文,但被网络延迟了。
接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 seq= x 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
- 保证「被动关闭连接」的一方,能被正确的关闭;
如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。
4)服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。
所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
5)如果已经建立了连接,但是客户端突然出现故障了怎么办?
客户端出现故障指的是客户端的主机发生了宕机,或者断电的场景。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISHED 状态,占用着系统资源。
为了避免这种情况,TCP 存在一个保活机制:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会每隔一个时间间隔,发送一个探测报文,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
五、TCP状态机