前段时间写了一个局域网音视频通话的程序,使用开源 KCP 来实现可靠UDP传输。
通过研究发现KCP在发包时,会在数据包前面加上它自己的头。如果数据包较小,KCP可能会把多个数据包合成一个包发送,提高效率。
如下图所示。
kcp udp 包结构
28 bytes 4 bytes 4 bytes len1 28 bytes 4 bytes 4 bytes len2
├────────────┼────────┬────────┼────────┼────────────┼────────┬────────┼────────┤
│kcp header │ size1 │msg type│msg data│kcp header │ size2 │msg type│msg data│ ...
└────────────┴────────┴────────┴────────┴────────────┴────────┴────────┴────────┘
size1 = 8 + len1
size2 = 8 + len2
kcp头后面是程序里自定义的数据包结构,由8字节数据包头和实际发送的数据包组成,8字节数据包头里前4字节是头和数据包的总长度,后4字节是消息类型。
查看kcp代码,由下面两个函数确定kcp头的结构
发送视频包时,把 FFmpeg 编码后的视频帧拆成RTP包,先构造8字节头,再加上拆好后的RTP包用KCP发送,程序里视频包的msg type值为4738。
我过滤了一个只有视频包的抓包,直接打开如下:
用Lua实现 KCP 和 RTP 解析器插件后,再次打开效果如下,可以看到 KCP 头和 RTP 头各个字段的信息:
从GitHub kcp_rtp_dissector下载抓包文件和代码。
打开wireshark安装目录下文件 d:\Program Files\WiresharkPortable\App\Wireshark\init.lua:
在最后一行加上 dofile(DATA_DIR.."kcp_dissector.lua")--add this line ,如下图所示
if not running_superuser or run_user_scripts_when_superuser then dofile(DATA_DIR.."console.lua") end --dofile(DATA_DIR.."dtd_gen.lua") dofile(DATA_DIR.."kcp_dissector.lua")--add this line
把kcp_dissector.lua复制到init.lua所在目录,直接打开下载的抓包文件kcp_video_61961-26098.pcapng,就能看到上图解析后的KCP 和 RTP包内容。
代码kcp_dissector.lua如下,
1 -- author: yinkaisheng@foxmail.com 2 -- for decoding kcp udp msg 3 require "bit32" 4 5 do 6 kcp_parse_table = { } 7 msg_header_size = 8 8 9 function append_str(str, strformat, key, value) 10 if string.len(str) == 0 or string.sub(str, -1, -1) == '{' then 11 return str .. string.format(strformat, key, value) 12 else 13 return str .. ',' .. string.format(strformat, key, value) 14 end 15 end 16 17 function parse_le_uint8(protocol_type_name, start, name, buf, pkt, root, col_str) 18 local value = buf(start, 1):le_uint() 19 col_str = append_str(col_str, '%s=%u', name, value) 20 root:add_le(_G[protocol_type_name].fields[name], buf(start, 1)) 21 return start + 1, col_str 22 end 23 24 function parse_le_uint16(protocol_type_name, start, name, buf, pkt, root, col_str) 25 local value = buf(start, 2):le_uint() 26 col_str = append_str(col_str, '%s=%u', name, value) 27 root:add_le(_G[protocol_type_name].fields[name], buf(start, 2)) 28 return start + 2, col_str 29 end 30 31 function parse_le_uint32(protocol_type_name, start, name, buf, pkt, root, col_str) 32 local value = buf(start, 4):le_uint() 33 col_str = append_str(col_str, '%s=%u', name, value) 34 root:add_le(_G[protocol_type_name].fields[name], buf(start, 4)) 35 return start + 4, col_str 36 end 37 38 function parse_uint8(protocol_type_name, start, name, buf, pkt, root, col_str) 39 local value = buf(start, 1):uint() 40 col_str = append_str(col_str, '%s=%u', name, value) 41 root:add(_G[protocol_type_name].fields[name], buf(start, 1)) 42 return start + 1, col_str 43 end 44 45 function parse_int16(protocol_type_name, start, name, buf, pkt, root, col_str) 46 local value = buf(start, 2):int() 47 col_str = append_str(col_str, '%s=%d', name, value) 48 root:add(_G[protocol_type_name].fields[name], buf(start, 2)) 49 return start + 2, col_str 50 end 51 52 function parse_int32(protocol_type_name, start, name, buf, pkt, root, col_str) 53 local value = buf(start, 4):int() 54 col_str = append_str(col_str, '%s=%d', name, value) 55 root:add(_G[protocol_type_name].fields[name], buf(start, 4)) 56 return start + 4, col_str 57 end 58 59 function parse_uint16(protocol_type_name, start, name, buf, pkt, root, col_str) 60 local value = buf(start, 2):uint() 61 col_str = append_str(col_str, '%s=%u', name, value) 62 root:add(_G[protocol_type_name].fields[name], buf(start, 2)) 63 return start + 2, col_str 64 end 65 66 function parse_uint32(protocol_type_name, start, name, buf, pkt, root, col_str) 67 local value = buf(start, 4):uint() 68 col_str = append_str(col_str, '%s=%u', name, value) 69 root:add(_G[protocol_type_name].fields[name], buf(start, 4)) 70 return start + 4, col_str 71 end 72 73 -- rtp video 74 KCP_VIDEO_RTP_MSG_TYPE = 4738 75 kcp_video_protocol_name = 'KCPVideo' 76 kcp_video_protocol_desc = 'KCP Video Msg' 77 ProtoKCPVideo = Proto(kcp_video_protocol_name, kcp_video_protocol_desc) 78 field_kcp_length = ProtoField.uint32('KCP.Length', 'MsgLen', base.DEC) 79 field_kcp_msgtype = ProtoField.uint32('KCP.MsgType', 'MsgType', base.DEC) 80 field_rtp_payload = ProtoField.uint32('RTP.Payload', 'Payload', base.DEC) 81 field_rtp_marker = ProtoField.uint32('RTP.Marker', 'Marker', base.DEC) 82 field_rtp_seqno = ProtoField.uint32('RTP.SeqNO', 'SeqNo', base.DEC) 83 field_rtp_timestamp = ProtoField.uint32('RTP.TimeStamp', 'TimeStamp', base.DEC) 84 field_rtp_ssrc = ProtoField.uint32('HYP.SSRC', 'SSRC', base.DEC) 85 field_rtp_data = ProtoField.bytes('RTP.Data', 'RtpData') 86 87 ProtoKCPVideo.fields = {field_kcp_length, field_kcp_msgtype, field_rtp_seqno, field_rtp_timestamp, field_rtp_ssrc, field_rtp_data} 88 89 function parse_udp_video(start, msg_type, kcp_data_len, buf, pkt, root) 90 -- kcp_data_len = buf(20,4):le_uint() 91 local payload_index = start+msg_header_size + 1 92 local seqno_index = start+msg_header_size + 2 93 local timestamp_index = start+msg_header_size + 4 94 local ssrc_index = start+msg_header_size + 8 95 local indicator_index = start+msg_header_size + 12--rtp head 12 96 local second_byte_value = buf(payload_index, 1):uint() 97 local rtp_payload = bit32.band(second_byte_value, 0x7F)-- or second_byte_value >> 1 -- require lua 5.3 98 local rtp_marker = bit32.rshift(second_byte_value, 7)-- or second_byte_value & 1 -- require lua 5.3 99 local rtp_seqno = buf(seqno_index, 2):uint() 100 local rtp_timestamp = buf(timestamp_index, 4):uint() 101 local rtp_ssrc = buf(ssrc_index, 4):uint() 102 local indicator = buf(indicator_index, 1):uint() 103 local indicator_type = bit32.band(indicator, 0x1F) 104 local fu_start = 0 105 local fu_end = 0 106 if indicator_type == 28 then 107 local fuheader_index = indicator_index + 1 108 local fuheader = buf(fuheader_index, 1):uint() 109 fu_start = bit32.rshift(fuheader, 7) 110 fu_end = bit32.band(bit32.rshift(fuheader, 6), 1) 111 end 112 protocol_name = tostring(pkt.cols.protocol) 113 if protocol_name ~= kcp_video_protocol_name then 114 pkt.cols.protocol = kcp_video_protocol_name 115 end 116 local rtp_str = string.format(',SeqNo=%u,TimeStamp=%u,SSRC=%u,Payload=%u', rtp_seqno, rtp_timestamp, rtp_ssrc, rtp_payload) 117 if fu_start == 1 then 118 rtp_str = rtp_str .. ',Start=1' 119 end 120 if fu_end == 1 then 121 rtp_str = rtp_str .. ',End=1' 122 end 123 if rtp_marker == 1 then 124 rtp_str = rtp_str .. ',Marker=1' 125 end 126 col_str = tostring(pkt.cols.info) .. rtp_str 127 pkt.cols.info = col_str 128 local t = root:add(ProtoKCPVideo, buf(start, kcp_data_len)) 129 t:add(field_kcp_length, buf(start, 4)) 130 t:add(field_kcp_msgtype, buf(start + 4, 4)) 131 t:add(field_rtp_seqno, buf(seqno_index, 2)) 132 t:add(field_rtp_timestamp, buf(timestamp_index, 4)) 133 t:add(field_rtp_ssrc, buf(ssrc_index, 4)) 134 t:add(field_rtp_data, buf(start + msg_header_size, kcp_data_len - msg_header_size)) 135 return start + kcp_data_len - msg_header_size, col_str 136 end 137 138 kcp_parse_table[KCP_VIDEO_RTP_MSG_TYPE] = parse_udp_video 139 140 -- kcp 141 kcp_conv_table = {} 142 kcp_head_size = 28 143 kcp_header_protocol_name = 'KCPHeader' 144 kcp_header_protocol_desc = 'KCP Header' 145 ProtoKCPHeader = Proto(kcp_header_protocol_name, kcp_header_protocol_desc) 146 KCPHeaders = { 147 {'conv', ProtoField.uint32, parse_le_uint32, base.DEC}, -- default DEC, can be omitted 148 {'cmd', ProtoField.uint32, parse_le_uint8, base.DEC}, 149 {'frg', ProtoField.uint32, parse_le_uint8, base.DEC}, 150 {'wnd', ProtoField.uint32, parse_le_uint16, base.DEC}, 151 {'ts', ProtoField.uint32, parse_le_uint32, base.DEC}, 152 {'sn', ProtoField.uint32, parse_le_uint32, base.DEC}, 153 {'una', ProtoField.uint32, parse_le_uint32, base.DEC}, 154 {'len', ProtoField.uint32, parse_le_uint32, base.DEC}, 155 {'snd_una', ProtoField.uint32, parse_le_uint32, base.DEC}, 156 } 157 for key, value in pairs(KCPHeaders) do 158 local field = value[2](kcp_header_protocol_name .. '.' .. value[1], value[1]) 159 ProtoKCPHeader.fields[value[1]] = field 160 end 161 162 function parse_kcp(start, msg_type, kcp_len, buf, pkt, root) 163 local buf_len = buf:len() 164 protocol_name = tostring(pkt.cols.protocol) 165 if protocol_name == 'UDP' then 166 pkt.cols.protocol = kcp_header_protocol_name 167 end 168 local kcp_conv = buf(start, 4):le_uint() 169 kcp_conv_table[kcp_conv] = 1 170 local tree = root:add(ProtoKCPHeader, buf(start, kcp_head_size)) 171 col_str = '{' 172 for key, value in pairs(KCPHeaders) do 173 start, col_str = value[3]('ProtoKCPHeader', start, value[1], buf, pkt, tree, col_str) 174 end 175 col_str = col_str .. '}' 176 old_str = tostring(pkt.cols.info) 177 if string.find(old_str, '{conv') == nil then 178 fs, fe = string.find(old_str, ' → ') 179 if fe == nil then 180 pkt.cols.info = col_str 181 else 182 fs, fe = string.find(old_str, ' ', fe + 1) 183 if fs == nil then 184 pkt.cols.info = col_str 185 else 186 pkt.cols.info = string.sub(old_str, 1, fs) .. col_str 187 end 188 end 189 else 190 col_str = old_str .. col_str 191 pkt.cols.info = col_str 192 end 193 if start + msg_header_size <= buf_len then 194 local kcp_data_len = buf(start, 4):uint() 195 msg_type = buf(start + 4, 4):uint() 196 if kcp_len == kcp_data_len and start + kcp_data_len <= buf_len then 197 local parse_func = kcp_parse_table[msg_type] 198 if parse_func then 199 start_new, col_str = parse_func(start, msg_type, kcp_data_len, buf, pkt, root) 200 else 201 pkt.cols.info = tostring(pkt.cols.info) .. string.format(', no parse function for msg type %u', msg_type) 202 end 203 start = start + kcp_data_len 204 if start + kcp_head_size <= buf_len then 205 kcp_conv = buf(start, 4):le_uint() 206 kcp_len = buf(start + 20, 4):le_uint() 207 if kcp_conv_table[kcp_conv] == 1 then 208 parse_kcp(start, 0, kcp_len, buf, pkt, root) 209 else 210 end 211 end 212 else 213 if start + kcp_head_size <= buf_len then 214 kcp_conv = buf(start, 4):le_uint() 215 kcp_len = buf(start + 20, 4):le_uint() 216 if kcp_conv_table[kcp_conv] == 1 then 217 parse_kcp(start, 0, kcp_len, buf, pkt, root) 218 else 219 end 220 end 221 end 222 end 223 return start, col_str 224 end 225 226 -- protocal 227 kcp_protocol_name = 'KCP' 228 kcp_protocol_desc = 'KCP Protocol' 229 ProtoKCP = Proto(kcp_protocol_name, kcp_protocol_desc) 230 231 -- dissector 232 function ProtoKCP.dissector(buf, pkt, root) 233 local buf_len = buf:len() 234 if buf_len < msg_header_size then 235 return 236 end 237 protocol_name = tostring(pkt.cols.protocol) 238 -- pkt.cols.info = tostring(pkt.cols.info) .. ' |' .. protocol_name .. '|' 239 -- if 1 then 240 -- return 241 -- end 242 local data_len = buf(0, 4):uint() 243 if buf_len == data_len then 244 local msg_type = buf(4, 4):uint() 245 local parse_func = kcp_parse_table[msg_type] 246 if parse_func then 247 parse_func(0, msg_type, buf_len, buf, pkt, root) 248 else 249 pkt.cols.info = tostring(pkt.cols.info) .. string.format(', no parse function for msg id %u', msg_type) 250 end 251 elseif kcp_head_size + 8 <= buf_len then 252 data_len = buf(20, 4):le_uint() 253 local kcp_data_len = buf(kcp_head_size, 4):uint() 254 if data_len == kcp_data_len then 255 parse_kcp(0, 0, data_len, buf, pkt, root) 256 else 257 local kcp_conv = buf(0, 4):le_uint() 258 if kcp_conv_table[kcp_conv] == 1 then 259 parse_kcp(0, 0, data_len, buf, pkt, root) 260 else 261 end 262 end 263 elseif kcp_head_size <= buf_len then 264 local kcp_conv = buf(0, 4):le_uint() 265 if kcp_conv_table[kcp_conv] == 1 then 266 parse_kcp(0, 0, data_len, buf, pkt, root) 267 else 268 end 269 else 270 end 271 end 272 273 local udp_table = DissectorTable.get('udp.port') 274 udp_table:add('61961', ProtoKCP) 275 end
未完待续...