基于抓包详解TCP协议
[toc]
# 前言
这篇文章原文内容是笔者大学时网络系统集成的笔记,由于最近开始复习计算机网络基础,为了体系化知识,故将此文整理成一篇详尽的TCP详解博文。
# OSI7层模型
在正式了解TCP协议之前,我们需要对网络的层次划分有一个大体的认识,所以我们不妨来复习一下OSI七层模型,OSI七层模型自底向上的顺序为:
- 物理层:通过物理媒介将比特流数据传输到另一个设备中。
- 数据链路层:负责将上层的数据封装成数据帧。
- 网络层:负责在不同的网络之间进行数据路由,确定源地址和目标地址,该层常见的协议为IP、ICMP、RIP、IGMP、OSPF。
- 传输层:负责将数据进行分段或者重组,并提供端到端之间的可靠或者不可靠传输,并且该层还支持对错误数据的检测和纠正,常见协议是TCP、UDP协议。
- 会话层:负责建立、维护、终止程序之间的会话,该层同样还负责数据的同步和恢复功能,常见协议为SSL、TLS、DAP等。
- 表示层:负责传输数据的编码和解码,以便不同程序可以理解传输的内容,所以层也支持数据的加密和解密确保数据的安全性。
- 应用层:应用层即为用户提供服务的一层,即为应用程序提供服务的一层,常见的协议有HTTP、FTP、DNS、SSH等。
# 实践-基于抓包了解WireShark的基本使用
# 安装WireShark
- 两台可互通的centos(这里笔者用192.168.0.127模拟客户端、192.168.0.128模拟服务端)、一个spring boot web项目、可进行curl、telnet、ping、tcpdump等命令
- 一个抓包工具wireshark 关于wireshark的安装使用,笔者建议参考这篇文章 网络分析工具——WireShark的使用(超详细) (opens new window)
# 快速抓包了解一下抓包的技巧
在开始之前,我们先了解一下需求,我们希望在服务端即可ip为192.168.0.128的服务器上开启tcpdump命令,抓取127客户端的tcp包。

首先我们可以键入如下命令查看客户端的ip以及网口名称
ifconfig
由此确认,笔者客户端所使用的ip为192.168.0.127网口名称为ens

然后使用tcpdump监听对应网口的ping 192.168.0.128的数据包 ,这里笔者就简单介绍一下下面命令的含义。 -i即指定抓包的网口,由于上文笔者使用ifconfig看到网口名为ens33,所以这里-i所指定的端口就是ens33。 后续跟着一个icmp就是icmp协议,使用and拼接后续的条件这里指明了笔者目标服务器的ip 192.168.0.128。 然后我们再加一个-nn即可表示我们抓到的包不解析为主机名,显示ip和port即可。 最后一个-w ping.pcap意思为将抓到的数据包写到ping.pcap文件中
tcpdump -i ens33 icmp and host 192.168.0.128 -nn -w ping.pcap
2
关于tcpdump更多的命令参数选项如下所示:
| 选项 | 示例 | 说明 |
|---|---|---|
| host、src host、dst host | tcpdump -nn host 192.168.0.100 | 过滤出对应主机地址的数据包 |
| port、src port、dst port | tcpdump -nn port 80 | 过滤出对应端口的数据包 |
| ip、ip6、arp、tcp、udp、icmp | tcpdump -nn udp | 过滤出对应协议的数据包 |
| and、or、not | tcpdump -nn host 192.168.0.100 and port 80 | 将多个条件组合起来的参数 |
完成上述抓包监听之后,我们使用客户端再开一个终端,ping目标服务器3次
ping 192.168.0.128 -I ens33 -c 3
2
ping结束后,我们的将128服务器中的ping.pcap文件导到windows系统的wireshark。打开后可以看到客户端端发一个echo request,服务端就会回应一个echo reply。

