4、实时数据传输:TCP/IP协议栈、UDP与TCP对比、Socket编程基础、数据包结构与封装

各位好,我是老张。在PIS系统里,数据采集完了,怎么传回来?这就是我们今天要聊的核心——实时数据传输。

说实话,很多刚入行的朋友觉得数据传输就是“发个包过去”,没那么简单。我在做地铁PIS项目时,就遇到过因为传输协议选错,导致车厢视频流卡成PPT的惨痛教训。嗯,今天咱们就把这块彻底讲透。

4.1 TCP/IP协议栈——数据是怎么“爬”上车的?

先问个问题:你发一条指令到车厢显示屏,中间经历了什么?

TCP/IP协议栈,说白了就是一套“分层打包”的规则。我习惯把它想象成寄快递:

  • 应用层:你写的业务数据,比如“播放下一站信息”
  • 传输层:决定用TCP还是UDP,给数据加个“快递单号”
  • 网络层:写上源IP和目标IP,相当于“发件地址”和“收件地址”
  • 链路层:加上MAC地址,搞定“最后一公里”的物理传输

每一层只关心自己的事。我在调试PIS系统时,经常用Wireshark抓包,一层层剥开看。你想想看,如果应用层数据是100字节,经过层层封装,到线路上可能变成150字节。这多出来的50字节,就是协议开销。

关键点:PIS系统的实时性要求高,协议栈的每一层都可能成为瓶颈。我曾经在项目里发现,就是因为网络层路由配置不当,导致数据包绕了远路,延迟直接飙到500ms。

4.2 UDP与TCP对比——选谁?看场景!

这是PIS系统架构师最常做的选择题。我直接说结论:

特性 TCP UDP
连接方式 面向连接(先握手) 无连接(直接发)
可靠性 保证送达,丢包重传 不保证,丢了就丢了
传输效率 较低(有确认机制) 高(无确认)
适用场景 控制指令、配置文件下发 音视频流、传感器实时数据

我个人习惯:控制类用TCP,数据流用UDP

举个例子。在PIS系统中,列车到站信息必须准确无误,我用TCP。但车厢内的视频监控流,如果丢几帧画面,乘客根本感觉不到,用UDP就对了。

避坑指南:我曾经在一个项目里,用TCP传视频流,结果因为网络抖动,TCP的拥塞控制机制导致画面反复卡顿。后来换成UDP + 应用层简单校验,问题就解决了。记住:TCP不是万能的。

4.3 Socket编程基础——代码怎么写?

Socket,说白了就是操作系统给应用层开的一扇“网络窗口”。你通过这个窗口,把数据扔出去,或者从外面接进来。

我给大家一个最简的TCP服务端代码框架,PIS系统中常用:

// TCP服务端(PIS数据接收端)
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    // AF_INET: IPv4, SOCK_STREAM: TCP
    
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
    address.sin_port = htons(8080);        // PIS常用端口
    
    bind(server_fd, (struct sockaddr*)&address, sizeof(address));
    listen(server_fd, 3);  // 最大排队连接数
    
    // 接受客户端连接(车厢设备)
    int client_fd = accept(server_fd, NULL, NULL);
    
    char buffer[1024] = {0};
    read(client_fd, buffer, 1024);  // 读取车厢数据
    printf("收到数据: %s\n", buffer);
    
    close(client_fd);
    close(server_fd);
    return 0;
}

这段代码看着简单,但我在实际项目中踩过坑。比如htons()这个函数,字节序转换。不同CPU架构的字节序不一样,如果不转换,数据解析出来全是乱的。

小技巧:调试Socket程序时,先用telnetnc工具模拟客户端发数据,比写完整客户端快得多。我经常这么干。

4.4 数据包结构与封装——别让数据“裸奔”

数据在网络上传输,不能光着身子跑。得穿衣服——也就是封装。

一个典型的PIS数据包结构,我习惯这样设计:

+----------------+----------------+----------------+----------------+
|  帧头 (2字节)   |  长度 (2字节)  |  类型 (1字节)  |  序列号 (2字节) |
+----------------+----------------+----------------+----------------+
|  时间戳 (4字节)  |  源地址 (2字节) |  目标地址 (2字节)|  校验 (2字节)  |
+----------------+----------------+----------------+----------------+
|                        数据负载 (N字节)                          |
+----------------------------------------------------------------+
|                          帧尾 (2字节)                           |
+----------------------------------------------------------------+

为什么要有帧头和帧尾?说白了就是告诉接收方:“我开始了”和“我结束了”。

我在项目中遇到过一个问题:两个数据包粘在一起了,接收方解析出错。后来加了帧头(比如0xAA55)和帧尾(0x55AA),并在代码里做状态机解析,问题就解决了。

封装要点

  • 帧头:固定魔数,用于同步
  • 长度字段:告诉接收方要读多少字节
  • 校验字段:CRC16或简单异或,防止数据被篡改
  • 序列号:用于检测丢包和重排序

你想想看,如果车厢里的温湿度传感器数据,因为封装不规范,导致地面服务器解析出错的温度值,那空调控制逻辑就全乱了。嗯,这就是细节决定成败。

注意:数据包大小要控制。以太网MTU是1500字节,如果PIS数据包超过这个值,会被分片传输,增加延迟和丢包风险。我一般把单个数据包控制在1400字节以内。

好了,关于实时数据传输的核心内容就这些。总结一句话:TCP保可靠,UDP保实时,Socket是窗口,封装要规范。下一章我们聊聊数据在PIS系统中的存储与转发策略,到时候见。