c/c :tcp的三次握手和四次挥手(实验)-尊龙官方平台

c/c :tcp的三次握手和四次挥手(实验)

el/2024/3/25 16:53:38

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:

客户端:jiang@192.168.44.144

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 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 serverchar 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 serverint 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 terminateprintf("%s\n", msg);close(client_fd); // free connectionreturn 0;
}

服务端:test1280@192.168.44.148

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define backlog 16int main()
{// socketint 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);}// bindstruct sockaddr_in server_addr;server_addr.sin_family = af_inet; // ipv4server_addr.sin_port = htons(12500); // portserver_addr.sin_addr.s_addr = htonl(inaddr_any); // ipif (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){fprintf(stderr, "socket bind error=%d(%s)!!!\n", errno, strerror(errno));exit(1);}// listenif (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");// acceptint 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 clientchar 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 clientsnprintf(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 eofwhile (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(实际不可能发生)。

参考资料:《计算机网络 第六版 谢希仁》


http://www.ngui.cc/el/5127101.html

相关文章

tcp/ip:连接服务器失败(错误原因:connection refused)

tcp/ip:连接服务器失败(错误原因:connection refused) linux中,通过系统调用(system call) connect 连接指定服务器建立tcp连接。 connect 最常见的失败原因是 connection refused。 假设服务…

c/c :系统idle、进程cpu占用率、cpu核心数以及进程中的线程数

c/c:系统idle、进程cpu占用率、cpu核心数以及进程中的线程数 top命令可以显示当前进程的cpu占用率、cpu核心数以及系统忙闲程度(idle)。 他们之间有什么关系呢? 实验主机配置:物理cpu*4,逻辑cpu*8。 [r…

c:时间函数 localtime localtime_r

c:时间函数 localtime localtime_r localtime 和 localtime_r 的函数功能: converts the calendar time timep to broken-time representation 在调用 localtime 和 localtime_t 函数时,需特别注意: localtime 是不可重入函数&…

linux:strings 工具常用方法

linux:strings 工具常用方法 strings - print the strings of printable characters in files. strings prints the printable character sequences that are at least 4 characters long and are followed by an unprintable character. strings is mainly usef…

linux:ssh 信任免密登录

linux:ssh 信任免密登录 ssh 登录通常需要输入 remote 的用户名和密码。 可以通过在 local 和 remote 两侧进行一些信任配置,使得 local 通过 ssh 登录 remote 时不需要输入密码而直接登录。 主机: local: client192.168.44.150 [clientl…

lua:编译 lua 报错:error: readline/readline.h: no such file or directory

lua:编译 lua 报错:error: readline/readline.h: no such file or directory lua 版本:lua-5.3.5 在 lua-5.3.5 目录中执行 make linux,报错: gcc -stdgnu99 -o2 -wall -wextra -dlua_compat_5_2 -dlua_use_linux …

lua:linux 平台编译 lua 为动态链接库

lua:linux 平台编译 lua 为动态链接库 在 lua 源码的 makefile 中,没有编译动态链接库的目标(tagete),只可编译为静态库。 如何将 lua 编译为动态链接库呢? 仔细研究下 lua 的 makefile 文件,…

makefile:变量定义的优先级

makefile:变量定义的优先级 makefile 中使用的变量有三个定义来源: 1. environment: 当执行 make 的 shell 进程的环境表中定义相关变量(export)。 或者说 make 进程环境变量表,因为 make 进程是 shel…

lua:开源库 lua-cjson 安装及使用

lua:开源库 lua-cjson 安装及使用 开源库 lua-cjson 是一个简单小巧的动态库,可被 lua 脚本 require 加载。 在 lua 中通过一系列的 lua-cjson api 调用完成 lua 值与 json 值的相互转换(编码及解码)。 注:lua-cjso…

lua:lua-cjson:cannot serialise table: excessively sparse array

lua:lua-cjson:cannot serialise table: excessively sparse array code: local cjson require "cjson" local cjson2 cjson.new() local array {} array[19] 19 cjson2.encode(array) 执行时异常: cannot seria…
网站地图