我们不妨双击一个请求查看他的数据包的详细内容,可以看到wireshark的强大,他将物理层到网络层的所有信息都捕获到了,关于报文的具体内容笔者都已详尽注释,读者可以自行参考阅读。

然后我们不妨看看网络包中每一层的具体内容,先看看数据链路层,他指明了上层数据的类型,以及源mac地址和服务器(192.168.0.128)的mac地址

再来看看网络层抓到的内容

最后看看icmp协议,可以看到报文内容很简单,就是一个简单的echo request

总结一下,上文的数据分层结构如下图所示,每一层都会在数据包中添加一个头。

# 抓到一个HTTP协议的数据包
该实验是为了了解客户端和服务端进行HTTP协议交互时,网络数据包的交互流程。所以我们可以准备一个web进行实验。 因为笔者是Java开发,所以 准备一个spring boot的web应用部署到服务器后台运行,这里笔者从spring官网生成了一个jdk8版本的springboot应用,将端口设置为80端口,编写一个测试的web请求方法,完成后打包到服务器上。

这里我们就编写了一个简单的http请求方法

在服务器上测试接口过程如下。
curl 127.0.0.1/shark/hello
2
可以看到响应了一个hello world的字符串,我们终于可以抓包干活了

然后我们就用ip为127的客户端开个tcpdump监听本机关于192.168.0.128的http请求
tcpdump -i any and host 192.168.0.128 and port 80 -w http.pcap
2
完成上述命令后,我们客户端在开一个中断curl一下服务器(192.168.0.128)
curl 192.168.0.128/shark/hello
最终客户端在tcpdump的中断按住CTRL C终止监听,并将抓到的数据包的pcap文件导入wireshark中,于是我们就得到下图所示的文件。

我们可以看到正常的三次握手过程以外,还看到了http交互过程,在建立tcp连接后:
- 客户端会向服务端发送一个http请求,即一个get的请求。
- 服务端发一个tcp的ack数据包表示确认收到,再将一个text类型的数据发给客户端
- 客户端会给服务端一个ack确认本次会话就结束了。
在4次挥手时,我们看到一个很奇怪的现象,看着我们抓包的情况来看,tcp断开连接好像3次交互就够了,难道书本上教的内容是错误的嘛? 并不是,就以本次连接为例,由于http请求的数据不大,在断开连接的时候,服务端并没有要再传给客户端的数据,所以将在服务端向客户端发送确认报文ack时,捎带了fin数据包。这就造成了断开连接只要3次的情况。
当然,要是觉得这样看不够直观,我们完完全全可以使用wireshark自带的统计流程图看看tcp交互过程 如下图所示,点击流量图

再将流类型改为tcp flows即可,是不是很直观呢?

这样3次握手、建立连接、4次挥手的过程就很直观了。

# 理论篇-关于TCP的一些常见问题解答
# TCP如何加急数据包
将PSH(push)置为1,即将数据直接向上层传输,这一点我们可以在HTTP请求的数据包的传输层看到这个标志位。

# SYN-SEND这个标志位的含义
SYN-SEND意味同步发送,因为要求同步,所以我们要在建立连接时,将同步位syn为同步位设置为1。
# PSH和URG两个标志位的区别是什么
URG标志位表示紧急数据指针有效。在TCP协议中,有些数据可能需要优先传输,比如某些控制信息或者紧急数据。如果发送方需要发送这些紧急数据,就可以设置URG标志位,并指定紧急数据的位置。当接收方接收到URG标志位并识别出紧急数据指针时,就会立即处理这些紧急数据,以保证其能够及时被处理。

PSH标志位表示发送方要求接收方立即将缓冲区中的数据发送出去。在传输数据时,通常会先将数据存储到发送方的缓冲区中,等到缓冲区填满或者超时后再发送出去。但是,如果发送方使用了PSH标志位,就表示它希望接收方立即将缓冲区中的数据发送出去,而不需要等到缓冲区填满或者超时。这样可以减少延迟,提高数据传输效率。

