引言:本文的目的是通过生动的例子和实际操作,将抽象的、虚拟的网络转化为具体的、可视化的形式。通过逐步掌握TCP三次握手的核心知识点,为深入学习TCP协议打下基础。
通俗版:如下图所示,小明(客户端)给小美(服务端)打电话,在经过互相询问和应答,确认通信畅通后,才开始愉快地聊天。(请注意,这个例子并不一定是完美的,主要是为了理解概念)
细节版:一个TCP报文段分为首部和数据两部分,TCP的所有功能都体现在首部的各个字段中。
序列号:本报文段所发送数据的第一个字节的序号,在建立连接时会随机生成初始序列号ISN(Inital Sequence Number)。
确认号:下一次应该收到的数据的序列号,若确认号为N,则代表到序列号N-1为止的数据都已经正确收到。
控制位:
func main() {
// 监听端口
l, err := net.Listen("tcp", "0.0.0.0:8080")
if err != nil {
panic(err)
}
defer l.Close()
for {
// 接受连接
conn, err := l.Accept()
if err != nil {
log.Printf("accept err: %sn", err)
continue
}
go hello(conn)
}
}
func hello(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
// 读数据
if _, err := conn.Read(buf); err != nil {
log.Printf("conn read err: %sn", err)
return
}
log.Printf("%sn", string(buf))
// 写数据
if _, err := conn.Write([]byte("hello, xiao ming.n")); err != nil {
log.Printf("conn write err: %sn", err)
return
}
}
echo -n "hello, xiao mei." | nc 81.68.197.93 8080
由于上述报文都未携带数据,即len=0,所以响应的ack等于请求的seq+1。
实战版:为了捕获比较纯净的数据包,我们实现了一个简单的TCP服务器,用于接收客户端的请求并进行回复。
客户端请求:
使用Wireshark进行分析(统计-流量图),可以清楚地看到三次握手的过程。同时也可以看到序列号(seq)和确认号(ack)之间的关系。在握手成功后,发送了len=16的数据包,然后ack=17表示序列号在16之前的数据都已经正确接收。
注意:ISN是一个随机数,并不为0。为了显示更友好,Wireshark使用了相对序号。取消相对序号显示后,可以看到真正的序列号,例如Seq=3503481500。
# 使用 netstat 命令查看当前系统的 TCP 连接状态
$ netstat -t
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 ubuntu:58824 39.156.66.18:http TIME_WAIT
tcp 0 0 localhost:20172 localhost:37276 ESTABLISHED
握手包源文件下载地址
状态机:TCP所谓的面向连接本质上是客户端和服务端各自维护的一个”连接状态”。在三次握手期间,状态的变化如下:
LISTEN:服务端主动监听一个端口,等待客户端的连接请求
SYN-SENT:发送SYN包后的状态
SYN-RECEIVED:收到SYN包,并发送了SYN+ACK包后的状态
ESTABLISHED:连接建立成功