前段时间写了一个局域网音视频通话的程序,使用开源 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如下,
-- author: yinkaisheng@foxmail.com
-- for decoding kcp udp msg
require "bit32" do
kcp_parse_table = { }
msg_header_size = function append_str(str, strformat, key, value)
if string.len(str) == or string.sub(str, -, -) == '{' then
return str .. string.format(strformat, key, value)
else
return str .. ',' .. string.format(strformat, key, value)
end
end function parse_le_uint8(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):le_uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add_le(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_le_uint16(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):le_uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add_le(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_le_uint32(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):le_uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add_le(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_uint8(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_int16(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):int()
col_str = append_str(col_str, '%s=%d', name, value)
root:add(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_int32(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):int()
col_str = append_str(col_str, '%s=%d', name, value)
root:add(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_uint16(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end function parse_uint32(protocol_type_name, start, name, buf, pkt, root, col_str)
local value = buf(start, ):uint()
col_str = append_str(col_str, '%s=%u', name, value)
root:add(_G[protocol_type_name].fields[name], buf(start, ))
return start + , col_str
end -- rtp video
KCP_VIDEO_RTP_MSG_TYPE =
kcp_video_protocol_name = 'KCPVideo'
kcp_video_protocol_desc = 'KCP Video Msg'
ProtoKCPVideo = Proto(kcp_video_protocol_name, kcp_video_protocol_desc)
field_kcp_length = ProtoField.uint32('KCP.Length', 'MsgLen', base.DEC)
field_kcp_msgtype = ProtoField.uint32('KCP.MsgType', 'MsgType', base.DEC)
field_rtp_payload = ProtoField.uint32('RTP.Payload', 'Payload', base.DEC)
field_rtp_marker = ProtoField.uint32('RTP.Marker', 'Marker', base.DEC)
field_rtp_seqno = ProtoField.uint32('RTP.SeqNO', 'SeqNo', base.DEC)
field_rtp_timestamp = ProtoField.uint32('RTP.TimeStamp', 'TimeStamp', base.DEC)
field_rtp_ssrc = ProtoField.uint32('HYP.SSRC', 'SSRC', base.DEC)
field_rtp_data = ProtoField.bytes('RTP.Data', 'RtpData') ProtoKCPVideo.fields = {field_kcp_length, field_kcp_msgtype, field_rtp_seqno, field_rtp_timestamp, field_rtp_ssrc, field_rtp_data} function parse_udp_video(start, msg_type, kcp_data_len, buf, pkt, root)
-- kcp_data_len = buf(20,4):le_uint()
local payload_index = start+msg_header_size +
local seqno_index = start+msg_header_size +
local timestamp_index = start+msg_header_size +
local ssrc_index = start+msg_header_size +
local indicator_index = start+msg_header_size + --rtp head 12
local second_byte_value = buf(payload_index, ):uint()
local rtp_payload = bit32.band(second_byte_value, 0x7F)-- or second_byte_value >> 1 -- require lua 5.3
local rtp_marker = bit32.rshift(second_byte_value, )-- or second_byte_value & 1 -- require lua 5.3
local rtp_seqno = buf(seqno_index, ):uint()
local rtp_timestamp = buf(timestamp_index, ):uint()
local rtp_ssrc = buf(ssrc_index, ):uint()
local indicator = buf(indicator_index, ):uint()
local indicator_type = bit32.band(indicator, 0x1F)
local fu_start =
local fu_end =
if indicator_type == then
local fuheader_index = indicator_index +
local fuheader = buf(fuheader_index, ):uint()
fu_start = bit32.rshift(fuheader, )
fu_end = bit32.band(bit32.rshift(fuheader, ), )
end
protocol_name = tostring(pkt.cols.protocol)
if protocol_name ~= kcp_video_protocol_name then
pkt.cols.protocol = kcp_video_protocol_name
end
local rtp_str = string.format(',SeqNo=%u,TimeStamp=%u,SSRC=%u,Payload=%u', rtp_seqno, rtp_timestamp, rtp_ssrc, rtp_payload)
if fu_start == then
rtp_str = rtp_str .. ',Start=1'
end
if fu_end == then
rtp_str = rtp_str .. ',End=1'
end
if rtp_marker == then
rtp_str = rtp_str .. ',Marker=1'
end
col_str = tostring(pkt.cols.info) .. rtp_str
pkt.cols.info = col_str
local t = root:add(ProtoKCPVideo, buf(start, kcp_data_len))
t:add(field_kcp_length, buf(start, ))
t:add(field_kcp_msgtype, buf(start + , ))
t:add(field_rtp_seqno, buf(seqno_index, ))
t:add(field_rtp_timestamp, buf(timestamp_index, ))
t:add(field_rtp_ssrc, buf(ssrc_index, ))
t:add(field_rtp_data, buf(start + msg_header_size, kcp_data_len - msg_header_size))
return start + kcp_data_len - msg_header_size, col_str
end kcp_parse_table[KCP_VIDEO_RTP_MSG_TYPE] = parse_udp_video -- kcp
kcp_conv_table = {}
kcp_head_size =
kcp_header_protocol_name = 'KCPHeader'
kcp_header_protocol_desc = 'KCP Header'
ProtoKCPHeader = Proto(kcp_header_protocol_name, kcp_header_protocol_desc)
KCPHeaders = {
{'conv', ProtoField.uint32, parse_le_uint32, base.DEC}, -- default DEC, can be omitted
{'cmd', ProtoField.uint32, parse_le_uint8, base.DEC},
{'frg', ProtoField.uint32, parse_le_uint8, base.DEC},
{'wnd', ProtoField.uint32, parse_le_uint16, base.DEC},
{'ts', ProtoField.uint32, parse_le_uint32, base.DEC},
{'sn', ProtoField.uint32, parse_le_uint32, base.DEC},
{'una', ProtoField.uint32, parse_le_uint32, base.DEC},
{'len', ProtoField.uint32, parse_le_uint32, base.DEC},
{'snd_una', ProtoField.uint32, parse_le_uint32, base.DEC},
}
for key, value in pairs(KCPHeaders) do
local field = value[](kcp_header_protocol_name .. '.' .. value[], value[])
ProtoKCPHeader.fields[value[]] = field
end function parse_kcp(start, msg_type, kcp_len, buf, pkt, root)
local buf_len = buf:len()
protocol_name = tostring(pkt.cols.protocol)
if protocol_name == 'UDP' then
pkt.cols.protocol = kcp_header_protocol_name
end
local kcp_conv = buf(start, ):le_uint()
kcp_conv_table[kcp_conv] =
local tree = root:add(ProtoKCPHeader, buf(start, kcp_head_size))
col_str = '{'
for key, value in pairs(KCPHeaders) do
start, col_str = value[]('ProtoKCPHeader', start, value[], buf, pkt, tree, col_str)
end
col_str = col_str .. '}'
old_str = tostring(pkt.cols.info)
if string.find(old_str, '{conv') == nil then
fs, fe = string.find(old_str, ' → ')
if fe == nil then
pkt.cols.info = col_str
else
fs, fe = string.find(old_str, ' ', fe + )
if fs == nil then
pkt.cols.info = col_str
else
pkt.cols.info = string.sub(old_str, , fs) .. col_str
end
end
else
col_str = old_str .. col_str
pkt.cols.info = col_str
end
if start + msg_header_size <= buf_len then
local kcp_data_len = buf(start, ):uint()
msg_type = buf(start + , ):uint()
if kcp_len == kcp_data_len and start + kcp_data_len <= buf_len then
local parse_func = kcp_parse_table[msg_type]
if parse_func then
start_new, col_str = parse_func(start, msg_type, kcp_data_len, buf, pkt, root)
else
pkt.cols.info = tostring(pkt.cols.info) .. string.format(', no parse function for msg type %u', msg_type)
end
start = start + kcp_data_len
if start + kcp_head_size <= buf_len then
kcp_conv = buf(start, ):le_uint()
kcp_len = buf(start + , ):le_uint()
if kcp_conv_table[kcp_conv] == then
parse_kcp(start, , kcp_len, buf, pkt, root)
else
end
end
else
if start + kcp_head_size <= buf_len then
kcp_conv = buf(start, ):le_uint()
kcp_len = buf(start + , ):le_uint()
if kcp_conv_table[kcp_conv] == then
parse_kcp(start, , kcp_len, buf, pkt, root)
else
end
end
end
end
return start, col_str
end -- protocal
kcp_protocol_name = 'KCP'
kcp_protocol_desc = 'KCP Protocol'
ProtoKCP = Proto(kcp_protocol_name, kcp_protocol_desc) -- dissector
function ProtoKCP.dissector(buf, pkt, root)
local buf_len = buf:len()
if buf_len < msg_header_size then
return
end
protocol_name = tostring(pkt.cols.protocol)
-- pkt.cols.info = tostring(pkt.cols.info) .. ' |' .. protocol_name .. '|'
-- if 1 then
-- return
-- end
local data_len = buf(, ):uint()
if buf_len == data_len then
local msg_type = buf(, ):uint()
local parse_func = kcp_parse_table[msg_type]
if parse_func then
parse_func(, msg_type, buf_len, buf, pkt, root)
else
pkt.cols.info = tostring(pkt.cols.info) .. string.format(', no parse function for msg id %u', msg_type)
end
elseif kcp_head_size + <= buf_len then
data_len = buf(, ):le_uint()
local kcp_data_len = buf(kcp_head_size, ):uint()
if data_len == kcp_data_len then
parse_kcp(, , data_len, buf, pkt, root)
else
local kcp_conv = buf(, ):le_uint()
if kcp_conv_table[kcp_conv] == then
parse_kcp(, , data_len, buf, pkt, root)
else
end
end
elseif kcp_head_size <= buf_len then
local kcp_conv = buf(, ):le_uint()
if kcp_conv_table[kcp_conv] == then
parse_kcp(, , data_len, buf, pkt, root)
else
end
else
end
end local udp_table = DissectorTable.get('udp.port')
udp_table:add('', ProtoKCP)
end
未完待续...