19.TCP协议解析、tcpdump抓包分析三次握手和四次挥手

转载自:https://blog.csdn.net/Hanoi_ahoj/article/details/105477135

1.tcp报文格式:
在这里插入图片描述

16 位源端口,16 位目的端口:
数据从何而来,去向何方。

32 位序号,32 位确认序号:
和 TCP 的 ACK 机制有关,发送端给数据进行编号,接收端收到数据后确认收到哪些编号的数据。

4 位报头长度:
表示 TCP 的首部占用多少个 4 字节。

6 个标志位:
URG 紧急指针是否有效;
ACK 确认号是否有效;
PSH 提示接收端应用程序立刻从TCP缓冲区把数据读走;
RST 复位标志,对方要求重新建立连接;
SYN 同步标志,请求建立连接;
FIN 结束标志,通知对方, 本端要关闭了。

16 位窗口大小:
和 TCP 滑动窗口相关,在下文。

16 位校验和:
发送端填充,CRC 校验,接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。

16 位紧急指针:
标识哪部分数据是紧急数据。

选项和填充位:
为了对其做相关填充?

2.一个 TCP 程序:
TCP 回显服务器:

#include <stdio.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() 
{
    
    
    // 1. 创建 socket
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) 
	{
    
    
        perror("socket");
        return -1;
    }

    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. bind ip 和 port
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(9090);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.141");
	
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) 
	{
    
    
        perror("bind");
        return -1;
    }

    // 3. 进入 listen 监听到来的连接
    if (listen(listen_fd, 128) < 0) 
	{
    
    
        perror("listen");
        return -1;
    }

 
	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);
	int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
	if (connect_fd < 0) 
	{
    
    
		perror("accept");
		return -1;
	}

	printf("%s:%d is connected!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

	while (1) 
	{
    
    
		char req[1024] = {
    
    0};
		int n = read(connect_fd, req, sizeof(req));
		if (n < 0) 
		{
    
    
			perror("read");
			continue;
		} 
		else if (n == 0) 
		{
    
    
			printf("%s:%d is closed!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
			close(connect_fd);
			break;
		} 
		else 
		{
    
    
			req[n] = '\0';
			printf("recv <-- %s\n", req);
					
			char *resp = req;
			int nw = write(connect_fd, req, strlen(req));
			if (nw < 0) {
    
    
				perror("write");
				continue;
			}
			printf("send --> %s\n", req);
		}
	}

    close(listen_fd);
    return 0;
}

TCP 回显客户端:

#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() 
{
    
    
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) 
	{
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(9090);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.141");
	
    if (connect(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) 
	{
    
    
        perror("connect");
        return -1;
    }

    while (1) 
	{
    
    
        printf("send --> ");
        char req[1024] = {
    
    0};
        scanf("%s", req);
		
		if(strncmp(req,"quit",4) == 0)
		{
    
    			
			break;
		}
		
        int n = write(socket_fd, req, strlen(req));
        if (n < 0) 
		{
    
    
            perror("write");
            continue;
        }

        char resp[1024] = {
    
    0};
        n = read(socket_fd, resp, sizeof(resp));
        if (n < 0) 
		{
    
    
            perror("read");
            continue;
        }
        if (n == 0) 
		{
    
    
            printf("server closed!\n");
            break;
        }
        printf("recv <-- %s\n", resp);
    }

    close(socket_fd);
    return 0;
}

测试:
在这里插入图片描述
3.确认应答:
主机A Data 1~1000> 主机B
主机A <=收到 再从1001开始发= 主机B
主机A =Data 1001~2000=> 主机B
主机A <=收到 再从2001开始发= 主机B

TCP将每个包的数据都进行了编号,即为序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。

4.超时重传:

4.1.发送的数据未到达的情况:
主机A Data 1~1000X 主机B
…等待一会,没有收到 主机B 的确认应答…
主机A =Data 1~1000=> 主机B
主机A <=收到 再从1001开始发= 主机B

4.2.响应数据未到达的情况:
主机A Data 1~1000> 主机B
主机A X=收到 再从1001开始发= 主机B
…等待一会,没有收到 主机B 的确认应答…
主机A =Data 1~1000=> 主机B
主机A <=收到 再从1001开始发= 主机B

主机A,主机B 可能因此会接收到多次同一个数据,TCP 会根据这些数据的序列号来去重。

4.3.超时时间是多少呢?
最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”. 但是这个时间的长短, 随着网络环境的不同, 是有差异的;
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包,造成计算、网络资源的浪费。

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。

Linux 中(BSD Unix和Windows也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍;
如果重发一次之后, 仍然得不到应答, 等待 2x500ms 后再进行重传.
如果仍然得不到应答, 等待 4x500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。

5.连接管理
5.1.建立连接,三次握手
在这里插入图片描述
服务器端通过 socket,bind 和 listen 完成了被动套接字的准备工作,被动的意思就是等着别人来连接,然后调用 accept,就会阻塞在这里,等待客户端的连接来临;

客户端通过调用 socket 和 connect 函数之后,也会阻塞。接下来的事情是由操作系统内核完成的,更具体一点的说,是操作系统内核网络协议栈在工作。

5.2.三次握手流程:
1)客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;
2)服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
3)客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
4)应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

