C/C++:TCP的三次握手和四次挥手(实验)
建立TCP连接需经过三次握手,释放TCP连接需经过四次挥手。
TCP报文段的首部中,比较重要的字段除了源端口、目的端口,还有以下几个字段与TCP连接的建立与释放相关:
1)序号
序号长度4字节。序号最大值是2的32次方(4B=4*8=32bit)减一,最小值为0。
序号增加到2^32-1后,下一个序号又回到0。即,序号使用 mod 2^32 运算。
TCP是面向字节流的协议,在应用层看来,数据是像“一滴水”(每个字节)一样在TCP协议(河道)中流动(数据河)。
序号的作用就是将每个在河道(协议/信道)中流动的水滴(每个字节)都按序编号。(第0个字节、第100个字节…)
一个TCP segment 的协议首部中序号值指的是本报文段所发送的数据的第一个字节的序号。
比如说序号值是401,携带的数据共有200字节,则本 TCP segment 的数据第一个字节在当前“河道”中的序号是401(第401个字节),最后一个字节的序号是600(第600个字节)。
TCP是全双工工作的,即在一条TCP连接中实际上有两条“流向”相反的数据字节“河流”。
一条“河流”将数据从客户端传送(流向)到服务端;一条“河流”将数据从服务端传送(流向)到客户端。
数据字节“河流”在一条TCP连接中有两个,在一条TCP连接中也有两个序号,分别维持记录着数据字节“河流”已经传送的字节量。
对一个Socket(同一个连接)可以同时进行读和写操作,是因为读和写是不同的“数据字节河流”,当然可以同时进行。
2)确认号
确认号长度4字节。取值范围、递增方式、计算方法同上述“序号”。
确认号是希望收到对方下一个TCP报文段的第一个数据字节的序号。
假设现在有C和S,C给S发送一个TCP报文段,这个TCP报文段中序号是701,实际数据长度是300。
当S正确收到这个报文段后,将回复给C一个确认报文段,告知C:
我(S)已经正确收到了一个你(C)发来的报文段,我(S)希望你(C)下一次发送的数据的序号是1001。
当C收到这个确认报文段后一看,确认号是1001,说明对端(S)希望我(C)发送的下个报文段的序号值是1001。
同时C也知道,这对S意味着,1001之前的数据 [701, 1000] 已经一个字节不落的全部被S接收完毕。
若收到 确认号 = N 的确认报文段,则表明 到序号 N-1 为止的所有数据 对端 都已经正确接收到。
3)确认ACK(ACKnowlegment)标识位
仅当 ACK = 1 时确认号字段有效。
TCP规定,在TCP连接建立后所有传送的报文段都必须把ACK置为1。
4)复位RST(ReSeT)标识位
RST=1 有几种情况:
a.
拒绝打开一个连接。
例如C去 connect 主机H上的12500端口,但实际H上并未有进程监听12500端口。
当主机H收到SYN请求连接包时,将由H的内核回复给C一个RST包告知其拒绝建立连接。
b.
拒绝一个非法报文段。
c.
TCP连接出现严重差错,必须释放连接,然后再重新建立连接。
5)同步SYN(SYNchronization)标识位
连接建立时用于同步序号的标识。
当C去 connect S的某监听端口时:
第一次握手:C发送TCP报文段到S,此报文段中SYN=1,ACK=0,包含C的起始序号。
第二次握手:S发送TCP报文段到C,此报文段中SYN=1,ACK=1,包含S的起始序号。
第三次握手:C发送TCP报文段到S,此报文段中SYN=0,ACK=1。
当报文段中 SYN=1 表示这是一个 连接请求 或 连接接受 报文。
6)终止FIN(FINs,结束)标识位
FIN=1 的报文段是连接终止报文段,用于释放一个TCP连接。
当C想要释放和S的连接时,C给S发送一个连接终止报文段,告知S:我(C)要释放TCP连接。
注:TCP连接双方都有权 主动 释放连接,主动发送连接终止报文段,终止一个TCP连接。
C发送 FIN=1 的报文段,C就是主动终止连接一方,C就不能再发送数据给S(但C依然可以接收来自S的数据)。
TCP三次握手:
注:
1.SYN为同步标识位;ACK为确认标识位;seq=序号值;ack=确认号;
2.SYN=1的报文段不能携带数据且要消耗掉一个序号。(即:一个SYN包如同携带单字节有效数据的TCP报文包);
3.ACK=1的报文段可以携带数据。如果不携带数据则不消耗序号。区别于SYN本身要消耗一个序号,ACK本身不消耗序号;
4.第一个报文段中的seq=x是初始序号由客户端决定,第二个报文段中的seq=y是初始序号由服务端决定;
TCP四次挥手:
注:
1)第二次挥手和第三次挥手的seq一个是v一个是w,说明在CLOSE-WAIT期间被动关闭一方可能向主动关闭一方传输数据;
2)第二次挥手和第三次挥手的ack都是u+1,说明在CLOSE-WAIT期间,主动关闭一方(先发FIN方)绝对不会再发送数据;
3)FIN=1的报文段可以携带数据,但同SYN=1一样,一定会消耗掉一个序号。
实验
Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
fprintf(stderr, "create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12500);
if (inet_pton(AF_INET, "192.168.44.148", &server_addr.sin_addr) <= 0)
{
fprintf(stderr, "inet_pton error!!!\n");
exit(1);
}
if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
fprintf(stderr, "socket connect error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
fprintf(stdout, "connect to server ok!\n");
// SEND msg to server
char msg[1024];
snprintf(msg, sizeof(msg), "Hi, I'm client!");
if (write(client_fd, msg, strlen(msg)) != strlen(msg))
{
fprintf(stderr, "send msg to server error!!!\n");
exit(1);
}
// RECV msg from server
int rbytes = read(client_fd, msg, sizeof(msg)-1);
if (rbytes < 0)
{
fprintf(stderr, "read error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
msg[rbytes] = 0; // null terminate
printf("%s\n", msg);
close(client_fd); // free connection
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BACKLOG 16
int main()
{
// socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
{
fprintf(stderr, "create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
int flag = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) < 0)
{
fprintf(stderr, "socket setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// bind
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(12500); // Port
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
fprintf(stderr, "socket bind error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// listen
if (listen(listen_fd, BACKLOG) < 0)
{
fprintf(stderr, "socket listen error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
fprintf(stdout, "server init ok, start to accept new connect...\n");
// accept
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0)
{
fprintf(stderr, "socket accept error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
fprintf(stdout, "accept one new connect!!!\n");
// RECV msg from client
char msg[1024] = "";
int rbytes = read(client_fd, msg, sizeof(msg)-1);
if (rbytes < 0)
{
fprintf(stderr, "read error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
msg[rbytes] = 0;
fprintf(stdout, "%s\n", msg);
// SEND msg to client
snprintf(msg, sizeof(msg), "Hi, I'm server");
if (write(client_fd, msg, strlen(msg)) != strlen(msg))
{
fprintf(stderr, "send msg to client error!!!\n");
exit(1);
}
// read client-FIN=1 EOF
while (read(client_fd, msg, sizeof(msg)-1) == 0)
break;
close(client_fd);
return 0;
}
代码概要:
1)客户端向服务端发起连接请求,建立TCP连接;
2)客户端向服务端发送数据,在Server侧接收;
3)服务端向客户端发送数据,在Client侧接收;
4)客户端主动断开连接,发送FIN终止连接包到服务端;
5)服务端被动断开连接,发送FIN终止连接包到客户端;
编译:
[jiang@localhost client]$ gcc -o client client.c
[jiang@localhost client]$ ll
total 16
-rwxrwxr-x. 1 jiang jiang 9670 Jun 10 07:59 client
-rw-rw-r--. 1 jiang jiang 1358 Jun 10 07:37 client.c
[test1280@localhost server]$ gcc -o server server.c
[test1280@localhost server]$ ll
total 16
-rwxrwxr-x. 1 test1280 test1280 9893 Jun 10 07:59 server
-rw-r--r--. 1 test1280 test1280 1980 Jun 10 07:37 server.c
运行:
[jiang@localhost client]$ ./client
connect to server ok!
Hi, I'm server
[test1280@localhost server]$ ./server
server init ok, start to accept new connect...
accept one new connect!!!
Hi, I'm client!
使用tcpdump抓包:
[root@localhost ~]# tcpdump tcp port 12500 -i eth0 -s 0 -w jiang.cap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
^C10 packets captured
10 packets received by filter
0 packets dropped by kernel
分析
a.
144客户端向148服务端发起TCP连接请求,服务端监听端口为12500,客户端随机(内核分配)端口为39928。
这是第一次握手,SYN标识位为1,客户端起始序号为0。
b.
148服务端向144客户端发送TCP连接接受报文。
这是第二次握手,SYN标识位为1,服务端起始序号为0。
由于第一次握手中SYN标识位消耗一个序号,并且第一次握手的起始序号为0,服务端期待收到客户端的下个序号是0+1=1,ack为1,且ACK为1。
c.
144客户端向148服务端发送对第二次握手的确认报文段。
这是第三次握手,SYN标识位为0。
由于第二次握手中SYN标识位消耗一个序号,并且第二次握手的起始序号为0,客户端期待收到服务端的下个序号是0+1=1,ack为1,且ACK为1。
d.
144客户端向148服务端发送数据:Hi, I’m client!(15字节)
第一个字节H在由客户端到服务端方向的“河流中”的序号是第二次握手中服务端期待的序号值:1。(seq=1)
TCP规定当连接建立后,每个报文段的ACK标识位都为1,此处ACK=1。
e.
148服务端收到step d的数据报文段后,回复给144客户端确认报文段。
ack的值为第一次握手的SYN序号(+1)和144客户端向148服务端传送的数据(+15)共计16字节[0, 15],ack=15+1=16。
f.
148服务端向144客户端发送数据:Hi, I’m server(14字节)
第一个字节H在由服务端到客户端方向的“河流中”的序号是第三次握手中客户端期待的序号值:1。(seq=1)
我们观察发现,虽然148服务端发送到144客户端很多报文,但实际上都是一些应答并未携带有效数据,除了第一个连接受理报文。
g.
144客户端向148服务端发送确认报文段。
ack的值为第二次握手的SYN序号(+1)和148服务端向144客户端传送的数据(+14)共计15字节[0, 14],ack=14+1=15。
h.
这是第一次挥手,FIN=1。
144客户端主动关闭连接,向148服务端发送连接终止报文段(FIN=1)。
在这个挥手报文段中并未携带数据,但是FIN标识位要消耗占用一个序号。
i.
这是第二次第三次挥手(合并),FIN=1。
148服务端收到来自144客户端的FIN报文段,向144客户端发送FIN=1终止报文段。
148服务端期待下次收到来自144客户端的报文段序号值是(16+1=17),因为第一次挥手的FIN消耗占用一个序号呀!
虽然148服务端还“期待下次收到来自144客户端的报文段,序号值是17”,但实际上已不可能发生(144客户端不会再写数据)。
j.
这里的FIN=1也需要消耗占用一个序号,FIN=1占用的是SEQ=16字节的序号值。
144客户端对148服务端的应答确认报文段。
由于 step i 中FIN=1需占用SEQ=16字节的序号值,所以144客户端期待148服务端的下个报文段序号值为17(实际不可能发生)。
参考资料:《计算机网络 第六版 谢希仁》