本文旨在通过形象的例子和实操,把无形的、虚拟的网络转为具体的、可视化的。带领网络小白一步步的掌握 TCP 三次握手核心知识点,为后续深入学习 TCP 协议打基础。
通俗版如下图所示,小明(客户端)给小美(服务端)打电话,在经过互相询问和应答,确认通信畅通后,才开始愉快地聊天。(本例子不一定无懈可击主要是意会即可)
细节版一个 TCP 报文段分为首部和数据两部分,TCP 所有的功能都体现在首部的各个字段中。
序列号:本报文段所发送数据的第一个字节的序号,在建立连接时会随机生成初始序列号 ISN(Inital Sequence Number)。
确认号:下一次应该收到的数据的序列号,若确认号为 N,则代表到序列号 N-1 为止的数据都已经正确收到。
控制位:
-
ACK:为 1 时确认号才生效,在建立连接后所有传送的报文段 ACK 均为 1。
-
SYN:为 1 时表示这是连接请求(SYN=1,ACK=0)或者连接接受(SYN=1,ACK=1)报文。
由于上述报文都未携带数据,即 len=0,所以响应的 ack 等于请求的 seq+1。
实战版为了抓到比较纯净的包,我们实现一个简单的 TCP Server,接受客户端的请求并回复。
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: %s\n", 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: %s\n", err)
return
}
log.Printf("%s\n", string(buf))
// 写数据
if _, err := conn.Write([]byte("hello, xiao ming.\n")); err != nil {
log.Printf("conn write err: %s\n", err)
return
}
}
客户端请求:
echo -n "hello, xiao mei." | nc 81.68.197.93 8080
使用 wireshark 分析(统计 - 流量图)可以清楚的看到三次握手的过程。
同时也能看到 seq 和 ack 的关系,在握手成功后发送了 len=16 的包后,然后 ack=17 表示序列号在 16 之前的数据都已经正确收到。
注意:ISN 是一个随机数,并不为 0,wrieshark 为了显示更友好,使用了相对序号,在鼠标右键选项中取消即可看到真正的序列号,如:Seq=3503481500。
握手包源文件下载地址
状态机TCP 所谓的面向连接本质就是客户端和服务端的数据结构都各自维护了一个”连接状态“,三次握手期间状态变化如下:
LISTEN:服务端主动监听一个端口,等待客户端的连接请求
SYN-SENT:发送 SYN 包后的状态
SYN-RECEIVED:收到 SYN 包,且发送了 SYN+ACK 包后的状态
ESTABLISHED:连接建立成功
# 使用 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/IP(第5版)
计算机网络(第7版)谢希仁
酷壳 - TCP 的那些事儿(上)