5.3.客户端状态变化:
客户端调用 socket() 后,进入 CLOSED 状态。
客户端调用 connect(),发送 SYN 报文,进入 SYN_SENT 状态。
客户端在收到刚刚 SYN 报文的 ACK 后,进入 ESTABLISHED 状态,从 connect() 返回。

5.4.服务端状态变化:
服务器调用 socket() 后,进入 CLOSED 状态。
服务器调用 bind(), listen() 后进入 LISTEN 状态,等待客户端连接,阻塞在 accept()。
收到 SYN 报文后进入 SYN_RCVD 状态,就将该连接放入内核等待队列中,返回 SYN + ACK 报文。
在客户端收到后,发送 ACK 报文,服务器从 accept() 返回,进入 ESTABLISHED 状态。
至此连接建立完成,客户端、服务端都进入了 已连接 状态,即 ESTABLISHED。

5.5.linux tcpdump抓包
1)服务器启动:
$ ./server

2)抓包工具启动:
$ sudo tcpdump -i eth0

3)客户端启动:
./client

4)三次握手的抓包数据:

aston@ubuntu:/mnt/hgfs/share/insight/0.interview/2.tcp_ip_test$ sudo tcpdump -i eth0
[sudo] password for aston: 
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
07:06:15.876362 IP 192.168.1.10.937 > 192.168.1.141.nfs: Flags [S], seq 2753872176, win 14600, options [mss 1460,sackOK,TS val 38359 ecr 0,nop,wscale 2], length 0
07:06:15.876431 IP 192.168.1.141.nfs > 192.168.1.10.937: Flags [S.], seq 3137960352, ack 2753872177, win 28960, options [mss 1460,sackOK,TS val 91222247 ecr 38359,nop,wscale 7], length 0
07:06:15.877469 IP 192.168.1.10.937 > 192.168.1.141.nfs: Flags [.], ack 1, win 3650, options [nop,nop,TS val 38366 ecr 91222247], length 0
07:06:15.877507 IP 192.168.1.10.937 > 192.168.1.141.nfs: Flags [P.], seq 1:121, ack 1, win 3650, options [nop,nop,TS val 38366 ecr 91222247], length 120: NFS request xid 2311710952  access fh Unknown/0100070105580A0000000000E46E78EB87B14F25B75819649125D60A3B580A00 NFS_ACCESS_READ|NFS_ACCESS_LOOKUP|NFS_ACCESS_MODIFY|NFS_ACCESS_EXTEND|NFS_ACCESS_DELETE
07:06:15.877513 IP 192.168.1.141.nfs > 192.168.1.10.937: Flags [.], ack 121, win 227, options [nop,nop,TS val 91222247 ecr 38366], length 0
07:06:15.877636 IP 192.168.1.141.nfs > 192.168.1.10.937: Flags [P.], seq 1:125, ack 121, win 227, options [nop,nop,TS val 91222247 ecr 38366], length 124: NFS reply xid 2311710952 reply ok 120 access c 001f

5.6.问题:为什么客户端回复时ack=1呢?而不是seq+1
客户端–>服务器: seq 2753872176,
服务器–>客户端: seq 3137960352, ack 2753872177 //ack等于 客户端2753872176+1
客户端–>服务器: ack 1 //?

原因:tcpdump显示的是相对序列值:
解决:
如果想显示绝对数值,在使用tcpdump命令时加上-S即可

tcpdump -i eth0 -S

5.7.tcpdump抓包保存成.cap格式,然后用wireshark分析;
root@ubuntu:~# tcpdump -i eth0 -w test.cap

