在我的博客阅读本文
根据TCP/IP四层模型,在TCP场景下如下图:
其中,TCP头的信息:
要分析TCP
协议,尽管可以直接使用一个http
的访问进行抓包,但是http
的过程中又涉及DNS
,ARP
等过程混淆我们,为了纯粹的分析TCP
协议,我们这里使用Golang
写了一个简单的9830端口的TCP
服务器与客户端,源代码简单展示如下:
服务端:
package server
import (
"fmt"
"net"
"os"
"strings"
"test/util"
)
func StartTCPServer(c chan<- string) {
listener, err := net.Listen("tcp", "localhost:9830")
if err != nil {
util.HandleError(err)
os.Exit(1)
}
defer listener.Close()
c <- "ready"
// 等待连接
conn, err := listener.Accept()
if err != nil {
util.HandleError(err)
os.Exit(1)
}
// 处理连接
handleRequest(conn)
}
func handleRequest(conn net.Conn) {
defer conn.Close()
var res strings.Builder
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
break
}
res.Write(buf[:n])
}
fmt.Print("Received client message, size:", res.Len())
}
客户端:
package client
import (
"fmt"
"net"
"os"
"strings"
"test/util"
)
func StartTCPClient() {
conn, err := net.Dial("tcp", "localhost:9830")
if err != nil {
util.HandleError(err)
os.Exit(1)
}
defer conn.Close()
// 发送200kb消息
largeMessage := strings.Repeat("A", 200*1024)
_, err = conn.Write([]byte(largeMessage))
if err != nil {
util.HandleError(err)
os.Exit(1)
}
fmt.Println("Sent message to server!")
}
执行入口:
package main
import (
"fmt"
"sync"
c "test/internal/client"
s "test/internal/server"
)
// main wireshark filter express: tcp.port==9830
func main() {
var wg sync.WaitGroup
wg.Add(2)
serverReady := make(chan string, 1)
go func() {
s.StartTCPServer(serverReady)
wg.Done()
}()
go func() {
<-serverReady
c.StartTCPClient()
wg.Done()
}()
wg.Wait()
}
编译执行,控制台输出如下:
由于我们这里TCP
的客户端和服务端都是面向localhost
,使用adapter for loopback traffic capture
接口捕获回环流量,过滤器过滤tcp端口9830即可:
tcp.port==9830
需要注意,即使代码和我一样,最终抓包的数据依然会有差距,因为MTU
,window size
等参数会因为OS
的策略不同有差异,求同存异抓共性。
大体分为三部分:
TCP
协议中的“三次握手”,此时建立TCP
连接**TCP
协议中的“四次挥手”,此时释放TCP
连接**点开包号96,我们查看其Network层的数据情况:
Source Port
之类的字段比较直观易于理解,下面我们重点看一下一些不太直观的字段代表的数据含义。
网络对包的大小是存在限制的,这部分是在建立TCP连接的“三次握手”中进行声明,即:
MSS
(Maximum Segment Size,最大段大小)MTU
(Maximum Transmission Unit,最大传输单元):MSS
+ TCP
头长度 + IP
头长度即是MTU
在“三次握手中”,包号94中,客户端向服务端声明了MSS=65495
,可以计算MTU
(单位为Byte,一般Wireshark中出现的数字单位都是Byte):
MTU = 65495 + 20(TCP头) + 20(IP头) = 65535
同理,包号95中,服务端在给客户端的ACK
包中也声明了自己的MSS
,也可以计算出MTU
。
客户端和服务端各自有MTU,实际的传输包的大小是由客户端与服务端2个MTU
中较小的数值去决定的。
TCP
提供的是有序传输,每个数据段都要标上一个序号。
在实际场景中,TCP
传输数据时并不能严格保证有序,总会出现乱序的情况,需要根据序号进行排序。
Len
:length,数据长度Seq
:Sequence Number,序号,即上一个数据段Seq
+ Len
,发送方和接收方都需各自维护Seq状态Ack
:Acknowledge Number,确认号,待接受的数据序号,发送方和接收方都需各自维护Ack
状态以包号97~99为例:
Seq
=1,Len
=65495Seq
=1,Len
=0,同时计算Ack
=65496,即发送方的Seq
+ Len
Seq
=65496,Len
=65495,ACK
为1,即上一步中服务端的Seq
+ Len
可以推断出:
Seq
和Len
主要表达的是作为发送方当前的数据序号和长度Ack
主要表达的是作为接收方的接收到的数据序号和长度的和TCP
通信中,一个客户端/服务端既有发送的场景,也有接受的场景,因此Seq
,和Ack
都各自需要单独维护,Len
则是实际的数据长度。TCP
头包含一系列的标识位(也称作控制位或标志位),它们用于控制和管理TCP
连接的不同方面。每个标识位都占用TCP
头部中的1个比特。
Wireshark不仅帮我们“翻译”了当前封包的状态,也把TCP头的所有状态位都做了提示。
包号94~96行则为TCP
的“三次握手”行为,发生在TCP
连接建立之初:
“三次握手”主要分为3步:
Seq
Seq
,声明Ack
=客户端声明的初始Seq
+ 1Ack
=服务端初始Seq
+ 1包号104~107行则为TCP
的“四次挥手”行为,发生在TCP连接需要中断,释放连接时:
**“四次挥手”**主要分为4步,发起方可能是客户端,也可能是服务端:
Seq
和Ack
Ack
=第1.步中发起方的Seq
+ 1Seq
和Ack
(与上一步一样)Ack
=第3.步中接收方的Seq
+ 1TCP
客户端代码中发送了一个200kb的字符串给服务端,之前分析过我们的MTU
限制在65535b,不算TCP
头和IP
头的MSS
为65495b。我们实际无法在一个数据帧中发送全部数据,需要按照MTU拆分为多个数据帧进行发送。TCP
连接的一方连续发送的数据帧数是存在限制的,否则无限发送数据帧,另一方处理速率赶不上就会遇到Back Pressure(背压)
的情况,这个限制则是TCP Window(TCP窗口)
,即Wireshark
中的Win
的数值:MTU
/MSS
要求拆分数据帧Len
=65495的数据ACK
,调整了自己的TCP窗口
,声明Win
=2161152。这里如果窗口大小为零,发送方将停止发送数据,并等待下一个窗口更新。也就是说,窗口大小是动态调整的。ACK
,Seq
按序增加,由于服务端没有回复ACK
,这3帧数据的Ack都一样为1。ACK
,Ack
=204801,刚好是包号101的Seq
+ len
,这里实际上代表当前客户端已经收到了包号99~101的内容,这被称为TCP
的累计确认。窗口大小在TCP
协议设计之初只留了2byte,这个可以在Wireshark中看到:
后续为了提升Wireshark的长度,在TCP“三次握手”时的Options
中有Window scale
:
最终窗口大小计算公式 = Window的数值 8442 * 2的window scale次方
在上一部分大包拆分中,可以看到TCP窗口是会动态调整的,这是有一个具体的调整策略的。
TCP
连接过程中有可能出现丢包的情况。
对于发送方来说,如果发出去的包不像往常一样得到确认,有可能是网络延迟导致,发送方会等待一段时间再去判断,如果一直收不到,就会判定包已丢失,进行重传。这个过程称为超时重传,从发出原始包刀重传该包的时间段称为RTO
:
重传之后,TCP
窗口也会被调整,会先降到1个MSS
,之后会进入慢启动过程。不过这次从慢启动到拥塞的临界窗口值有了参考依据,RFC5681建议应该设置为拥塞时没被确认的数据量的1/2,不小于2个MSS
:
RTO的过程对性能影响是比较大的:
与RTO不同,如果拥塞很轻微,发生部分丢包,发送方还是能接收到部分Ack包,Ack会有期望Seq号,通过这个期望Seq号,发送方会意识到发生包丢失,当发送方收到3个及以上Dup Ack(重复确认)时会立刻重传,这个过程称为快速重传。
这里之所以需要凑齐3个及以上Dup Ack的原因是实际场景中包可能会乱序,Ack好像表现出一个缺失的包,但是这个包并没有丢失,而是由于乱序还在数据传输路上,可能很快就会传输过来,因此设置一个数量要求一定程度避免乱序导致快速重传:
快速重传的过程对性能影响是比较小的,因为依然有部分包能够被发送和接收到,说明拥塞并不严重,因此只需传慢一点即可,无需突然大幅度TCP窗口重新慢启动。RFC5681建议重新设置临时窗口为拥塞时没被确认数据的1/2,不小于2个MSS,拥塞窗口设置为临界窗口+3个MSS