我们知道 UDP 协议乐观且心大,相信网络环境比较健康,数据是可以送达的,即使送达不了也没关系。而 TCP(Transmission Control Protocol,传输控制协议) 就不一样了,它是悲观且严谨,认为网络环境是恶劣的,丢包、乱序、重传和拥塞是常有的事,一言不合就可能送达不了了,因而要从算法层面来保证可靠性。
TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。本文一起看一下 TCP 协议保证可靠性的机制之一:提供可靠的连接服务,通过三次握手建立连接,通过四次挥手关闭连接。
我们先来看下 TCP 头的格式。从下图可以看成,它比 UDP 复杂得多。
首先,源端口号和目标端口号必不可少,这跟 UDP 是一样的。因为如果没有这两个端口号,数据就不知道发给哪个应用。
接下来是包的序号。给包编号的好处是可以解决乱序的问题。
还应该有确认序号。发出的包应该有确认,不然怎么知道对方收到了没?没收到就应该重新发送,直到送达,可以有效地解决丢包问题。
接下来是一些状态位。例如 SYN(Synchronize Sequence Number)是发起一个连接,ACK(Acknowledgement)是回复,FIN(Finish)是结束连接等。TCP 是面向连接的,所以双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态的变更。
还有一个重要的就是窗口大小。TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前的处理能力,避免对方发送太快或太慢。
TCP 除了做流量控制,还会做拥塞控制,对于道路的拥堵无能为力,只能控制自己的发送速度。不过拥塞窗口没有体现在 TCP 包头部。
无论哪一方向另一方发送数据,都必须先在双方之间建立一条连接。
TCP 的连接建立,我们常常称为三次握手。
具体到 TCP 协议,三次握手建立连接如下图所示:
一开始,A 和 B 都处于 CLOSED 状态。B 主动监听某个端口,处于 LISTEN 状态。
A、B 会在第一次发送 SYN 时分别生成随机初始序列号 seq=x,seq=y 接下来,每次交流时,seq=自己上一个包的序列号+1,ack=对方上一个包的序列号+1(期望收到对方下一包数据的序列号)。
可以看到,三次握手后,就可以确认双方的接收能力和发送能力是否正常、同步双方的序列号,为后面的可靠性传输做准备。
为什么序号不能都从 1 开始?
因为这样往往会出现冲突。例如,A 连上 B 后,发送了 1、2、3 三个包,但发送 3 时,丢了或有延迟,于是重新发送。后来 A 掉线了,重新连上 B,序号又从 1 开始,然后发送 2,但并没有想发送 3,但上次延迟的 3 又到了,发给了 B,B 以为这是下一个包,于是发生了错误。
为什么需要三次握手?
我们知道,三次握手主要是为了确认双方的接收能力和发送能力是否正常、同步双方的初始序列号,那么两次或四次可以吗?
同样,我们在设计中往往也是需要考虑各种异常情况的,这样才能提高程序的健壮性。
看完了建立连接,我们看下关闭连接,关闭连接通常被称为四次挥手。
这时候,只是 A 没有要说的了,即 A 不会再发送数据,但 B 能不能在 ACK 的时候直接关闭呢?不行的,很可能 B 还有话要说,还是可以发送数据,所以称为半关闭状态。
这个时候,A 可以选择不再接收数据,也可以选择最后在接收一段数据,等待 B 也主动关闭。
这样整个连接就关闭了。
具体到 TCP 协议,四次挥手关闭连接如下图所示:
客户端和服务端都处于 ESTABLISHED 状态。
为什么客户端在四次挥手后还要等待 2MSL 后才会真正关闭连接?
MSL(Maximum Segment Lifetime,报文最大生存时间),是任何报文在网络上存在的最长时间,超过这个时间的报文将被丢弃。
为什么需要四次挥手?
TCP 是全双工的(任何时刻数据都可以双向收发), A 到 B 是一个通道,B 到 A 又是另一个通道。
当 A 执行完第一次挥手后,只能证明 A 不会再向 B 请求数据,B 返回确认后,便不再接收 A 的数据。
但是 B 可能还在给 A 发送数据,A 还是可以接收数据的。只有当 B 需要把数据传输完毕后才能发送关闭请求,且确认 A 接收后,两边才会真正断开连接。
Socket 封装了底层 TCP / IP 协议栈的功能,供应用层使用。Socket 在不同的操作系统上有不同的版本,我们下面看下 Linux 系统上的 C 语言版本。
为了简单起见,以下程序省略了一些错误处理。
服务端 server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc , char *argv[]) {
// Create a socket with IPv4 domain and TCP protocol
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("Fail to create a socket.");
}
struct sockaddr_in server_address, client_address;
unsigned int addr_len = sizeof(client_address);
bzero(&server_address, sizeof(server_address));
// Bind the socket with the values address and port from the sockaddr_in structure
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(8700);
bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address));
// listen on specified port with a maximum of 5 requests
listen(sockfd, 5);
char input_buffer[256] = {};
char message[] = {"Hi, client!"};
while (1) {
// Accept connection signals from the client
int client_sockfd = accept(sockfd, (struct sockaddr*)&client_address, &addr_len);
// Receive data sent by the client
recv(client_sockfd, input_buffer, sizeof(input_buffer), 0);
printf("[2] Recv: %s\n",input_buffer);
sleep(1);
// Send data to the client
send(client_sockfd, message, sizeof(message), 0);
printf("[3] Send: %s\n", message);
sleep(1);
// Terminate the socket connection
close(client_sockfd);
printf("[6] Close Socket\n");
}
return 0;
}
客户端 client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc , char *argv[]) {
// Create a socket with IPv4 domain and TCP protocol
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("Fail to create a socket.");
}
struct sockaddr_in info;
bzero(&info, sizeof(info));
// Connect to the server
info.sin_family = AF_INET;
info.sin_addr.s_addr = inet_addr("127.0.0.1");
info.sin_port = htons(8700);
int err = connect(sockfd, (struct sockaddr *)&info, sizeof(info));
if (err == -1) {
printf("Connection error");
}
// Send message to the server
char message[] = {"Hi, server! I'm client."};
send(sockfd, message, sizeof(message), 0);
printf("[1] Send: %s\n", message);
// Receive a message from the server
char receiveMessage[100] = {};
recv(sockfd, receiveMessage, sizeof(receiveMessage), 0);
printf("[4] Recv: %s\n", receiveMessage);
// Terminate the socket connection
close(sockfd);
printf("[5] Close Socket\n");
return 0;
}
程序运行结果如下。
服务端:
$ gcc server.c -o server # Linux 上编译 C 语言程序
$ ./server # Linux 上运行服务端
[2] Recv: Hi, server! I'm client.
[3] Send: Hi, client!
[6] Close Socket
新打开一个终端,运行客户端:
$ gcc client.c -o client # Linux 上编译 C 语言程序
$ ./client # Linux 上运行客户端
[1] Send: Hi, server! I'm client.
[4] Recv: Hi, client!
[5] Close Socket
如果在运行客户端之前先打开一个新的终端并运行 tcpdump 命令进行抓包:
sudo tcpdump -S -i any tcp port 8700
可以得到如下输出:
# 三次握手
# 标志位:SYN
14:04:19.846717 IP localhost.57400 > localhost.8700: Flags [S], seq 3120777010, win 65495, options [mss 65495,sackOK,TS val 1331636511 ecr 0,nop,wscale 7], length 0
# 标志位:SYN + ACK
14:04:19.846741 IP localhost.8700 > localhost.57400: Flags [S.], seq 1145627628, ack 3120777011, win 65483, options [mss 65495,sackOK,TS val 1331636511 ecr 1331636511,nop,wscale 7], length 0
# 标志位:ACK
14:04:19.846761 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627629, win 512, options [nop,nop,TS val 1331636511 ecr 1331636511], length 0
# 收发数据
14:04:19.846794 IP localhost.57400 > localhost.8700: Flags [P.], seq 3120777011:3120777035, ack 1145627629, win 512, options [nop,nop,TS val 1331636511 ecr 1331636511], length 24
14:04:20.847301 IP localhost.8700 > localhost.57400: Flags [P.], seq 1145627629:1145627641, ack 3120777035, win 512, options [nop,nop,TS val 1331637512 ecr 1331636511], length 12
14:04:20.847328 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627641, win 512, options [nop,nop,TS val 1331637512 ecr 1331637512], length 0
# 四次挥手
# 标志位:FIN + ACK
14:04:20.847499 IP localhost.57400 > localhost.8700: Flags [F.], seq 3120777035, ack 1145627641, win 512, options [nop,nop,TS val 1331637512 ecr 1331637512], length 0
# 标志位:ACK
14:04:20.888037 IP localhost.8700 > localhost.57400: Flags [.], ack 3120777036, win 512, options [nop,nop,TS val 1331637553 ecr 1331637512], length 0
# 标志位:FIN + ACK
14:04:21.847617 IP localhost.8700 > localhost.57400: Flags [F.], seq 1145627641, ack 3120777036, win 512, options [nop,nop,TS val 1331638512 ecr 1331637512], length 0
# 标志位:ACK
14:04:21.847644 IP localhost.57400 > localhost.8700: Flags [.], ack 1145627642, win 512, options [nop,nop,TS val 1331638512 ecr 1331638512], length 0
也可以尝试将以上服务端和客户端程序分别运行在两台机器上,并将 IP 地址 "127.0.0.1" 替换成所在机器的真实 IP,体验下两台机器间的通信。
TCP 通过三次握手建立连接,通过四次挥手断开连接。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。