网络通信与协议模块 —— 项目详细解析(代码篇)

说明:本文聚焦代码层面,逐子系统、逐类讲解本模块的实现细节、数据结构与关键算法。它面向海康威视 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/setNonBlockingsetOptions 内统一设置 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<<6marker?0x80payload&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=2RC=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_ 与心跳时间。
  • procDescribeparseUrl 拆出 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 —— 会话生命周期与超时回收

createSessionstd::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 —— 接入与编排

  • RtspListenerstart(port,ssl) 创建监听 NetSocketsetOptions(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 一个线程handleSessionstart 里用 lambda 把 onNewConnection 注册给 listener,stop 统一 listener_.stop() + sessionMgr_.stopAll()

五、HTTP/Web 多协议服务

5.1 HttpRequest / HttpResponse —— 服务端消息模型

  • HttpRequeststd::map<string,string> headers + url/method/version/body/clientAddr,提供 header(name)isContentType(type)
  • HttpResponse::serialize():拼 HTTP 响应,自动补全 Content-TypeContent-Length(若未设则填 body.size())并强制加 Connection: close——避免人为漏写头导致客户端悬挂。

5.2 HttpParser —— 增量解析

parse(data,len,consumed):先用 headerBuf_.append 缓存头部,找到 \r\n\r\nconsumed = 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=="*")后:先按路由声明的 authTypecheckAuth(未认证回 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/delsetProxy/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 帧置 closeText 帧经 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:协议无关地输出请求/响应行 + 头 + 体。

generateCallIdstd::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 / reportAlarmbuildMessage 携带 DeviceControl / Alarm 类型的 MANSCDP XML 上送平台。
  • listenLoop:UDP recvFrom 收平台响应,SipMessage::parsehandleMessage 分发,eventCb_ 回上行业务。

控制协议(SIP/SDP)与媒体传输(RTP)分离:SDP 只协商端口/载荷,实际画面由 RTP 通道推送。


八、SDK 私有协议

8.1 SdkNetCmdHeader —— 二进制命令头

固定 32 字节(5×4 + 6),serialize/parse 负责与线上格式互转;网络序统一用 htonl/ntohllength = 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:创建 TCP NetSocket、设读写超时、connect(serverIp,port)connected_=true启动 recvLoop 接收线程处理服务器主动推送。
  • sendCommand(cmd,data,len,response,timeout)同步——sendHeadersend(data),再 recvResponse(先收 32 字节头、SdkNetCmdHeader::parsebodyLen,循环读满)。
  • 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/getClientinit/deinit 内处理 Windows 的 WSAStartup/WSACleanupnetParams_ 存 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 移动语义 + RAIIfd 双重 closenet_socket
跨平台 #ifdef _WIN32 收口Winsock2/POSIX 差异net_socket
UdpPortPool 位图RTP/RTCP 端口偶对并发分配net_socket
consumed 增量解析TCP 粘包/半包rtsp_message / http_serverParser::parse
90kHz 时间戳 + 网络序实时媒体时间/字节序对齐rtp_packet
乱序/复位状态机抖动误判为丢包RtpSourceStats::update
RTSP 会话 map + 超时回收多客户端 + 防僵尸占资源rtsp_server
HTTP URL 前缀路由多协议(ISAPI/ONVIF/…)分流http_server
WS 帧长度分级 + MaskRFC 6455 大/小负载与加密websocket_server
SIP 注册状态机 + SDP 协商GB28181 注册/点播sip_session
SDK 同步/异步命令双模式阻塞 vs 实时控制sdk_net_conn
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