1

前言:

KCP是一个可靠的协议,它的延迟率比起tcp低很多。在游戏和视频推流领域广泛使用。
在TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。而 KCP是为流速设计的(单个数据包从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。
文章中内容解析了整个kcp的传输数据数据包组成。

系统: mac OS 10.14.3
golang: go1.12 darwin/amd64
抓包工具:tcpdump

kcp协议介绍

KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。

(一)rpcx的kcp实现源码

代码可以在github中获取:
https://github.com/rpcx-ecosystem/rpcx-examples3/tree/master/kcp

client端:

package main

import (
   "context"
   "crypto/sha1"
   "flag"
   "fmt"
   "log"
   "net"
   "time"

   example "github.com/rpcx-ecosystem/rpcx-examples3"
   "github.com/smallnest/rpcx/client"
   kcp "github.com/xtaci/kcp-go"
   "golang.org/x/crypto/pbkdf2"
)

var (
   addr = flag.String("addr", "localhost:8972", "server address")
)

const cryptKey = "rpcx-key"
const cryptSalt = "rpcx-salt"

func main() {
   flag.Parse()

   pass := pbkdf2.Key([]byte(cryptKey), []byte(cryptSalt), 4096, 32, sha1.New)
   bc, _ := kcp.NewAESBlockCrypt(pass)
   option := client.DefaultOption
   option.Block = bc

   d := client.NewPeer2PeerDiscovery("kcp@"+*addr, "")
   xclient := client.NewXClient("Arith", client.Failtry, client.RoundRobin, d, option)
   defer xclient.Close()

   // plugin
   cs := &ConfigUDPSession{}
   pc := client.NewPluginContainer()
   pc.Add(cs)
   xclient.SetPlugins(pc)

   args := &example.Args{
      A: 10,
      B: 20,
   }

   start := time.Now()
   //for i := 0; i < 10000; i++ {
   // reply := &example.Reply{}
   // err := xclient.Call(context.Background(), "Mul", args, reply)
   // if err != nil {
   //    log.Fatalf("failed to call: %v", err)
   // }
   // //log.Printf("%d * %d = %d", args.A, args.B, reply.C)
   //}
   reply := &example.Reply{}
   err := xclient.Call(context.Background(), "Mul", args, reply)
   if err != nil {
      log.Fatalf("failed to call: %v", err)
   }
   log.Printf("%d * %d = %d", args.A, args.B, reply.C)
   dur := time.Since(start)
   qps := 10000 * 1000 / int(dur/time.Millisecond)
   fmt.Printf("qps: %d call/s", qps)
}

type ConfigUDPSession struct{}

func (p *ConfigUDPSession) ConnCreated(conn net.Conn) (net.Conn, error) {
   session, ok := conn.(*kcp.UDPSession)
   if !ok {
      return conn, nil
   }

   session.SetACKNoDelay(true)
   session.SetStreamMode(true)
   return conn, nil
}

server端:

package main

import (
   "crypto/sha1"
   "flag"
   "net"

   example "github.com/rpcx-ecosystem/rpcx-examples3"
   "github.com/smallnest/rpcx/server"
   kcp "github.com/xtaci/kcp-go"
   "golang.org/x/crypto/pbkdf2"
)

var (
   addr = flag.String("addr", "localhost:8972", "server address")
)

const cryptKey = "rpcx-key"
const cryptSalt = "rpcx-salt"

func main() {
   flag.Parse()

   pass := pbkdf2.Key([]byte(cryptKey), []byte(cryptSalt), 4096, 32, sha1.New)
   bc, err := kcp.NewAESBlockCrypt(pass)
   if err != nil {
      panic(err)
   }

   s := server.NewServer(server.WithBlockCrypt(bc))
   s.RegisterName("Arith", new(example.Arith), "")

   cs := &ConfigUDPSession{}
   s.Plugins.Add(cs)

   err = s.Serve("kcp", *addr)
   if err != nil {
      panic(err)
   }
}

type ConfigUDPSession struct{}

func (p *ConfigUDPSession) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
   session, ok := conn.(*kcp.UDPSession)
   if !ok {
      return conn, true
   }

   session.SetACKNoDelay(true)
   session.SetStreamMode(true)
   return conn, true
}

client端和server端使用了AESBlockCrypt加密,密钥对应const cryptKey = "rpcx-key" 和 const cryptSalt = "rpcx-salt"。
kcp支持:Salsa20、sm4、Twofish、Triple_DES、CAST-128、Blowfish、AES、TEA、XTEA等加密方式。

