在嵌入式上位机开发中,UDP协议因其无连接、低延迟的特性,常被用于实时性要求较高的场景。与TCP协议不同,UDP不需要建立连接即可直接发送数据,这使得UDP Client端的实现比TCP Client更加简洁高效。
实际项目中,UDP特别适合传感器数据采集、视频流传输等对实时性要求高但允许少量丢包的场景。我曾在一个工业温度监测系统中使用UDP协议,将分布在车间各处的传感器数据实时上报给上位机,实测延迟比TCP降低了60%以上。
创建UDP套接字是通信的第一步,关键代码如下:
c复制iSocketClient = socket(AF_INET, SOCK_DGRAM, 0);
if (iSocketClient == INVALID_SOCKET) {
printf("socket创建失败: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
这里有几个需要注意的技术细节:
AF_INET表示使用IPv4地址族SOCK_DGRAM指定了UDP数据报类型在Windows平台开发时,务必先调用
WSAStartup初始化Winsock库。我曾遇到过忘记初始化导致socket创建失败的情况,调试了半天才发现问题所在。
UDP通信需要明确指定目标服务器的地址和端口:
c复制tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, argv[1], &tSocketServerAddr.sin_addr);
这里有几个关键点:
htons函数将主机字节序转换为网络字节序inet_pton将点分十进制的IP地址转换为二进制形式实际项目中,我建议将服务器IP和端口做成可配置参数,而不是硬编码在代码中。可以通过配置文件或命令行参数传入,提高程序的灵活性。
UDP使用sendto函数发送数据,其核心特点是每次发送都需要指定目标地址:
c复制int iSendLen = sendto(iSocketClient, acSendBuf, strlen(acSendBuf), 0,
(struct sockaddr*)&tSocketServerAddr, sizeof(tSocketServerAddr));
使用sendto时需要注意:
在工业现场,我发现UDP包大小控制在1200字节以下时丢包率最低。超过这个大小,特别是在WiFi环境下,丢包率会明显上升。
原始代码已经实现了基本功能,但可以做一些改进:
优化后的主循环如下:
c复制while (1) {
printf("请输入要发送的消息(输入quit退出):");
if (fgets(acSendBuf, sizeof(acSendBuf), stdin)) {
if (strcmp(acSendBuf, "quit\n") == 0) break;
// 发送数据
int iSendLen = sendto(iSocketClient, acSendBuf, strlen(acSendBuf), 0,
(struct sockaddr*)&tSocketServerAddr, sizeof(tSocketServerAddr));
if (iSendLen == SOCKET_ERROR) {
printf("发送失败: %d\n", WSAGetLastError());
continue;
}
// 接收回复
char acRecvBuf[1024] = {0};
int iAddrLen = sizeof(tSocketServerAddr);
int iRecvLen = recvfrom(iSocketClient, acRecvBuf, sizeof(acRecvBuf), 0,
(struct sockaddr*)&tSocketServerAddr, &iAddrLen);
if (iRecvLen > 0) {
printf("收到回复: %s\n", acRecvBuf);
}
}
}
对于需要同时处理收发数据的场景,可以使用多线程:
c复制DWORD WINAPI RecvThread(LPVOID lpParam) {
SOCKET sock = *(SOCKET*)lpParam;
char acRecvBuf[1024];
struct sockaddr_in fromAddr;
int iAddrLen = sizeof(fromAddr);
while (1) {
int iRecvLen = recvfrom(sock, acRecvBuf, sizeof(acRecvBuf), 0,
(struct sockaddr*)&fromAddr, &iAddrLen);
if (iRecvLen > 0) {
acRecvBuf[iRecvLen] = '\0';
printf("\n收到消息: %s\n", acRecvBuf);
}
}
return 0;
}
// 在主函数中创建接收线程
HANDLE hThread = CreateThread(NULL, 0, RecvThread, &iSocketClient, 0, NULL);
bash复制nc -ul 8888
python复制import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 8888))
while True:
data, addr = sock.recvfrom(1024)
print(f"收到来自{addr}的消息: {data.decode()}")
sin_family是否设置为AF_INETrecvfrom返回值inet_pton时检查返回值,确保IP格式正确在调试一个跨网段UDP通信问题时,我发现Windows防火墙默认会阻止入站UDP连接。需要在防火墙设置中添加例外规则,或者临时关闭防火墙进行测试。
c复制int nSendBuf = 1024 * 1024; // 1MB
setsockopt(iSocketClient, SOL_SOCKET, SO_SNDBUF, (char*)&nSendBuf, sizeof(nSendBuf));
c复制int nRecvBuf = 1024 * 1024; // 1MB
setsockopt(iSocketClient, SOL_SOCKET, SO_RCVBUF, (char*)&nRecvBuf, sizeof(nRecvBuf));
对于重要数据,可以实现简单的应用层确认机制:
c复制typedef struct {
uint32_t seq_num;
char data[1020];
} UDPPacket;
// 发送方
UDPPacket packet;
packet.seq_num = htonl(seq++);
strncpy(packet.data, acSendBuf, sizeof(packet.data));
sendto(iSocketClient, (char*)&packet, sizeof(packet), 0, ...);
// 接收方
UDPPacket packet;
recvfrom(sock, (char*)&packet, sizeof(packet), 0, ...);
uint32_t recv_seq = ntohl(packet.seq_num);
// 发送ACK
sendto(sock, (char*)&recv_seq, sizeof(recv_seq), 0, ...);
在某生产线监控项目中,我们使用UDP协议实现了以下功能:
遇到的问题及解决方案:
在STM32+LWIP的嵌入式设备上,UDP通信需要注意:
MEM_SIZE和PBUF_POOL_SIZEudp_recv注册接收回调函数示例代码片段:
c复制struct udp_pcb *upcb;
void udp_receive_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, u16_t port)
{
// 处理接收到的数据
pbuf_free(p);
}
upcb = udp_new();
udp_bind(upcb, IP_ADDR_ANY, 8888);
udp_recv(upcb, udp_receive_callback, NULL);
在选择协议时考虑以下因素:
对于需要可靠传输的场景,可以考虑:
UDP通信的安全措施:
实现数据包验证的示例:
c复制typedef struct {
uint32_t magic; // 魔数标识 0x55AA55AA
uint32_t crc32; // 数据部分的CRC校验
uint16_t cmd; // 命令字
uint16_t length; // 数据长度
char data[]; // 可变长数据
} SecureUDPPacket;
在嵌入式开发中,UDP协议因其简洁高效的特点,仍然是许多实时应用的理想选择。通过合理的优化和错误处理,可以构建出稳定可靠的UDP通信系统。