网络通信与协议模块
网络通信与协议模块 —— 项目详细解析(代码篇)
说明:本文聚焦代码层面,逐子系统、逐类讲解本模块的实现细节、数据结构与关键算法。它面向海康威视 IPTC 热成像测温摄像机(GY1 平台)软件中的网络通信与协议模块(
netproto命名空间,C++14 实现),覆盖:Socket 封装、UDP 端口池、RTP/RTCP 媒体传输、RTSP 服务、HTTP/Web 多协议服务、WebSocket、GB28181/SIP、SDK 私有协议。
一、模块代码结构
netproto(C++ 命名空间)
├── net_types.h / net_socket.{h,cpp} 传输层:SockAddr / NetSocket / UdpPortPool
├── rtp_packet.{h,cpp} RTP 报文封装解析 + RtpSourceStats 统计机
├── rtcp_packet.{h,cpp} RTCP 报文(SR/RR/SDES/BYE/APP)+ RtcpSession
├── rtsp_message.{h,cpp} RTSP 消息模型 + 增量解析器
├── rtsp_server.{h,cpp} RTSP 服务端:Session / SessionManager / Listener / Server
├── http_server.{h,cpp} HTTP 服务端与客户端 + 多协议 URL 路由
├── websocket_server.{h,cpp} WebSocket 帧编解码 + 服务端会话
├── sip_session.{h,cpp} GB28181/SIP 消息 + SipManager 状态机
└── sdk_net_conn.{h,cpp} SDK 私有协议:命令头/登录/同步异步命令/接口分发
分层关系:应用层(各协议 Server)→ 协议层(消息/报文编解码)→ 传输层(NetSocket/端口池)→ 平台层(POSIX/Winsock2 差异收口)。
二、传输层:NetSocket 与 SockAddr
2.1 SockAddr —— 地址的跨平台抽象
用 enum class Family { IPv4 } + std::string ip + uint16_t port 持有地址,并提供两套互转:
fromSockaddr(const sockaddr_in&):把系统结构转成内部表示,用inet_ntop取 IP、ntohs转端口。toSockaddrV4():反向,inet_pton填 IP;特判ip == "0.0.0.0" || ip.empty()时填INADDR_ANY。
struct sockaddr_in SockAddr::toSockaddrV4() const {
struct sockaddr_in sa; memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
if (ip == "0.0.0.0" || ip.empty()) {
sa.sin_addr.s_addr = INADDR_ANY;
} else {
inet_pton(AF_INET, ip.c_str(), &sa.sin_addr);
}
return sa;
}
这样上层代码永远不用碰 sockaddr_in 的字节序与 AF_INET 细节。
2.2 NetSocket —— 移动语义 + RAII 的 socket 封装
核心设计目标是防止 fd 双重 close。socket 是独占句柄,若两个对象共享同一 fd,析构时会各自 close 一次,第二次可能误关操作系统新分配的 fd。因此:
- 析构自动 close,并把 fd 置
INVALID_SOCKET:
NetSocket::~NetSocket() { close(); }
void NetSocket::close() {
if (fd_ != INVALID_SOCKET) {
#ifdef _WIN32
::closesocket(fd_); // Windows
#else
::close(fd_); // POSIX
#endif
fd_ = INVALID_SOCKET;
}
}
- 只允许移动、禁止拷贝,移动构造/赋值把源对象 fd 置空:
NetSocket::NetSocket(NetSocket&& other) noexcept
: fd_(other.fd_), type_(other.type_), localAddr_(std::move(other.localAddr_)),
peerAddr_(std::move(other.peerAddr_)) {
other.fd_ = INVALID_SOCKET; // 释放源的所有权
}
- 封装 TCP/UDP 全生命周期与 IO:
create/bind/listen/accept/connect/send/recv/sendTo/recvFrom/setOptions/setNonBlocking。setOptions内统一设置SO_REUSEADDR / SO_KEEPALIVE / SO_RCVBUF / SO_SNDBUF / SO_RCVTIMEO / SO_SNDTIMEO,超时在 Windows 用DWORD ms、POSIX 用struct timeval,两套#ifdef收敛在一个函数里。
2.3 UdpPortPool —— RTP/RTCP 端口偶对分配
实时媒体要求 RTP 用一端口、RTCP 用相邻端口(rtpPort 偶、rtcpPort = rtpPort + 1),便于 NAT/防火墙放行。用一个 std::vector<bool> used_ 做位图 + std::mutex 保护:
bool UdpPortPool::acquire(uint16_t& rtpPort, uint16_t& rtcpPort) {
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < poolSize_; ++i) {
if (!used_[i]) {
used_[i] = true;
rtpPort = startPort_ + static_cast<uint16_t>(i * 2);
rtcpPort = rtpPort + 1;
return true;
}
}
return false; // 池满
}
void UdpPortPool::release(uint16_t rtpPort, uint16_t) {
std::lock_guard<std::mutex> lock(mutex_);
size_t idx = (rtpPort - startPort_) / 2;
if (idx < poolSize_) used_[idx] = false;
}
acquire 从起始端口起按 i*2 步进找空闲偶端口;release 由 RTP 端口反推下标归还。这样多路预览并发时不会互相抢占端口。
三、协议层:RTP / RTCP 媒体传输
3.1 RtpPacket —— RTP 报文编解码
构造分两种:解析模式(从裸字节构造,parseHeader 校验版本并拆出各字段)与打包模式(从 RtpParam + 负载构造,pack() 时填充头 + 拼负载)。
parseHeader 用位运算拆解 12 字节定长头:
int ver = (byte0 >> 6) & 0x03;
if (ver != RTP_VERSION) return false; // 版本检查
int marker = (byte1 >> 7) & 0x01;
int payload = byte1 & 0x7F; // 载荷类型
uint16_t seq = ntohs(*(uint16_t*)&buf[2]); // 网络序→主机序
uint32_t ts = ntohl(*(uint32_t*)&buf[4]);
uint32_t ssrc = ntohl(*(uint32_t*)&buf[8]);
并处理可变长部分:CSRC 计数(offset = 12 + csrcCount*4)、扩展头(读 id/len,offset += 4 + extHeader.len*4)、padding(最后一个字节是填充长度,payloadLen -= buf[len-1]),逐段校验 offset > len 防越界。
pack() 时调用 buildHeader():把 RTP_VERSION<<6、marker?0x80、payload&0x7F 拼成前两字节,再用 htons/htonl + memcpy 严格按网络字节序写入 seq/ts/ssrc。
3.2 RtpSourceStats —— 乱序/丢包/抖动状态机
这是接收端质量统计的核心,按 RFC 3550 附录 A 实现。维护 baseSeq / maxSeq / cycles / received:
void RtpSourceStats::update(uint16_t seq, uint32_t ts, uint32_t arrivalTs) {
uint16_t delta = seq - maxSeq;
if (delta == 0) return; // 重复包
if (delta < RTP_MAX_DROPOUT) { // 顺序到达
if (seq < maxSeq) cycles += RTP_SEQ_MOD; // 计数进位
maxSeq = seq;
} else if (delta < RTP_SEQ_MOD - RTP_MAX_MISORDER) {
init(seq); // 序列号跳变过大→视为复位
} else {
return; // 乱序,忽略
}
received++;
// 抖动计算:一阶滤波
int arrivalDelta = (int)(arrivalTs - ts);
int transitDelta = arrivalDelta - (int)transit;
transit = arrivalTs - ts;
if (transitDelta < 0) transitDelta = -transitDelta;
jitter += transitDelta - ((jitter + 8) >> 4);
}
delta < RTP_MAX_DROPOUT:正常到达,维护maxSeq与循环计数cycles(序列号回绕)。delta介于 dropout 与SEP_MOD-MISORDER之间:说明序列号跳变异常大,判定为新源/复位并调用init重来——避免把网络抖动误判成丢包。- 否则按乱序处理,不计入。
fractionLost() 按 expected - received 得到丢包数,再用分段统计(expectedPrior / receivedPrior)计算区间丢包率,供 RTCP RR 上报。
3.3 RtcpPacket —— RTCP 报文族编码
提供 write8/16/32 三个字节流写入原语(统一 htons/htonl 转网络序),然后一个通用的 writeCommon(type,count,length) 写首个公共头(V=2、RC=count&0x1F、type、length)。
buildSR / buildRR / buildSDES / buildBYE / buildAPP 各封装一种 RTCP 包型。长度字段的语义是"32 位字数减 1",例如 SR 中 length = 6 + reportCount*6(固定 6 个字 + 每个报告块 6 个字),SDES/BYE 都做了4 字节对齐填充——SDES 尾部补 END item 并对齐,BYE 理由串也按 (4 - size%4)%4 补齐。
parse() 支持复合包:一个 UDP 报文可能拼接多个 RTCP 包,据此用 while 循环 offset += (length+1)*4 逐个解出 SR/RR/BYE,且每段都校验 offset + pktLen > len 防止越界。
3.4 RtcpSession —— 收发统计会话
维护一个 RtpSourceStats source_,收到 RTP 包时 onRtpReceived 喂给它;按需生成:
generateRR:从source_.fractionLost() / cycles + maxSeq / jitter填报告块,回给对端,这是发送端感知丢包/抖动的通道。generateSR / generateSDES / generateBYE:上报发送统计、CNAME、结束会话。
四、RTSP 服务:控制与媒体分离
4.1 RtspMsg / RtspRequest / RtspResponse —— 消息模型
RtspMsg:多组头 (std::vector<RtspHeader>,用 vector 而非 map 以保留重复头/顺序) +content_负载 +cseq_/method_。提供addHeader / getHeaderValue(含 int 版,内部stoi容错)与serializeHeaders。RtspResponse::statusMsg():把RtspStatus枚举映射到 RFC 状态文本(OK/Created/BadRequest/…)。appendTransport:按传输模式拼Transport头——UDP 填RTP/AVP;unicast;client_port=...;server_port=...-...,TCP 填RTP/AVP/TCP;unicast;interleaved=0-1,多播填RTP/AVP;multicast。
4.2 RtspMsgParser —— 增量式解析(粘包/半包)
parse(data, len, consumed) 一次调用可能只收到请求的一部分,它只解析头部 + 最终返回 consumed(本次消费的字节数),由调用方把剩余字节留到下次拼接:
size_t headerEnd = buf.find("\r\n\r\n"); // 找头结束
if (headerEnd == std::string::npos) return nullptr; // 半包,等更多数据
consumed = headerEnd + 4;
...
int contentLength = 0;
msg->getHeaderValue("Content-Length", contentLength);
if (contentLength > 0 && consumed + contentLength <= len) {
msg->content(data + consumed, contentLength);
consumed += contentLength; // 含消息体的完整消费
}
并区分请求行(METHOD URL RTSP/x.y)与响应行(RTSP/x.y CODE REASON),parseRequestLine / parseResponseLine / parseProtocolVersion 各自负责。
4.3 RtspSession —— 协议状态机
int RtspSession::processMessage(RtspMsg* msg) {
RtspMethod method = msg->method();
switch (method) {
case RtspMethod::OPTIONS: return procOptions(msg);
case RtspMethod::DESCRIBE: return procDescribe(msg);
case RtspMethod::SETUP: return procSetup(msg);
case RtspMethod::PLAY: return procPlay(msg);
case RtspMethod::PAUSE: return procPause(msg);
case RtspMethod::TEARDOWN: return procTeardown(msg);
case RtspMethod::GET_PARAMETER: return procGetParameter(msg);
case RtspMethod::HEARTBEAT: return procHeartbeat(msg);
...
}
}
open(socket, clientAddr, ssl):socket_ = std::move(socket)把新连接所有权移交会话,置active_与心跳时间。procDescribe:parseUrl拆出chanId/streamId/isPlayback(URL 格式rtsp://host:port/path),返回 SDP。procSetup:解析Transport头决定传输模式(RTP_TCP / RTP_MCAST / RTP_UDP),回Session头与Transport头。procHeartbeat:刷新lastHeartbeatMs_,供超时判定isTimeout(now, timeout) = (now - lastHeartbeatMs_) > timeout。
4.4 RtspSessionManager —— 会话生命周期与超时回收
createSession 用 std::map<SessionId, unique_ptr<RtspSession>> 持有会话(nextId_.fetch_add 生成自增 id,unique_ptr 保证所有权单一):
RtspSession* RtspSessionManager::createSession() {
std::lock_guard<std::mutex> lock(mutex_);
SessionId id = nextId_.fetch_add(1);
auto session = std::make_unique<RtspSession>(id);
RtspSession* ptr = session.get();
sessions_[id] = std::move(session);
return ptr; // 返回裸指针,所有权仍在 map
}
checkTimeout 周期性扫描:非激活或心跳超时的会话被 stop() 并从 map 摘下,回收其端口与内存,防止僵尸连接耗尽资源。
4.5 RtspListener 与 RtspServer —— 接入与编排
RtspListener:start(port,ssl)创建监听NetSocket,setOptions(SO_REUSEADDR),bind(0.0.0.0:port),listen(128),listenLoop死循环accept(),拿到新连接后回调newConnCb_(std::move(client), peerAddr())。RtspServer::onNewConnection:为新连接createSession()→session->open(std::move(socket), addr)→ detach 一个线程跑handleSession。start里用 lambda 把onNewConnection注册给 listener,stop统一listener_.stop() + sessionMgr_.stopAll()。
五、HTTP/Web 多协议服务
5.1 HttpRequest / HttpResponse —— 服务端消息模型
HttpRequest:std::map<string,string> headers+url/method/version/body/clientAddr,提供header(name)、isContentType(type)。HttpResponse::serialize():拼 HTTP 响应,自动补全Content-Type、Content-Length(若未设则填body.size())并强制加Connection: close——避免人为漏写头导致客户端悬挂。
5.2 HttpParser —— 增量解析
parse(data,len,consumed):先用 headerBuf_.append 缓存头部,找到 \r\n\r\n 后 consumed = headerEnd + 4;解析请求行(拆 METHOD URL HTTP/1.1)与逐行头部(拆 name: value,去前导空白);headerParsed_ 置位后,根据 Content-Length 读消息体,且 bodyAvailable < contentLength 时返回 false 等待后续字节——这是解决 HTTP 粘包/半包的关键。
5.3 HttpServer —— 多协议 URL 路由
一台设备要同时接待 ISAPI(私有)、ONVIF、PSIA、RESTful、SDK HTTP 几种"方言"。做法是一个监听 + 前缀路由:
void HttpServer::routeISAPI(const std::string& path, HttpHandler handler) {
route(HttpMethod::GET, "/ISAPI/" + path, handler, HttpAuthSchema::Digest);
route(HttpMethod::PUT, "/ISAPI/" + path, handler, HttpAuthSchema::Digest);
route(HttpMethod::POST, "/ISAPI/" + path, handler, HttpAuthSchema::Digest);
}
void HttpServer::routeONVIF(const std::string& path, HttpHandler handler) {
route(HttpMethod::POST, "/onvif/" + path, handler, HttpAuthSchema::None);
}
dispatch 遍历路由表,匹配 method + 前缀(req.url.find(pattern)==0 || pattern=="*")后:先按路由声明的 authType 走 checkAuth(未认证回 401 + WWW-Authenticate),再调 route.handler(req, resp)。handleConnection 中每连接一个 std::thread(...).detach(),读缓冲 8192。
5.4 HttpClient —— 客户端
sendRequest(method,url,body,type,resp):解析 URL(http://host:port/path)、NetSocket::connect、拼请求头发送(Host/Connection:close/Content-Type/Content-Length/Authorization: Basic),循环 recv 收响应,找 \r\n\r\n 拆头体、stoi 解析状态码。提供 get/post/put/del、setProxy/setAuth。
六、WebSocket 服务
6.1 WsFrame —— RFC 6455 帧编解码
encode(opcode,data,len,mask):首字节 0x80|opcode(FIN=1 + opcode),第二字节高位是 MASK、低 7 位是长度——小于 126 直接写;126-65535 用扩展 2 字节;更大用扩展 8 字节;若有 MASK 再写 4 字节掩码并进行异或加密负载:
if (len < 126) frame.push_back((mask?0x80:0) | len);
else if (len < 65536) { frame.push_back((mask?0x80:0) | 126); /* 2字节长度 */ }
else { frame.push_back((mask?0x80:0) | 127); /* 8字节长度 */ }
if (mask) { /* 4字节Mask-Key + 负载异或 */ }
decode 对称处理,返回 consumed(支持一缓冲多帧循环解)。
6.2 WebSocketServer —— 握手与会话
handleConnection:先 recv 到 HTTP 升级请求,找 Sec-WebSocket-Key,回 101 Switching Protocols 握手;建 WsSession(存 sockFd/clientIp/valid/close)入 sessions_ map;消息循环里 WsFrame::decode 解出各帧,Close 帧置 close,Text 帧经 msgCb_ 回调上层。sendJsonResponse 把 {status,url,channel,streamId} 序列化为 JSON 经帧发送(供实时推送/OTAP 分片状态返回)。
七、GB28181/SIP 平台接入
7.1 SipMessage —— SIP 消息模型
- 多种
buildXxx工厂:buildRegister(REGISTER、含From/To/Call-ID/CSeq/Contact/Expires)、buildInvite(INVITE + 内嵌 SDP 构建:v/o/s/c/t/m行 +y=放 SSRC)、buildBye/buildOptions/buildMessage(MESSAGE 承载Application/MANSCDP+xml的报警/PTZ XML)。 parse:区分响应行(SIP/2.0 200 OK)与请求行(METHOD sip:... SIP/2.0),把方法串映射到SipMethod枚举(REGISTER/INVITE/ACK/BYE/CANCEL/...),逐行解析头。serialize:协议无关地输出请求/响应行 + 头 + 体。
generateCallId 用 std::random_device + mt19937 生成不重复的 Call-ID。
7.2 SipManager —— 注册与点播状态机
std::map<int, SipPlatformParam> platforms_ 和 std::map<int, SipRegState> regStates_(枚举 NotRegistered/Registering/Registered/Unregistering):
registerTo:置Registering,发 REGISTER(Expires=expire);handleMessage收到200时把Registering推进为Registered——注册保活靠 Expires 过期后重发。unregister:置Unregistering,发Expires=0的 REGISTER 注销。startPreview:构建含 SDP(video/RTP/AVP+PS载荷 + SSRC)的 INVITE 发给平台,并登记SipSession(callId/chanId/streamId)到sessions_。ptzControl / reportAlarm:buildMessage携带DeviceControl / Alarm类型的MANSCDPXML 上送平台。listenLoop:UDPrecvFrom收平台响应,SipMessage::parse后handleMessage分发,eventCb_回上行业务。
控制协议(SIP/SDP)与媒体传输(RTP)分离:SDP 只协商端口/载荷,实际画面由 RTP 通道推送。
八、SDK 私有协议
8.1 SdkNetCmdHeader —— 二进制命令头
固定 32 字节(5×4 + 6),serialize/parse 负责与线上格式互转;网络序统一用 htonl/ntohl,length = 26 + dataLen:
ByteBuf SdkNetCmdHeader::serialize() const {
ByteBuf buf; buf.resize(32);
uint32_t* p32 = (uint32_t*)buf.data();
p32[0]=htonl(length); p32[1]=htonl(ifVer); p32[2]=htonl(netCmd);
p32[3]=htonl(userID); p32[4]=htonl(clientIp);
memcpy(p+20, clientMac, 6);
return buf;
}
8.2 SdkLoginReq::encryptPassword —— 魔数异或加密
对密码做逐字节固定密钥异或,密钥表是一个 16 字节魔数数组,i % keyLen 循环取:
static const unsigned char magicKey[] = { 0x12,0x34,...,0x88 };
result.push_back((char)((unsigned char)input[i] ^ magicKey[i % keyLen]));
8.3 SdkNetClient —— 同步/异步双模式命令
connect:创建 TCPNetSocket、设读写超时、connect(serverIp,port)、connected_=true、启动recvLoop接收线程处理服务器主动推送。sendCommand(cmd,data,len,response,timeout):同步——sendHeader后send(data),再recvResponse(先收 32 字节头、SdkNetCmdHeader::parse得bodyLen,循环读满)。sendCommandAsync(cmd,data,len,cb):异步——detach 一个线程复制数据、内部走同步sendCommand,完成后回调cb(cmd,response),不阻塞调用线程,特别适合高频的 PTZ/预览控制。login:按固定偏移拼登录体(userName(32)+encryptedPassword(32)+version(4)+clientIp(16)+clientMac(6)),发LOGIN命令;按偏移解析响应中的result/userId/serverIp/serverVersion/deviceType/channelCount。
8.4 SdkNetManager 与 NetInterfaceHandler
SdkNetManager:多客户端管理——std::map<uint32_t, shared_ptr<SdkNetClient>> clients_,createClient/destroyClient/getClient;init/deinit内处理 Windows 的WSAStartup/WSACleanup;netParams_存 DDNS/UPnP/NTP 等默认网络参数。NetInterfaceHandler:命令→处理函数 分发表,std::map<SdkNetCmd, CommandHandler> handlers_。提供一组便捷注册:registerConfigHandler(CONFIG_GET/SET)、registerPtzHandler(PTZ_CONTROL)、registerAlarmHandler、registerCaptureHandler、registerUpgradeHandler(UPGRADE_START/DATA/END)、registerVoiceTalkHandler——上层把业务实现登记进来,底层收到命令即handle分发。
九、关键设计要点汇总
| 设计点 | 解决的问题 | 实现位置 |
|---|---|---|
| NetSocket 移动语义 + RAII | fd 双重 close | net_socket |
跨平台 #ifdef _WIN32 收口 | Winsock2/POSIX 差异 | net_socket |
| UdpPortPool 位图 | RTP/RTCP 端口偶对并发分配 | net_socket |
consumed 增量解析 | TCP 粘包/半包 | rtsp_message / http_server 的 Parser::parse |
| 90kHz 时间戳 + 网络序 | 实时媒体时间/字节序对齐 | rtp_packet |
| 乱序/复位状态机 | 抖动误判为丢包 | RtpSourceStats::update |
| RTSP 会话 map + 超时回收 | 多客户端 + 防僵尸占资源 | rtsp_server |
| HTTP URL 前缀路由 | 多协议(ISAPI/ONVIF/…)分流 | http_server |
| WS 帧长度分级 + Mask | RFC 6455 大/小负载与加密 | websocket_server |
| SIP 注册状态机 + SDP 协商 | GB28181 注册/点播 | sip_session |
| SDK 同步/异步命令双模式 | 阻塞 vs 实时控制 | sdk_net_conn |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)