tcp全名是传输控制协议,tcp协议在ip协议基础上增加了数据包完整性检查等保证传输完整性的机制,使其在现在的数据领域得到了广泛的应用
按照下面的步骤可以快速了解tcp数据包中包含的信息
tcp协议rfc文档解读
rfc参考:tools.ietf.org/html/rfc793
核心tcp数据包结构如下
可以看到tcp报文由十余个字段组成,最后一个data字段代表了本次tcp数据报文承载的数据,这个数据一般是应用层的数据,比如http报文数据就是在这个tcp包的data字段中其中常用字段如下
字段 | 作用 |
---|---|
Source Port | 发包机器的端口号 |
Destination Port | 收包机器的端口号 |
Sequence Number | 包编号 |
Acknowledgment Number | 确认包号 |
urg/ack/psh/rst/syn/fin | 标志位,设置是/否的操作标志 |
Window | 流量控制窗口 |
Checksum | 包完整性校验 |
注意:客户端和服务端使用独立的包编号计数器器 checksum服务端和客户端会分别计算,客户端依靠这个值判断tcp包在传输过程中是否被异常改变/篡改
获取案例数据包
可以使用wireshark获取一个tcp数据包,在tcp层单击右键->复制->as a Hex Stream即可
这里得到的tcp层包数据如下
1f90f04f3747d146dcae23f3801831bf14ef00000101080a3176450b31764503
复制代码
下面就可以编写程序从这个tcp的16进制中解析出报文的具体数据了
解析tcp数据报文
二进制数字位数和16进制位数换算关系:1个16进制数可以表示4个二进制数 比如二进制:00011111 10010000 可以使用16进制表示为:1f90
可以使用下面的代码将16进制转成二进制字符串
func hex2bin(hex string) string {
var bin string
for i := 0; i < len(hex); i++ {
hex2int, _ := strconv.ParseInt(string(hex[i]), 16, 64)
bin = bin + fmt.Sprintf("%04b", hex2int)
}
return bin
}
复制代码
然后就可以按二进制位读取tcp数据报信息了,参考代码如下
func main() {
atcp := "1f90f04f3747d146dcae23f3801831bf14ef00000101080a3176450b31764503"
bintcp := hex2bin(atcp)
sourcePort, _ := strconv.ParseInt(bintcp[0:16], 2, 64)
fmt.Printf("sourcePort is %d \n", sourcePort)
destinationPort, _ := strconv.ParseInt(bintcp[16:32], 2, 64)
fmt.Printf("destinationPort is %d \n", destinationPort)
sequenceNumber, _ := strconv.ParseInt(bintcp[32:64], 2, 64)
fmt.Printf("sequenceNumber is %d \n", sequenceNumber)
acknowledgmentNumber, _ := strconv.ParseInt(bintcp[64:96], 2, 64)
fmt.Printf("acknowledgmentNumber is %d \n", acknowledgmentNumber)
dataOffset, _ := strconv.ParseInt(bintcp[96:100], 2, 64)
fmt.Printf("dataOffset is %d \n", dataOffset)
reserved, _ := strconv.ParseInt(bintcp[100:106], 2, 64)
fmt.Printf("reserved is %d \n", reserved)
// Control Bits 控制位,从106-1012共有6位,每位表示一个控制位的开关
urg, _ := strconv.ParseInt(bintcp[106:107], 2, 64)
ack, _ := strconv.ParseInt(bintcp[107:108], 2, 64)
psh, _ := strconv.ParseInt(bintcp[108:109], 2, 64)
rst, _ := strconv.ParseInt(bintcp[109:110], 2, 64)
syn, _ := strconv.ParseInt(bintcp[110:111], 2, 64)
fin, _ := strconv.ParseInt(bintcp[111:112], 2, 64)
fmt.Printf("控制位标识如下:\n")
fmt.Printf(" urg: %d\n", urg)
fmt.Printf(" ack: %d\n", ack)
fmt.Printf(" psh: %d\n", psh)
fmt.Printf(" rst: %d\n", rst)
fmt.Printf(" syn: %d\n", syn)
fmt.Printf(" fin: %d\n", fin)
// 数据窗口 16位
window, _ := strconv.ParseInt(bintcp[112:128], 2, 64)
fmt.Printf("window is %d \n", window)
// checksum 16位
checksum, _ := strconv.ParseInt(bintcp[128:144], 2, 64)
fmt.Printf("checksum is %d \n", checksum)
// urgentPointer
urgentPointer, _ := strconv.ParseInt(bintcp[144:160], 2, 64)
fmt.Printf("urgentPointer is %d \n", urgentPointer)
// options and padding
optionsAndPaddings := bintcp[160:]
fmt.Printf("optionsAndPaddings is %s \n", optionsAndPaddings)
fmt.Printf("tcp raw data is %s \n", atcp)
fmt.Printf("tcp bin data is %s \n", bintcp)
fmt.Printf("tcp bin data length is %d\n", len(bintcp))
}
复制代码
执行效果如下
wireshark解析结果如下
可以看到解析是ok的
一些注意的点
wireshark中复制的tcp包数据是16进制的,但是tcp协议中,部分字段仅占用一位,16进制是2进制的4的整数倍,直接解析16进制会导致tcp中的某些字段无法获取,需要先转成二进制进行处理
tcp包数据最终会进行32位对齐,整个tcp数据包大小如果不是正好是32位长度的整数倍,会用0在末尾填充到32位整数倍
tcp协议在rfc3168中新增了cwr和ece标志位,可以参考:tools.ietf.org/html/rfc316…
抓包可以使用tcpdump: tcpdump -n -XX -i lo0 -s0 'tcp port 8080'
参考资料
- github.com/google/pack…
- klamath.stanford.edu/~nickm/pape…
- pcapplusplus.github.io/docs/tutori…
- tools.ietf.org/html/rfc793…
- forums.ni.com/t5/LabVIEW/…
有疑问加站长微信联系(非本文作者)