因此我们可以理解为PUSH用于提高数据传输效率,而URG 则是为了加速数据处理。
# 为什么TCP需要进行3次握手?
由于tcp要实现全双工通信,所以要求双方都具备收发功能,这就要求第一次建立请求之后,双方都要各自进行一次收发。 这些内容我们完完全全可以在上文的抓包中得到印证。
# 随机序号 ISN是什么?
三次握手的协商参数
# TCP报文中的序列号序列号如何排序
切分后的报文段的第一个字节字节号作为数据包的序列号。
# 什么是后退N协议
为保证tcp可靠传输的一个方案,其特点就是累计确认,例如 接受方受到连续按序的五个报文时候发送确认消息6,就代表前五个数据包都收到了,期待第六个数据包。

# 选择重发协议是什么
哪个分组损坏或者没收到就要求发送方重发哪个分组。
# 流水线传输是什么
每次批量发送数据包,由于与批量传输,所以适用于通信状况比较稳定的场所。但数据包不可达时,选用后退n或者选择重发进行数据重传。
# 捎带确认是什么
接收方接收后回复ack时顺带一些需要发送给发送方的数据。
# TCP如何做到流量控制
滑动窗口,在tcp建立连接时,接收方会高速发送方他的接收窗口的大小,发送方的发送窗口就会根据接收方所能接受的窗口动态的调整窗口大小。 工作原理即接收方发送确认包时,发送方动态向右滑动窗口继续发送新数据,且该窗口始终保持接收方的当前所能接受的窗口大小。

# TCP拥塞控制是什么意思
其核心就是用慢启动算法,拥塞窗口根据网络拥塞程度动态变化,一旦网络拥塞就减小拥塞窗口,以减少发送到网络中的报文数量。 这其中有个关键字 MSS(最大报文段大小),取值一般取1460(tcp头和ip头各占20字节,加起来1500刚好是以太网标准的mtu)
# 流量控制和拥塞控制的区别
流量控制是收发两端的问题,即点对点问题,通过抑制发送方发送数据的大小进行控制。这其中就涉及到一个重要的概念-缓存,如下图所示:通信两端在每道工序中货扮演生产者或扮演消费者的角色,发送方按照接收方所能容纳的大小将数据放入缓存,缓存满了进入下一工序,并提醒下部工序的角色做好准备,正是这样以套紧密联接的分工,才保证的流量控制。

而拥塞控制是全局性问题,无法做到完全解决,只能说在收发两端使用慢启动和拥塞避免的手段尽可能的优化。
# TCP的三次握手交互流程是什么样的
TCP建立3次握手时需要考虑两种情况,当传输的数据长度为0时,交互过程如下:
- 建立连接者a发送一个同步号序列为1的报文。序列号为x。
- 接收方b收到报文后,生成确认位,值为a发送的序列号x+1,同样生成一个序列号y。
- a收到报文后,同样生成一个确认位值为b的序列号y+1,序列号为上一次发送的序列号x+1.

再来说说第二种情况,假如发送的数据长度不为0,每次接收的序列号则都是基于对方的序列号+长度+1:

# 四次挥手
# 为什么结束通信需要四次挥手?
因为通信是双向的,所以需要经过双方都确认才可,而结束是单方面要求,必须经由双方各自全双工通道都断开连接才可。
# 四次挥手图解
四次挥手如下图所示:
- 发起结束方a发起结束,数据包中结束为FIN设置为1,序列号为u。
- 结束方b收到后发起确认,却认为ACK为1,ack值为对端序列号u+1,并生成自己数据包的序列号v,开始将剩余数据发送完成。
- 结束方b完成所有数据后,通知a可以关闭连接了,于是发送的报文FIN为1和ACK也为1,代表确认建议结束了,ack的值为a的序列号u+1,生成本地会话序列号为w。
- a收到确认结束后,发送收到通知,所以报文ACK为1,序列号基于自己第一次发送的数据包u+1,ack的序列号则是b发送的序列号w+1。

