我如果想要知道一个字节从一个终端经由TCP传输到另一个终端所耗费的时间,即 TCP 的传输延迟(Latency),应该怎么办?
经过查阅相关信息,发现了两种方案:
TCPing(绝大部分答案,但真的如此吗?)
通过手动构造 TCP SYN 包来测量延迟(github.com/grahamking/latency )
然后还有我的灵光一现…
关于 TCPing 尝试搜索了一下 TCPing 的原理,却发现没有一个人从传输层的角度 对此进行说明,我甚至得不到我想要的答案。
于是在 GitHub 上搜了一下关键字 tcping
,star 最多的项目是 github.com/cloverstd/tcping ,很巧是基于 Go 语言实现的,立刻 clone 下来查看源码,却发现乏善可陈…
cloverstd/tcping 入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var rootCmd = cobra.Command{ Run: func (cmd *cobra.Command, args []string ) { host := args[0 ] parseHost := ping.FormatIP(host) target := ping.Target{ Host: parseHost, } switch protocol { case ping.TCP: pinger = ping.NewTCPing() } pinger.SetTarget(&target) pingerDone := pinger.Start() }, }
1 2 3 4 5 6 7 8 9 func (tcping TCPing) Start() <-chan struct {} { go func () { duration, remoteAddr, err := tcping.ping() }() return tcping.done }
TCPing 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (tcping TCPing) ping() (time.Duration, net.Addr, error ) { var remoteAddr net.Addr duration, errIfce := timeIt(func () interface {} { conn, err := net.DialTimeout("tcp" , fmt.Sprintf("%s:%d" , tcping.target.Host, tcping.target.Port), tcping.target.Timeout) if err != nil { return err } remoteAddr = conn.RemoteAddr() conn.Close() return nil }) return time.Duration(duration), remoteAddr, nil }
可以看到,原理:计时开始 –> 建立连接并马上关闭连接 –> 计时结束
zhengxiaowai/tcping 前面一个比较离谱,再看一个吧:zhengxiaowai/tcping ,Python 实现的 TCPing,star 数第二名
入口 1 2 if __name__ == '__main__' : cli()
main
函数进来直接到 cli
1 2 3 4 5 6 7 @click.command() @click.argument('host' ) def cli (host, port, count, timeout, report ): ping = Ping(host, port, timeout) try : ping.ping(count)
TCPing 实现 可以看到就是建立连接到关闭连接,调用了 connect
1 2 3 4 5 6 def ping (self, count=10 ): cost_time = self.timer.cost( (s.connect, s.shutdown), ((self._host, self._port), None )) self.statistics(n)
connect 再看看这个 connect
,唔姆,如出一辙,直接传域名
1 2 def connect (self, host, port=80 ): self._s.connect((host, int (port)))
i3h/tcping 再看一个比较靠谱的,i3h/tcping ,同样是 Go 实现,入口部分就不再贴了。这位老哥其实没有实现 TCPing,实现的是 HTTPing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 trace := &httptrace.ClientTrace{ DNSStart: func (info httptrace.DNSStartInfo) { t0 = time.Now().UnixNano() }, DNSDone: func (info httptrace.DNSDoneInfo) { t1 = time.Now().UnixNano() if info.Err != nil { err = info.Err log.Fatal(info.Err) } }, ConnectStart: func (net, addr string ) { }, ConnectDone: func (net, addr string , err error ) { if err != nil { log.Fatalf("unable to connect to host %v: %v" , addr, err) } t2 = time.Now().UnixNano() }, GotConn: func (info httptrace.GotConnInfo) { t3 = time.Now().UnixNano() }, GotFirstResponseByte: func () { t4 = time.Now().UnixNano() }, TLSHandshakeStart: func () { t5 = time.Now().UnixNano() }, TLSHandshakeDone: func (_ tls.ConnectionState, _ error ) { t6 = time.Now().UnixNano() }, } if t0 == 0 { t0 = t2 t1 = t2 } stats.DNS = t1 - t0 stats.TCP = t2 - t1 stats.Process = t4 - t3 stats.Transfer = t7 - t4 stats.TLS = t6 - t5 stats.Total = t7 - t0
以上便是 TCPing 的实现情况,然而 TCP 的握手再关闭应该是多个 RTT 吧?
按照标准的三次握手四次挥手来算,连接然后立刻关闭应该是 3 个 RTT,但是也不能简单的除 3,因为有时候挥手的 FIN & ACK
是合并发送的,或者有更加复杂的情况…
TCPing 的过程包含多个 RTT,并不是我想知道的“从一端到另一端的延迟”。
那么除了 TCPing,是否有更科学的方法?
手动构造 TCP SYN 包 GitHub 搜索 TCP Latency
,我们可以发现这个尘封的 Go Project(上一次 Commit 是 2016 年)
grahamking/latency
latency
sends a TCP SYN packet (the opening of the three-way handshake) to a remote host on port 80. That host will respond with either a RST (if the port is closed), or a SYN/ACK (if the port is open). Either way, we time how long it takes between sending the SYN and receiving the response. That’s your network latency.
根据 README
,我们手动构造一个 SYN
包发送出去,然后监听收到的SYN & ACK
,即TCP三次握手中的前两次,这样便是实实在在的一个 RTT,跟描述一样,这个项目真的手撸了一个 TCP 打包…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 const ( FIN = 1 SYN = 2 RST = 4 PSH = 8 ACK = 16 URG = 32 ) type TCPHeader struct { Source uint16 Destination uint16 SeqNum uint32 AckNum uint32 DataOffset uint8 Reserved uint8 ECN uint8 Ctrl uint8 Window uint16 Checksum uint16 Urgent uint16 Options []TCPOption } func NewTCPHeader (data []byte ) *TCPHeader { var tcp TCPHeader r := bytes.NewReader(data) binary.Read(r, binary.BigEndian, &tcp.Source) binary.Read(r, binary.BigEndian, &tcp.Destination) binary.Read(r, binary.BigEndian, &tcp.SeqNum) binary.Read(r, binary.BigEndian, &tcp.AckNum) var mix uint16 binary.Read(r, binary.BigEndian, &mix) tcp.DataOffset = byte (mix >> 12 ) tcp.Reserved = byte (mix >> 9 & 7 ) tcp.ECN = byte (mix >> 6 & 7 ) tcp.Ctrl = byte (mix & 0x3f ) binary.Read(r, binary.BigEndian, &tcp.Window) binary.Read(r, binary.BigEndian, &tcp.Checksum) binary.Read(r, binary.BigEndian, &tcp.Urgent) return &tcp } func Csum (data []byte , srcip, dstip [4]byte ) uint16 { pseudoHeader := []byte { srcip[0 ], srcip[1 ], srcip[2 ], srcip[3 ], dstip[0 ], dstip[1 ], dstip[2 ], dstip[3 ], 0 , 6 , 0 , byte (len (data)), } sumThis := make ([]byte , 0 , len (pseudoHeader)+len (data)) sumThis = append (sumThis, pseudoHeader...) sumThis = append (sumThis, data...) lenSumThis := len (sumThis) var nextWord uint16 var sum uint32 for i := 0 ; i+1 < lenSumThis; i += 2 { nextWord = uint16 (sumThis[i])<<8 | uint16 (sumThis[i+1 ]) sum += uint32 (nextWord) } if lenSumThis%2 != 0 { sum += uint32 (sumThis[len (sumThis)-1 ]) } sum = (sum >> 16 ) + (sum & 0xffff ) sum = sum + (sum >> 16 ) return uint16 (^sum) } func (tcp *TCPHeader) Marshal() []byte { buf := new (bytes.Buffer) binary.Write(buf, binary.BigEndian, tcp.Source) binary.Write(buf, binary.BigEndian, tcp.Destination) binary.Write(buf, binary.BigEndian, tcp.SeqNum) binary.Write(buf, binary.BigEndian, tcp.AckNum) var mix uint16 mix = uint16 (tcp.DataOffset)<<12 | uint16 (tcp.Reserved)<<9 | uint16 (tcp.ECN)<<6 | uint16 (tcp.Ctrl) binary.Write(buf, binary.BigEndian, mix) binary.Write(buf, binary.BigEndian, tcp.Window) binary.Write(buf, binary.BigEndian, tcp.Checksum) binary.Write(buf, binary.BigEndian, tcp.Urgent) for _, option := range tcp.Options { binary.Write(buf, binary.BigEndian, option.Kind) if option.Length > 1 { binary.Write(buf, binary.BigEndian, option.Length) binary.Write(buf, binary.BigEndian, option.Data) } } out := buf.Bytes() pad := 20 - len (out) for i := 0 ; i < pad; i++ { out = append (out, 0 ) } return out }
这就是我心目中的 TCP 延迟测试的答案了,标准的一个 RTT 的时间
以上项目都有一个特点:通用
他们都是在只能控制一端的情况下对 TCP 的延迟进行测量
随后我就想到一种简单的算法,在控制两端的情况下测量一个 RTT 的传输延迟
一个奇思妙想 有两个前提:
TCP 两端由我来编程控制
网络在一个较小时间段(可以认为是一个 RTT 内)内没有较大的波动
TCP 包结构设计为每个包的首 8 个字节为客户端打包时的时间戳,由此便可以利用该时间戳计算从客户端打包到服务端接收所耗费的时间。
一个简单的思路是直接用服务端的当前时间减去包首部的时间,即可近似认为是传输过程中所消耗的延迟。但是这个思路忽视了一个常见的问题:客户端和服务端的时间往往是不同步的。
不同语言获取 timestamp
的具体实现可能不同,但是可以假定是调用了系统的 date
命令,那么可以认为在同一操作系统上的两个程序,他们的时间是同步的。而问题就出在这个“同一操作系统”:客户端与服务端通常情况下都不会在同一台机器上,更别提操作系统了。
那么既然大部分操作系统都是 ntp
同步过的时间,几毫秒的误差确实也在接受范围内,这个问题就不再深究了?如果两端操作系统的时间差距在几秒,几分钟呢?这正是本设计较真的地方——如何计算出准确的 TCP 延迟。
与通用场景不同,我注意到在本项目中客户端和服务端都是由我控制的,也就是说我可以同时在两边构造 payload
供计算延迟使用——那么,我就想到了一个简单且有效的算法:通过校时使得客户端的时间与服务端保持一致,校时需要一个 RTT,证明如下:
假设服务端有四个时间节点,分别为 $t_0,t_1,t_2,t_3$,其中 $t_0<t_1<t_2<t_3$,客户端与服务端存在可为任意实数的时差 $\delta$,对任意时间节点,满足: $$ t_{客户端}=t+\delta $$ 假设 $t_0$ 时刻服务端发送包含有 $t_0$ 的数据包到客户端,$t_1$ 时刻客户端接收到包,实际延迟为 $t_1-t_0$,客户端记录下: $$ d=t_{客1}-t_0=t_1+\delta-t_0 $$ 假设 $t_2$ 时刻客户端发送包含有 $t_{客2}-d$ 的数据包到客户端,$t_3$ 时刻服务端接收到包,实际延迟为 $t_3-t_2$,服务端记录下: $$ total=t_3-(t_{客2}-d)=t_3-((t_2+\delta)-(t_1+\delta-t_0)) $$
$$ total=t_3-(t_2-t_1+t_0) $$
$$ total=(t_3-t_2)+(t_1-t_0) $$
即服务端最终所得值 $total$ 为一次发包和一次收包延迟的和,与两服务器间的时差无关,与客户端收包到发包之间间隔的时间无关。
使用该算法,将客户端和服务端的时差调整至 1 小时,经过测试,计算得出的延迟仍为正常值。
下面是该算法的一个 PoC,用 Go 实现,代码来自 github.com/bipy/streamera
服务端发送校时包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func sendTimeStamp (conn net.Conn) { writer := bufio.NewWriter(conn) for { time.Sleep(time.Millisecond << 7 ) timePkt := make ([]byte , common.TimeStampPacketSize) binary.LittleEndian.PutUint64(timePkt, uint64 (time.Now().UnixMicro())) _, err := writer.Write(timePkt) if err != nil { return } err = writer.Flush() if err != nil { return } } }
客户端收到包并校时 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func handleReceive (client *Client) { reader := bufio.NewReader(client.TCPConn) timeStamp := make ([]byte , common.TimeStampPacketSize) for { _, err := io.ReadFull(reader, timeStamp) if err != nil { fmt.Println(common.Red(client.TCPConn.RemoteAddr().String() + " Down! " + err.Error())) break } curTime := time.Now().UnixMicro() recvTime := int64 (binary.LittleEndian.Uint64(timeStamp)) client.TimeDiff = curTime - recvTime } } func pack (frame []byte , td int64 ) []byte { timePkt := make ([]byte , common.TimeStampPacketSize) binary.LittleEndian.PutUint64(timePkt, uint64 (time.Now().UnixMicro() - td)) contentLengthPkt := make ([]byte , common.ContentLengthPacketSize) binary.LittleEndian.PutUint64(contentLengthPkt, uint64 (len (frame))) var pkt []byte pkt = append (pkt, timePkt...) pkt = append (pkt, contentLengthPkt...) pkt = append (pkt, frame...) return pkt }
服务端收到包并更新延迟 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func handleConn (server *Server, conn net.Conn) { for { server.Counter.Mutex.Lock() server.Counter.LatencyRealTime = (curTime - recvTime) >> 1 server.Counter.Mutex.Unlock() } }