(二)抓包

还是抓回环链路,捕获8972端口数据

tcpdump -i lo0 port 8972 -S -XX

这一块不会抓包的可以看看《【协议分析】rpcx网络协议分析之kcp》 那篇文章。
或者去官网学习下:
www.us.tcpdump.org/manpages/tcpdump.1.html

数据包数据如下:

22:00:28.410752 IP localhost.63488 > localhost.8972: UDP, length 115
    0x0000:  0200 0000 4500 008f 3b55 0000 4011 0000  ....E...;U..@...
    0x0010:  7f00 0001 7f00 0001 f800 230c 007b fe8e  ..........#..{..
    0x0020:  dd9f 8ab0 5984 b839 5bd3 6dbe c94f ac6a  ....Y..9[.m..O.j
    0x0030:  8429 1745 cd66 7630 47d7 289d d743 84f7  .).E.fv0G.(..C..
    0x0040:  9726 b1f2 d45a 5092 b27f 1135 2b12 4ad9  .&...ZP....5+.J.
    0x0050:  cf0b 49e2 f611 3a7a cf06 f328 3ca6 42fc  ..I...:z...(<.B.
    0x0060:  b9f2 1930 0dfd 5269 5ef7 b439 2d1e 9a92  ...0..Ri^..9-...
    0x0070:  64db 2eaf 95a6 95dc 3af3 eb46 5294 252e  d.......:..FR.%.
    0x0080:  1d3c e49c a8f9 e6d1 0bd0 cc58 fe71 6aa5  .<.........X.qj.
    0x0090:  24b7 e2                                  $..
22:00:28.411010 IP localhost.8972 > localhost.63488: UDP, length 52
    0x0000:  0200 0000 4500 0050 3e9c 0000 4011 0000  ....E..P>...@...
    0x0010:  7f00 0001 7f00 0001 230c f800 003c fe4f  ........#....<.O
    0x0020:  52fb 130d 850f 90d8 1b04 7a7e 855b e4b1  R.........z~.[..
    0x0030:  4173 c282 a462 f211 6aaf 18a2 92e5 5c4e  As...b..j.....\N
    0x0040:  a738 c934 c707 71ba c72f 19e1 4caf 56ec  .8.4..q../..L.V.
    0x0050:  5b1f e7d4                                [...
22:00:28.411180 IP localhost.8972 > localhost.63488: UDP, length 104
    0x0000:  0200 0000 4500 0084 0e44 0000 4011 0000  ....E....D..@...
    0x0010:  7f00 0001 7f00 0001 230c f800 0070 fe83  ........#....p..
    0x0020:  a99c 6f57 4549 66bb c8b9 5f72 07c7 d697  ..oWEIf..._r....
    0x0030:  3656 afd3 2eed 21cd 69b1 321b 85cf 3840  6V....!.i.2...8@
    0x0040:  2ec9 9aea 8e3c 4cbe bd2d ed4f b357 2106  .....<L..-.O.W!.
    0x0050:  0528 e99f b29d 0e85 3a3d a4e9 b398 e8a2  .(......:=......
    0x0060:  18ac 06a1 2672 87e5 72a8 0e54 4055 0980  ....&r..r..T@U..
    0x0070:  e3de cc33 788e 41ad e8b7 2289 75da 5e07  ...3x.A...".u.^.
    0x0080:  276e 329a 71fa 4f0a                      'n2.q.O.
22:00:28.411261 IP localhost.63488 > localhost.8972: UDP, length 52
    0x0000:  0200 0000 4500 0050 06a8 0000 4011 0000  ....E..P....@...
    0x0010:  7f00 0001 7f00 0001 f800 230c 003c fe4f  ..........#..<.O
    0x0020:  8408 27a6 f032 f202 f717 9f88 8e8e c915  ..'..2..........
    0x0030:  f1c5 12c7 0e57 5152 c471 de8c 5a5a f388  .....WQR.q..ZZ..
    0x0040:  9391 d2d6 5f9f 95e8 7518 e02e a8f1 7536  ...._...u.....u6
    0x0050:  74e4 370b                                t.7.

(三)解析UDP协议包与KCP协议包

3.1 UDP数据解析

解析数据包内容

22:00:28.410752 IP localhost.63488 > localhost.8972: UDP, length 115
    0x0000:  0200 0000 4500 008f 3b55 0000 4011 0000  ....E...;U..@...
    0x0010:  7f00 0001 7f00 0001 f800 230c 007b fe8e  ..........#..{..
    0x0020:  dd9f 8ab0 5984 b839 5bd3 6dbe c94f ac6a  ....Y..9[.m..O.j
    0x0030:  8429 1745 cd66 7630 47d7 289d d743 84f7  .).E.fv0G.(..C..
    0x0040:  9726 b1f2 d45a 5092 b27f 1135 2b12 4ad9  .&...ZP....5+.J.
    0x0050:  cf0b 49e2 f611 3a7a cf06 f328 3ca6 42fc  ..I...:z...(<.B.
    0x0060:  b9f2 1930 0dfd 5269 5ef7 b439 2d1e 9a92  ...0..Ri^..9-...
    0x0070:  64db 2eaf 95a6 95dc 3af3 eb46 5294 252e  d.......:..FR.%.
    0x0080:  1d3c e49c a8f9 e6d1 0bd0 cc58 fe71 6aa5  .<.........X.qj.
    0x0090:  24b7 e2                                  $..

网络传输中的字节码都是大端读取的,这个是为了兼容CPU架构。所以在编程中需要转换网络字节码,其实就是小端转成大端。
就像我们的02 00 。
KCPUDP.jpg.png

/UDP头定义,共8个字节/

typedef struct _UDP_HEADER 
{
 unsigned short m_usSourPort;       // 源端口号16bit
 unsigned short m_usDestPort;       // 目的端口号16bit
 unsigned short m_usLength;        // 数据包长度16bit
 unsigned short m_usCheckSum;      // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;

3.2 KCP数据解析

根据上面分析我们可以得知,我们的数据部分应该是从第33个字节开始。原因: 4字节(回环链路层) + 20字节(IP层) + 8字节(UDP层) = 32字节。
刚好是如下部分:

    0x0020:  dd9f 8ab0 5984 b839 5bd3 6dbe c94f ac6a  ....Y..9[.m..O.j
    0x0030:  8429 1745 cd66 7630 47d7 289d d743 84f7  .).E.fv0G.(..C..
    0x0040:  9726 b1f2 d45a 5092 b27f 1135 2b12 4ad9  .&...ZP....5+.J.
    0x0050:  cf0b 49e2 f611 3a7a cf06 f328 3ca6 42fc  ..I...:z...(<.B.
    0x0060:  b9f2 1930 0dfd 5269 5ef7 b439 2d1e 9a92  ...0..Ri^..9-...
    0x0070:  64db 2eaf 95a6 95dc 3af3 eb46 5294 252e  d.......:..FR.%.
    0x0080:  1d3c e49c a8f9 e6d1 0bd0 cc58 fe71 6aa5  .<.........X.qj.
    0x0090:  24b7 e2                                  $..dd9f 8ab0 5984 b839 5bd3 6dbe c94f ac6a 

因为我们是针对与传输数据时使用了AESBlockCrypt加密,所以我们可以编写一段go代码对数据包部分做一下解密工作。
代码如下:

package main

import (
   "crypto/sha1"
   "fmt"
   "github.com/xtaci/kcp-go"
   "golang.org/x/crypto/pbkdf2"
)
const cryptKey = "rpcx-key"
const cryptSalt = "rpcx-salt"
func main() {
   pass := pbkdf2.Key([]byte(cryptKey), []byte(cryptSalt), 4096, 32, sha1.New) //加密令牌
   bc, err := kcp.NewAESBlockCrypt(pass)
   if err != nil {
      fmt.Println(err)
      return
   }

   //数据包
   var dst []byte = []byte{
         0xdd, 0x9f, 0x8a, 0xb0, 0x59, 0x84, 0xb8, 0x39, 0x5b, 0xd3, 0x6d, 0xbe, 0xc9, 0x4f, 0xac, 0x6a,
         0x84, 0x29, 0x17, 0x45, 0xcd, 0x66, 0x76, 0x30, 0x47, 0xd7, 0x28, 0x9d, 0xd7, 0x43, 0x84, 0xf7,
         0x97, 0x26, 0xb1, 0xf2, 0xd4, 0x5a, 0x50, 0x92, 0xb2, 0x7f, 0x11, 0x35, 0x2b, 0x12, 0x4a, 0xd9,
         0xcf, 0x0b, 0x49, 0xe2, 0xf6, 0x11, 0x3a, 0x7a, 0xcf, 0x06, 0xf3, 0x28, 0x3c, 0xa6, 0x42, 0xfc,
         0xb9, 0xf2, 0x19, 0x30, 0x0d, 0xfd, 0x52, 0x69, 0x5e, 0xf7, 0xb4, 0x39, 0x2d, 0x1e, 0x9a, 0x92,
         0x64, 0xdb, 0x2e, 0xaf, 0x95, 0xa6, 0x95, 0xdc, 0x3a, 0xf3, 0xeb, 0x46, 0x52, 0x94, 0x25, 0x2e,
         0x1d, 0x3c, 0xe4, 0x9c, 0xa8, 0xf9, 0xe6, 0xd1, 0x0b, 0xd0, 0xcc, 0x58, 0xfe, 0x71, 0x6a, 0xa5,
         0x24, 0xb7, 0xe2}


   dec := make([]byte, len(dst))
   fmt.Println("加密数据:")
   for i := 0;i < len(dst) ;i++  {
      fmt.Printf("%02x ", dst[i])
      if (i + 1) % 16 == 0{
         fmt.Printf("\n")
      }
   }
   
   bc.Decrypt(dec,dst) //解密
   
   fmt.Println("\n")
   fmt.Println("解密数据:")
   for i := 0;i < len(dec) ;i++  {
      fmt.Printf("%02x ", dec[i])
      if (i + 1) % 16 == 0{
         fmt.Printf("\n")
      }
   }
   fmt.Println("\n")
}

输入数据如下:

加密数据:
dd 9f 8a b0 59 84 b8 39 5b d3 6d be c9 4f ac 6a 
84 29 17 45 cd 66 76 30 47 d7 28 9d d7 43 84 f7 
97 26 b1 f2 d4 5a 50 92 b2 7f 11 35 2b 12 4a d9 
cf 0b 49 e2 f6 11 3a 7a cf 06 f3 28 3c a6 42 fc 
b9 f2 19 30 0d fd 52 69 5e f7 b4 39 2d 1e 9a 92 
64 db 2e af 95 a6 95 dc 3a f3 eb 46 52 94 25 2e 
1d 3c e4 9c a8 f9 e6 d1 0b d0 cc 58 fe 71 6a a5 
24 b7 e2 

解密数据:
ac d0 ee 1a 3d 48 23 6c 74 27 e2 a4 73 34 ef 78 
b9 f2 0f 28 00 00 00 00 f1 00 59 00 dd 36 c5 20 
51 00 20 00 0a 00 00 00 00 00 00 00 00 00 00 00 
3f 00 00 00 08 00 00 30 00 00 00 00 00 00 00 00 
00 00 00 2f 00 00 00 05 41 72 69 74 68 00 00 00 
03 4d 75 6c 00 00 00 00 00 00 00 17 82 a1 41 d3 
00 00 00 00 00 00 00 0a a1 42 d3 00 00 00 00 00 
00 00 14 

KCP数据包组成如下:
RPCXKCP协议数据包.jpg.png
图中UDP数据包,则是上面解密程序的解密数据字节。

kcp协议部分。segment 结构体:

type segment struct {
conv uint32 
// 发送端与接收端通信时的匹配数字,发送端发送的数据包中此值与接收端的conv值匹配一致时,接收端才会接受此包

cmd uint8 
// 改数据包的协议号,协议号有以下枚举:
// IKCP_CMD_PUSH = 81 // cmd: push data,数据包
// IKCP_CMD_ACK = 82 // cmd: ack,确认包,告诉对方收到数据包
// IKCP_CMD_WASK = 83 // cmd: window probe (ask),询问远端滑动窗口的大小
// IKCP_CMD_WINS = 84 // cmd: window size (tell),告知远端滑动窗口的大小

frg uint8
// 分帧号,由于udp传输有数据包大小的限制,因此,应用层一个数据包可能被分为多个udp包

制自己接下来发送数据的大小wnd uint16
// 滑动窗口的大小
// 当Segment做为发送数据时,此wnd为本机滑动窗口大小,用于告诉远端自己窗口剩余多少
// 当Segment做为接收到数据时,此wnd为远端滑动窗口大小,本机知道了远端窗口剩余多少后,可以控

ts uint32
// timestamp , 当前Segment发送时的时间戳

sn uint32
// Sequence Number,Segment数据包的编号

una uint32
// una即unacknowledged,未确认数据包的编号,表示此编号前的所有包都已收到了。

rto uint32
// rto即Retransmission TimeOut,即超时重传时间,在发送出去时根据之前的网络情况进行设置

xmit uint32
// 基本类似于Segment发送的次数,每发送一次会自加一。用于统计该Segment被重传了几次,用于参考,进行调节

resendts uint32
// 即resend timestamp , 指定重发的时间戳,当当前时间超过这个时间时,则再重发一次这个包。

fastack uint32   
// 用于以数据驱动的快速重传机制;
// len uint32 c++版本有数据包的数据长度,go版本无此字段

data []byte // 协议数据的具体内容
}

kcp.go,如下为segment协议包组成源码如下:

func (seg *segment) encode(ptr []byte) []byte {
   ptr = ikcp_encode32u(ptr, seg.conv) //发送端与接收端通信时的匹配数字,进行加密
   ptr = ikcp_encode8u(ptr, seg.cmd)   //数据包的协议号,进行加密
   ptr = ikcp_encode8u(ptr, seg.frg)   //分帧号,进行加密
   ptr = ikcp_encode16u(ptr, seg.wnd)  //数据包窗口大小,进行加密
   ptr = ikcp_encode32u(ptr, seg.ts)   //当前Segment发送时的时间戳,进行加密
   ptr = ikcp_encode32u(ptr, seg.sn)   //Segment数据包的编号,进行加密
   ptr = ikcp_encode32u(ptr, seg.una)  //未确认数据包的编号,进行加密
   ptr = ikcp_encode32u(ptr, uint32(len(seg.data))) //数据长度,进行加密
   atomic.AddUint64(&DefaultSnmp.OutSegs, 1)
   return ptr
}

代码中的的ikcp_encode32u其实是调用了PutUint32进行加密。

sess.go,数据包输入函数:
func (l *Listener) packetInput(data []byte, addr net.Addr) {
   dataValid := false
   if l.block != nil { //判断是否加密
      l.block.Decrypt(data, data) //解密数据包
      data = data[nonceSize:]     //切片16字节,这部分对应的就是图中的nonce。
      checksum := crc32.ChecksumIEEE(data[crcSize:])  //获得crc checksum,data原始地址 + 16的4个字节
      if checksum == binary.LittleEndian.Uint32(data) {
         data = data[crcSize:]   //切片4字节,data原始地址 + 16 + 4
         dataValid = true
      } else {
         atomic.AddUint64(&DefaultSnmp.InCsumErrors, 1)
      }
   } else if l.block == nil {
      dataValid = true
   }

   if dataValid {
      l.sessionLock.Lock()
      s, ok := l.sessions[addr.String()]
      l.sessionLock.Unlock()

      if !ok { // new address:port
         if len(l.chAccepts) < cap(l.chAccepts) { // do not let the new sessions overwhelm accept queue
            var conv uint32
            convValid := false
            if l.fecDecoder != nil {  //判断开启FEC
               isfec := binary.LittleEndian.Uint16(data[4:]) //获取2字节FEC type,data原始地址 + 16 + 4 + 4
               if isfec == typeData { //判断是否为Plus2,标志位位0xf1
                  conv = binary.LittleEndian.Uint32(data[fecHeaderSizePlus2:])  //获取conv ,data原始地址 + 16 + 4 + 8
                  convValid = true
               }
            } else {
               conv = binary.LittleEndian.Uint32(data) //没有开启FEC时直接,获取conv = data原始地址 + 4
               convValid = true
            }
            //...省略
      } else {
         s.kcpInput(data)
      }
   }
}

根据以上代码我们可以看到获得数据时起时做了一个解密过程Uint32。

KCP数据包组成.jpg.png
图中为获取conv计算规则

总结:

1.kcp支持加密方式:Salsa20、sm4、Twofish、Triple_DES、CAST-128、Blowfish、AES、TEA、XTEA等加密方式。
2.kcp传输数据是通过udp协议。
3.kcp协议部分的segment采用了PutUint和Uint32进行加解密。

参考资料:

tcpdump命令详解:
www.us.tcpdump.org/manpages/tcpdump.1.html

kcp
[https://github.com/skywind300...](https://github.com/skywind300...
)

AESBlockCrypt
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

Salsa20
https://en.wikipedia.org/wiki/Salsa20

sm4
https://github.com/tjfoc/gmsm/tree/master/sm4

Twofish
https://en.wikipedia.org/wiki/Twofish

Triple_DES
https://en.wikipedia.org/wiki/Triple_DES

Blowfish
https://en.wikipedia.org/wiki/Blowfish_(cipher))

TEA
https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm

XTEA
https://en.wikipedia.org/wiki/XTEA


c_rain
23 声望8 粉丝