# TCP的日常故障排查
# TCP报文中的控制位出现RST怎么办?
可能是对方tcp服务关闭,确认一下对方的TCP服务程序是否关闭。
# 出现Retransmission
可以推测大概是以下3种原因:
- 接受方没有收到。
- 接受方收到,但确认报文丢失。
- 接收方收到有问题的报文。
# 出现大量SYN同步报文
如果这种情况伴随着我方发送同步报文后无响应,大概率存在Ddos攻击(分布式拒绝式服务攻击)
# tcp相关例题
# 发送方连发 1000 2000 3000 4000 5000 6000的报文,接收方回复ack序列号为4000,若为gbn需发送那些报文?若为sr则又要发送那些报文?
答:gbn回4000则是要求发送4000代表前4000的都收到了,4000字节段的报文可能出现问题,要求对方发送4000之后的报文。 而sr则是代表3000段收到了要求发4000,其他报文段没有回复确认同样需要发送。即sr回复4000需要发送1000 2000 4000 5000 6000
# a向b发送数据,序列号分别是70 110。那么报文字节数是多少?b收到第一个报文段的确认号是多少? 第2次收到报文后回复180,问第二个报文段长度是多少?a发送的第一个报文丢失,用累计确认的方式回复的确认好为多少?
答:报文字节数:110-70=40 由公式可知b收到第一个报文后的确认号为:70+40+1=111 第二个报文长度为180-110=70 用累计确认的方式,则可以认为第2个起报文都没收到,直接回复70。
# 拥塞问题为何超时重传无法解决?
答:网络拥塞后,将超时的包再次重传只会使得原本阻塞的网络环境变得更加糟糕,进而导致网络崩坏。
# 3个抓包实验巩固TCP基础
# 实验1(假如TCP刚刚建立连接的syn包就丢了)
# 模拟步骤
# 客户端开个tcpdump监听请求
tcpdump -i any tcp and host 192.168.0.128 and port 80 -w synClose.pcap
2
# 将服务器的网络断开
这步无需解释,笔者直接在虚拟机中将网络关掉,或者直接关机
# 客户端curl一下服务端地址
如下所示,为了更直观的看到时间,笔者在命令前后加了date
date;curl 192.168.0.128/shark/hello;date
2
# 取出抓到的包分析
从抓出来的数据来看,由于我们将服务器的网络断了,导致客户端的syn请求没有收到服务端的ack,所以客户端以为服务端没有收到,就重试了5次。

# 从操作系统内核配置查看原因
为什么是5次呢?我们可以通过操作系统内核的配置中一探究竟
键入以下命令
cat /proc/sys/net/ipv4/tcp_syn_retries
输出
5
这就是为什么一旦syn没有收到对端的ack,就会超时重试的次数
如果读者不相信这个结果可以改变结果,不妨修改这个值
echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
2
可以看到经过笔者的修改后,TCP建立连接丢包,就会重试两次。

还有一点读者需要留心,就是重试等待时间RTO,如下图所示,可以看出每次重试的时间都会基于上一次的等待时间乘2

# 实验2(假如服务端ack包丢失)
# 模拟步骤
# 开启服务端所有服务
这里只是做个补充,由于笔者上一个实验将服务器网络断开了,这里要再次将其连接回来
# 客户端开个tcpdump监听http请求并rejectServer.pcap
tcpdump -i any tcp and host 192.168.0.128 and port 80 -w rejectServer.pcap
# 客户端将iptables对于服务ip的ack包drop掉
iptables -I INPUT -s 192.168.0.128 -j DROP
# 使用curl请求服务端地址
date;curl 192.168.0.128/shark/hello;date
2
# 查看抓包结果
可以看到下面这样一张图,是不是很不直观,无妨,我们使用tcp flows查看一下结果

