浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用

  • 前言
  • 浅谈 OSI
  • TCP
    • 三次握手
    • 数据传输
    • 四次挥手
    • Socket 服务端/客户端通信测试
      • 服务端代码
      • 客户端代码
      • tcpdump 命令监控
      • 命令总结
    • FAQ
      • 怎么确认数据包的大小?
      • TCP 拥塞如何避免?
      • 如何理解 TCP keep-alive 原理?
  • 总结

前言

在网络知识体系,TCP 这块的三次握手、四次挥手是必备的,在面试中,也是老生常谈了,以下通过抓包工具抓取整个握手、挥手交互的过程进行分析.

浅谈 OSI

OSI(Open System Interconnect)缩写,意为开放式地系统互联,七层参考模型,利用了软件工程学的概念,是为了分层解耦
浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用_第1张图片

  1. 应用层:与用户打交道的那一层,通过 interface 接口交互的,比如浏览器、Tomcat,应用层出现的协议有 HTTP、FTP、SSH等等
  2. 表示层:协议、语义,是否符合协议规定由表示层来处理
  3. 会话层:服务器验证用户登录,session->保持会话的动作由会话层来完成
  4. 传输控制层:如何建立连接、如何传输,是成功还是失败的一个控制,传输层出现的协议有 TCP/UDP,TCP 是面向连接、可靠的传输协议,UDP 恰恰相反
  5. 网络层:由网络层来处理设备之间是如何路由,是如何找到的
  6. 链路层:点与点之间的通信 、点与点之间是什么样的通信协议,具体能发出什么
  7. 物理层:WIFI、光纤等设备

TCP

在这里主要介绍在 TCP 协议中服务端与客户端三次握手、四次挥手是如何完成的一个过程,TCP 是在传输控制层中进行交互的

服务端/客户端它们都会各自去维护自己的序列号:seq

三次握手

浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用_第2张图片

  1. 客户端调用 connect() 指令,发送 SYN-建立连接标识、seq 起始序列号给到服务端
  2. 服务端会有 listent() 指令监听来自客户端的链接,收到 SYN 标识后,发送服务端的 seq 起始序列号、ACK 确认号「客户端起始序列号+1」给到客户端,客户端应答后即可建立
  3. 客户端建立了连接以后,回复 ack 确认号「服务端起始序列号+1」给服务端,服务端也建立好了

以上是握手的交互流程,同时也说明会产生一些资源的消耗/开辟:线程、对象、文件描述符、调用等等操作

到这里,就会问两次握手可以去完成握手吗?

不可能是两次,若为两次的话;客户端先发起,服务端回复了,客户端后续就不管了做其他的事情去了,而服务端还在傻傻的等着回应,同时资源也还在占用,这及其不安全也消耗资源的

数据传输

浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用_第3张图片
在数据传输交互中过程中

  1. 客户端通过 write 指令去写入数据,客户端会携带:序列号+1 值、ack 确认号到服务端
  2. 服务端通过 read 指令读取数据,将数据进行处理后,会发送 ack 确认号到 客户端
  3. 这样,数据传输就能在客户端/服务端之间完成

四次挥手

浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用_第4张图片

  1. 客户端调用 close() 指令,携带 FIN-断开连接标识、序列号+2「之前 +1 的值用过了」、ack 确认号,发起断开连接请求
  2. 服务端确认后,回复确认号序列号 +3
  3. 服务端调用 close() 指令,携带 FIN-断开连接标识、系列号+1,发起断开连接请求
  4. 客户端确认后,回复确认号系列号 +2

到这里,就会问为什么要发生四次挥手,两次不可以吗?

因为 TCP 连接是双向的,因此在四次挥手中前两次是用于断开一方的连接,而后两次是用于断开另外一方的连接;同时,考虑到 socket 问题,端口号数量是有限的{socket:套接字通信,端口数量最多 65535 个},用完了必须要记得回收、关闭资源,保证端口释放以便给其他的服务进行使用;

Socket 服务端/客户端通信测试

通过 Socket 通信,采用 TCP 协议来进行服务端、客户端之间的交互演示

服务端代码