分析:
客户端-->服务器: seq 45 a5 8c b6, 	ack 0
服务器-->客户端: seq 6a 6c bb 20, 	ack 45 a5 8c b7	//ack等于 客户端seq+1
客户端-->服务器: seq 45 a5 8c b7,  ack 6a 6c bb 21	//ack等于 服务器seq+1

结论:
tcpdump和wireshark的seq和ack都是以 "relative sequence number"的形式显示的,
而实际的十六进制数据是绝对的数值;

5.8.tcpdump显示绝对数值:在使用tcpdump命令时加上-S即可

root@ubuntu:~# tcpdump -i eth0 -S
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
07:37:06.157597 IP 192.168.1.10.945 > 192.168.1.141.nfs: Flags [S], seq 2084759064, win 14600, options [mss 1460,sackOK,TS val 223408 ecr 0,nop,wscale 2], length 0
07:37:06.157660 IP 192.168.1.141.nfs > 192.168.1.10.945: Flags [S.], seq 2467243379, ack 2084759065, win 28960, options [mss 1460,sackOK,TS val 91684817 ecr 223408,nop,wscale 7], length 0
07:37:06.158889 IP 192.168.1.10.945 > 192.168.1.141.nfs: Flags [.], ack 2467243380, win 3650, options [nop,nop,TS val 223408 ecr 91684817], length 0
15 packets captured
15 packets received by filter
0 packets dropped by kernel

6.tcpdump、telnet 查看四次挥手:
在这里插入图片描述

6.1.标准的四次挥手(网上复制的)
说明:localhost.45788是客户端,localhost.ssh是服务器:

23:13:14.956630 IP localhost.45788 > localhost.ssh: Flags [F.], seq 3349160669, ack 377690074, win 342, options [nop,nop,TS val 1927014999 ecr 1927003705], length 0
23:13:14.956738 IP localhost.ssh > localhost.45788: Flags [.], ack 3349160670, win 342, options [nop,nop,TS val 1927015000 ecr 1927014999], length 0
23:13:14.959316 IP localhost.ssh > localhost.45788: Flags [F.], seq 377690074, ack 3349160670, win 342, options [nop,nop,TS val 1927015002 ecr 1927014999], length 0
23:13:14.959328 IP localhost.45788 > localhost.ssh: Flags [.], ack 377690075, win 342, options [nop,nop,TS val 1927015002 ecr 1927015002], length 0

在这里插入图片描述
6.2.有时候tcpdump抓取的挥手包只有3条(自己实际抓取的)
说明:192.168.1.10.32823是客户端,192.168.1.141.9090是服务器:

11:34:53.378185 IP 192.168.1.10.32823 > 192.168.1.141.9090: Flags [F.], seq 187740589, ack 2824594749, win 3650, options [nop,nop,TS val 79322 ecr 92901547], length 0
11:34:53.378331 IP 192.168.1.141.9090 > 192.168.1.10.32823: Flags [F.], seq 2824594749, ack 187740590, win 227, options [nop,nop,TS val 92904590 ecr 79322], length 0
11:34:53.379595 IP 192.168.1.10.32823 > 192.168.1.141.9090: Flags [.], ack 2824594750, win 3650, options [nop,nop,TS val 79322 ecr 92904590], length 0

出现的原因:
这是TCP的捎带ACK机制。
服务器将FIN的ACK和自己的FIN包一起发给了客户端

在这里插入图片描述

6.3.客户端、服务器状态变化:

1)客户端调用 close() ,发送一个FIN报文给服务器,客户端进入 FIN_WAIT_1 状态;
2)服务器收到 FIN 报文,响应一个ACK,也进入CLOSE_WAIT状态。
  服务器 read() 获取到了EOF,则执行 close() ,发送FIN报文,此时进入 LAST_ACK 状态; 
3)客户端收到服务器关于FIN的ACK报文,进入FIN_WAIT_2状态;
4)客户端收到服务器的FIN报文,进入 TIME_WAIT 状态,发送FIN的响应ACK给服务器;
5)客户端会在 TIME_WAIT 状态等待2MSL,然后进入CLOSE状态;

7.MSL(maximum segment lifetime)最长分节生命期。
Linux 系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为 60 秒。
也就是说,Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

查看 MSL:cat /proc/sys/net/ipv4/tcp_fin_timeout
root@ubuntu:~# cat /proc/sys/net/ipv4/tcp_fin_timeout
60

8.TIME_WAIT 状态
只有发起连接终止的一方会进入 TIME_WAIT 状态!
TIME_WAIT 的状态可能会超出进程的生命周期!
TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭。