首先我们看看客户端,除去第一次syn的数据包以外,由于我们把服务端的确认包拒绝了,导致客户端以为他发的syn数据包没有被收到,遂重试了5次,可以看到RTO也是基于上次等待时间乘2,符合我们上个实验所得结论

可以看出,服务端在第一次发syn+ack(同步+确认收到)包之后,每次客户端发syn包都会回复一个syn+ack(同步+确认收到)包,在下图用红色箭头所指。
一旦超时没有被收到就会再发一次,下图用蓝色箭头所指。如此往复5次,最终确认无法和对方建立连接。直接放弃建立连接。
需要补充一点,可能会有读者疑问,为什么iptables明明drop这个数据包,为什么tcpdump还能看到,如下所示,数据包流向iptabls之前会经过tcpdump,所以对于input的数据tcpdump可以看到,而出去的就看不到了
进来的顺序 Wire -> NIC -> tcpdump -> netfilter/iptables
出去的顺序 iptables -> tcpdump -> NIC -> Wire

# 从操作系统内核了解原因
键入以下命令,我们可以得到数字5,由此得出上文中syn+ack包重试次数为5,同样的,如果读者不信,可以将这个值修改一下抓包看看结果,这里就不做演示了
cat /proc/sys/net/ipv4/tcp_synack_retries
# 实验3(假如服务端没收到客户端的ack)
# 操作步骤
# 将原本客户端的iptables禁止的规则改为accept
由于上一个实验将规则drop了,这里需要改为accept
iptables -I INPUT -s 192.168.0.128 -j ACCEPT
# 设置服务端drop客户端的ack确认包
命令如下所示意为将客户端的确认包丢掉
iptables -I INPUT -s 192.168.0.127 -p tcp --tcp-flag ACK ACK -j DROP
2
# 客户设置tcpdump监听http请求
date;curl 192.168.0.128/shark/hello;date
2
# 抓包查看结果
同样看到密密麻麻的一排,我们将其使用统计图的形式来分析结果

首先看看服务端的数据包,可以看到由于ack包被drop了,服务端认为自己的syn+ack没有被收到,遂按照上文tcp_synack_retries重试了5次,发现失败,遂不了了之

所以我们在服务端键入如下指令
netstat -napt |grep 192.168.0.127
输出
tcp 0 0 192.168.0.128:80 192.168.0.127:58696 SYN_RECV
我们都知道客户端向服务端发送syn包之后,客户端会进入SYN_SEND,等待服务器确认包
当服务端收到syn包后,立刻回一个syn+ack,此时服务端的状态就是SYN_RECV 意为收到客户端的syn包,发送一个ack等待客户端确认,也就是上文的状态。
再来客户端的数据包可以看到笔者矩形所圈的部分就是客户端发送ack后,认为已经和服务端建立连接了此时状态为established。这一点我们可以按照如下指令即可看到结果
netstat -napt |grep 192.168.0.128
遂发送了一个http请求,结果没收到结果,发送了一个psh+ack再找服务端要一次,还是没要到,这时候客户端就认为这条连接有问题了。 所以后面15箭头所示的,超时重传请求15次去确认这条tcp连接是否可用。

# 从系统内核查看原因
这个15我们可以键入以下命令查看
cat /proc/sys/net/ipv4/tcp_retries2
2
需要补充的是tcp_retries2意思为在丢弃激活(已建立通讯状况)的TCP连接之前﹐需要进行多少次重试。默认值为15
# 小结
碍于本文篇幅原因,很多读者可能对文章某些内容云里雾里,以下便列出笔者认为阅读本文所需要补充的知识
# 更多的内核参数
tcp_syn_retries等参数详解 (opens new window)
# 关于iptables
Linux 防火墙教程:IPTables 表、链、规则基础 (opens new window)
# 关闭进程相关
Linux下如何通过一行命令查找并杀掉进程 (opens new window)
# 参考文献
SYN_RECV的意思 (opens new window)