import java.io.*;
import java.net.*;
public class SocketIoServer {
    // server socket listen property:
    private static final int RECEIVE_BUFFER = 10;
    private static final int SO_TIMEOUT = 0;
    private static final boolean REUSE_ADDR = false;
    // 备胎可以有两个:后台最多可以有多少个待处理的连接
    private static final int BACK_LOG = 2;
    // client socket listen property on server endpoint:服务端客户端之间维护着心跳,互相确认自己还活着
    private static final boolean CLI_KEEPALIVE = false;
    private static final boolean CLI_OOB = false;
    private static final int CLI_REC_BUF = 20;
    private static final boolean CLI_REUSE_ADDR = false;
    private static final int CLI_SEND_BUF = 20;
    private static final boolean CLI_LINGER = true;
    private static final int CLI_LINGER_N = 0;
    private static final int CLI_TIMEOUT = 0;
    // 关闭 Nagle 算法:不组合小分组的数据,而是每次都立即发送出去
    // 开启 Nagle 算法:组合小分组的数据,以一个分组的方式发送出去,降低了吞吐量
    private static final boolean CLI_NO_DELAY = false;
		/*
    StandardSocketOptions.TCP_NODELAY
    StandardSocketOptions.SO_KEEPALIVE
    StandardSocketOptions.SO_LINGER
    StandardSocketOptions.SO_RCVBUF
    StandardSocketOptions.SO_SNDBUF
    StandardSocketOptions.SO_REUSEADDR
 		*/
    public static void main(String[] args) {

        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9090), BACK_LOG);
            server.setReceiveBufferSize(RECEIVE_BUFFER);
            server.setReuseAddress(REUSE_ADDR);
            server.setSoTimeout(SO_TIMEOUT);
            System.out.println("server up use 9090!");
            while (true) {
                // System.in.read();  //分水岭:
                Socket client = server.accept();  // 阻塞的,一直卡着不动,内核指令:accept(4,
                System.out.println("client port: " + client.getPort());
                client.setKeepAlive(CLI_KEEPALIVE);
                client.setOOBInline(CLI_OOB);
                client.setReceiveBufferSize(CLI_REC_BUF);
                client.setReuseAddress(CLI_REUSE_ADDR);
                client.setSendBufferSize(CLI_SEND_BUF);
                client.setSoLinger(CLI_LINGER, CLI_LINGER_N);
                client.setSoTimeout(CLI_TIMEOUT);
                client.setTcpNoDelay(CLI_NO_DELAY);
                Thread thread = new Thread(
                        () -> {
                            try {
                                InputStream in = client.getInputStream();
                                BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                                char[] data = new char[1024];
                                while (true) {
                                    int num = reader.read(data);
                                    if (num > 0) {
                                        System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
                                    } else if (num == 0) {
                                        System.out.println("client readed nothing!");
                                        continue;
                                    } else {
                                        System.out.println("client readed -1...");
                                        System.in.read();
                                        client.close();
                                        break;
                                    }
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                );
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

客户端代码

import java.io.*;
import java.net.Socket;
public class SocketClient {
    public static void main(String[] args) {
        try {
        	// 172.16.249.12:虚拟机中 ifconfig 网卡中的外网 IP
            Socket client = new Socket("172.16.249.12",9090);
            client.setSendBufferSize(20);
            client.setTcpNoDelay(true);
            OutputStream out = client.getOutputStream();
            InputStream in = System.in;
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            while(true){
                String line = reader.readLine();
                if(line != null ){
                    byte[] bb = line.getBytes();
                    for (byte b : bb) {
                        out.write(b);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

tcpdump 命令监控

在这里会介绍一些常用的 Linux 命令以及如何在 Linux 使用 tcpdump

浅谈 TCP 握手/数据传输/挥手过程以及 tcpdump 抓包工具使用_第5张图片
通过 ifconfig 查看虚拟机中的网卡,ens160 基于外网交互的,lo 基于虚拟机内网交互的,由于服务端、客户端代码都会在这台虚拟机节点上进行编译,所以这里 会使用 lo 这个网卡去 dump 它们之间的 TCP 交互过程,使用 ens160 网卡去 dump 在控制台上是不会输出内容的!

1、启动 socket 服务端,编译后运行

[root@172 ~]# javac SocketIoServer.java && java SocketIoServer
server up use 9090!

2、开启一个新的窗口,查看 socket/tcp 网络信息:netstat -natp,会发生多了下面这条 listen 条目信息

[root@172 ~]# netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name           
tcp6       0      0 :::9090                 :::*                    LISTEN      3236/java 

3、查询服务端进程下文件描述符信息:lsof -op pid,此时 offset 偏移量为 0

[root@172 ~]# lsof -op 3236
COMMAND  PID USER   FD   TYPE             DEVICE     OFFSET     NODE NAME
java    3236 root    5u  IPv6              28833        0t0      TCP *:websm (LISTEN)

4、新开启一个窗口,打开 tcp 抓包的程序:tcpdump -i lo -nn port 9090

  • i:网络配置的接口,通过 ifconfig 命令拿到的
  • nn:显示 IP、端口号
  • v:显示更多详细信息

5、新开启一个窗口,客户端连接到服务端,按回车键

[root@172 ~]# javac SocketClient.java  && java SocketClient

此时,tcpdump 窗口就会显示出很多的信息出来,如下:

16:14:35.969838 IP 172.16.249.12.48028 > 172.16.249.12.9090: Flags [S], seq 823509243, win 65495, options [mss 65495,sackOK,TS val 1014857448 ecr 0,nop,wscale 7], length 0
48028 客户端发送 Flags:SYN,序列号:823509243,给到 9090 服务端
16:14:35.969852 IP 172.16.249.12.9090 > 172.16.249.12.48028: Flags [S.], seq 3254927829, ack 823509244, win 1152, options [mss 65495,sackOK,TS val 1014857448 ecr 1014857448,nop,wscale 0], length 0
9090 服务端发送 Flags:SYN,序列号:3254927829,ack=客户端序列号+1值,给到 48028 客户端
16:14:35.969859 IP 172.16.249.12.48028 > 172.16.249.12.9090: Flags [.], ack 1, win 512, options [nop,nop,TS val 1014857448 ecr 1014857448], length 0
48028 客户端发送 ack=1,双方已互相确认,此次连接建立完成

6、客户端发送数据:111,在服务端配置时,为客户端设置了参数 CLI_NO_DELAY{不组合小分组的数据},会有多条信息出来,如下所示:

16:21:29.986816 IP 172.16.249.12.48028 > 172.16.249.12.9090: Flags [P.], seq 1:2, ack 1, win 512, options [nop,nop,TS val 1015271464 ecr 1014857448], length 1
客户端 48028 发送 Flags:PSH{数据传输标识}、序列号:1:2,代表的意思就是发出去的是 1,期望收到的是 2,给到 9090 服务端
16:21:29.987257 IP 172.16.249.12.9090 > 172.16.249.12.48028: Flags [.], ack 2, win 1151, options [nop,nop,TS val 1015271465 ecr 1015271464], length 0
服务端 9090 发送 ack=客户端序列号+1,也就是客户端期望收到的值
16:21:29.987428 IP 172.16.249.12.48028 > 172.16.249.12.9090: Flags [P.], seq 2:3, ack 1, win 512, options [nop,nop,TS val 1015271465 ecr 1015271465], length 1
16:21:29.987555 IP 172.16.249.12.48028 > 172.16.249.12.9090: Flags [P.], seq 3:4, ack 1, win 512, options [nop,nop,TS val 1015271465 ecr 1015271465], length 1
客户端 48028 发送 Flags:PSH{数据传输标识}、序列号:2:3、3:4,组合两条数据一起发出去「受到了客户端 SEND_BUFFER_SIZE 参数值」,给到 9090 服务端
16:21:29.989499 IP 172.16.249.12.9090 > 172.16.249.12.48028: Flags [.], ack 4, win 1151, options [nop,nop,TS val 1015271467 ecr 1015271465], length 0
服务端 9090 发送 ack=客户端最新的序列号+1,也就是客户端期望收到的值

6、到这里,TCP 三次握手、数据传输的过程通过 dump 就搞定了,还剩下四次挥手的过程!
由于在虚拟机内网 lo 网卡中监测不到挥手是四次的过程,所以采用外网网卡 ens160 这里也不懂这是什么原因导致的?
所以在这里我模拟请求百度首页:www.baidu.com,tcpdump 80 端口,来演示四次挥手的过程

  • 在一个窗口命令:tcpdump -i ens160 -nn port 80,监控
  • 另外一个窗口操作命令:curl www.baidu.com 80,等这个请求处理完成以后,当前窗口强制退出断开,在第一个窗口尾部就会出现以下的信息

16:42:30.946631 IP 172.16.249.12.44270 > 163.177.151.109.80: Flags [F.], seq 447954833, ack 1899774463, win 62780, length 0
客户端 44270 发送 Flags:FIN(关闭连接标识) 、序列号、ack 确认号请求,给到百度服务端
16:42:30.948011 IP 163.177.151.109.80 > 172.16.249.12.44270: Flags [.], ack 1, win 64239, length 0
百度服务端确认,发送确认号给客户端 44270
16:42:30.962906 IP 163.177.151.109.80 > 172.16.249.12.44270: Flags [FP.], seq 1, ack 1, win 64239, length 0
百度服务端 发送 Flags:FIN(关闭连接标识) |PSH(数据传输)、序列号、确认号给客户端 44270
16:42:30.963000 IP 172.16.249.12.44270 > 163.177.151.109.80: Flags [.], ack 2, win 62780, length 0
客户端 44270 确认,发送确认号给百度服务端,此次通信完毕

命令总结

  1. yum install -y tcpdump:安装 tcpdump,⼀个命令⾏的⽹络流量分析⼯具,功能⾮常强⼤,⼀般我们⽤来抓 TCP 包
  2. lsof -p:进程中的文件描述符信息,比如:偏移量、文件类型、状态
  3. netstat -natp:socket 网络的信息,listener 条目信息
  4. tcpdump -i ens160 -nn port 9090:监听某端口下的 tcp 交互

TCP 序列号详解:在同一个 TCP 链接过程窗口中,seq 序列号值是持续递增的;SYN 建立链接、FIN 关闭链接,都会消耗 seq,凡是涉及到对端确认的,一定需要消耗 TCP 报文的序列号

FAQ

怎么确认数据包的大小?

最大传输单元: MTU,通过 ifconfig 查看,此时的大小是携带上了 ip、port 的

ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500

通过 tcpdump 观察数据传输过程的报文会有如下信息:

options [mss 1460,sackOK,TS val 2603013609 ecr 0,nop,wscale 7]

MSS:最大报文段长度-1460,由 MTU=1500,得出 ip、port 占有 40 字节

TCP 拥塞如何避免?

在网络交互过程中,当服务端的窗体没有容量了,在与客户端握手的过程,通过确认包会告知服务端没有余量了,客户端因此会将自己阻塞住,不再发送数据给到服务端了,等服务端空余了,再补充一个包告知客户端可以继续发了

如何理解 TCP keep-alive 原理?

在一个 TCP 连接上,若通信双方都不再向对方发送数据,那么 TCP 连接就不会有任何数据交换了;假设应用程序是一个 WEB 服务器,客户端发出三次握手以后故障宕机或被剔除网线,对于 WEB 服务器而言,下一个数据包将永远无法到来,但它却一无所知

TCP 协议设计者,考虑到了这种检测长时间死连接的需求,于是乎设计了 keep-alive 机制,它的作用就是探测对端的连接有没有失效,通过定时发送探测包来探测连接的对端是否存活,不过默认情况下需要 7200 s 没有数据包交互才会发送这个探测包,往往这个时间太久了,我们熟知的很多组件都没开启 keep-alive 特性,而是选择在应用层做心跳机制

通过 sysctl -a | grep keepalive 命令可查看 keep-alive 内核参数配置


# 探测包检测频率,多长一次,默认 75 s
net.ipv4.tcp_keepalive_intvl = 75
# 探测包次数,默认 9 次
net.ipv4.tcp_keepalive_probes = 9
# 没有数据交互后多长时间去探测
net.ipv4.tcp_keepalive_time = 7200

总结

该文章浅谈了 TCP 三次握手、数据传输、四次挥手的交互过程,通过 tcpdump 抓包工具对这三个流程进行了信息的输出以及介绍,文末整理了一些操作 TCP 常用的命令以及一些常见的问题,当然,对于整个 TCP 可靠性协议,这些只是皮毛一角了,后续会细究出更多内容进行输出!!

点一波关注不迷路,你的支持是对我最大的鼓励

你可能感兴趣的:(Linux,tcp/ip,tcpdump,网络)