8.1.将上面的TCP回显服务器、客户端启动后,Ctrl+c结束,再次启动server就会出现这样的情况:
在这里插入图片描述
8.2.运行一个服务器,再开启第二次出现的情况:(显示该服务器已经在使用,并处于 LISTEN 状态)

aston@ubuntu:/mnt/hgfs/share/insight/0.interview/2.tcp_ip_test$ ./server
bind: Address already in use
aston@ubuntu:/mnt/hgfs/share/insight/0.interview/2.tcp_ip_test$ netstat -anp | grep 9090
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 192.168.1.141:9090      0.0.0.0:*               LISTEN      9134/server     
aston@ubuntu:/mnt/hgfs/share/insight/0.interview/2.tcp_ip_test$ 

9.为什么要有这个 TIME_WAIT 状态,而不直接进入 CLOSED ?
9.1.为了确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭;
TCP 假设报文会出错,需要重传。

如果主机1给主机2回的 ACK 报文丢了,那主机2就当主机1没有收到,触发主机2的超时重传,重新发送 FIN 报文。这样处在 TIME_WAIT 状态的主机1还是可以收到这个 FIN 报文并且回复 ACK n+1 的,这样就可以确保对端也可以正常关闭。

如果主机 1 没有维护 TIME_WAIT 状态,而直接进入 CLOSED 状态,它就失去了当前状态的上下文,只能回复一个 RST 操作,从而导致被动关闭方出现错误。
如果在 TIME_WAIT 时间内,因为主机 1 的 ACK 没有传输到主机 2,主机 1 又接收到了主机 2 重发的 FIN 报文,那么 2MSL 时间将重新计时。
2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的!
在这里插入图片描述

9.2.和连接“化身”和报文迷走有关系,为了让旧连接的重复分节在网络中自然消失。
在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,如路由器重启,链路突然出现故障等。如果迷走报文到达时,发现 TCP 连接四元组(源 IP,源端口,目的 IP,目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃。我们考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。

9.3.为甚是 2MSL?不是 1MSL 或 3MSL?
保证在两个传输方向 上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到 来自上一个进程的迟到的数据, 但是这种数据很可能是错误的)。

因为客户端—ACK—> 服务端 (需要1MSL)
服务端 —FIN—> 客户端 (也需要1MSL,所以需要 2MSL)

9.4.TIME_WAIT 的危害?
1)内存资源占用,进程以及关闭,操作系统还在维护着一个 TIME_WAIT 状态处理一些迟到的响应。
2)对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。如果 TIME_WAIT 状态过多,会导致无法创建新连接。
就是上面的例子,服务器再次启动会出现 address already in used。

9.5.如何优化 TIME_WAIT?
1)通过 sysctl 命令,将系统值调小。这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,
系统就会将所有的 TIME_WAIT 连接状态重置,并且只打印出警告信息。

2)调低 TCP_TIMEWAIT_LEN,重新编译系统。

3)设置 SO_LINGER。linger,停留。

int setsockopt(int sockfd, int level, int optname, const void *optval, 
				socklen_t optlen);
struct linger {
    
     
	int  l_onoff;    /* 0=off, nonzero=on */ 
	int  l_linger;    /* linger time, POSIX specifies units as seconds */
};

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s,SOL_SOCKET,SO_LINGER, &so_linger,sizeof(so_linger));

说明:

如果 l_onoff 为 0,那么关闭本选项。l_linger 的值被忽略,这对应了默认行为,close 或 shutdown 立即返回。如果在套接字发送缓冲区中有数据残留,系统会将试着把这些数据发送出去。

如果 l_onoff 为非 0, 且 l_linger 值也为 0,那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。这种关闭的方式称为“强行关闭”。 在这种情况下,排队数据不会被发送,被动关闭方也不知道对端已经彻底断开。只有当被动关闭方正阻塞在recv()调用上时,接受到 RST 时,会立刻得到一个“connet reset by peer”的异常。

如果 l_onoff 为非 0, 且 l_linger 的值也非 0,那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到。

4)设置 net.ipv4.tcp_tw_reuse
从协议角度理解如果是安全可控的,可以复用处于 TIME_WAIT 的套接字为新的连接所用。

9.6.CLOSE_WAIT 状态
在这里插入图片描述
比如主机2是服务器,主机1是客户端。

9.7.如果在服务器上出现大量的 CLOSE_WAIT 状态,如何解决?
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题。

猜你喜欢

转载自blog.csdn.net/yanghangwww/article/details/106456584