UDP(User Datagram Protocol)是一种无连接的传输层协议,与TCP相比具有更低的延迟和更高的传输效率。在需要快速传输但对可靠性要求不高的场景下,UDP是理想的选择。本次我们将通过两个实际案例来深入理解UDP的应用:
第一个案例展示了如何使用UDP协议实现图片文件的网络传输和复制。客户端读取本地图片文件,通过UDP数据报发送给服务端,服务端接收数据并写入新文件。这个过程中有几个关键点值得注意:
第二个案例则实现了一个基于UDP的双机聊天程序。与第一个案例不同,这里使用了多线程技术:
这种架构使得收发消息可以同时进行,实现了真正的实时聊天功能。
服务端代码的核心逻辑可以分为以下几个步骤:
c复制int dest_fd = open("newfile.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);
这里使用open系统调用创建目标文件,注意使用了O_TRUNC标志,这意味着如果文件已存在会被清空。权限模式0666表示所有用户都有读写权限。
c复制int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
bind(udpfd, (SA)&ser, sizeof(ser));
关键点说明:
c复制while (1) {
char buf[4096] = {0};
int bytes = recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
if (bytes == 0) {
// 文件传输结束处理
break;
}
write(dest_fd, buf, bytes);
sendto(udpfd, "copying...", strlen("copying..."), 0, (SA)&cli, len);
}
这个循环持续接收客户端发来的数据并写入文件,每次接收后都会发送确认响应。当收到0字节的数据包时,认为传输结束。
客户端代码的主要流程如下:
c复制int src_fd = open("CSDN.png", O_RDONLY);
以只读方式打开待传输的文件,如果文件不存在会返回错误。
c复制struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
这里指定了服务器的IP和端口,注意使用inet_addr将点分十进制IP转换为网络字节序。
c复制while (1) {
char buf[4096];
int bytes = read(src_fd, buf, sizeof(buf));
sendto(udpfd, buf, bytes, 0, (SA)&ser, sizeof(ser));
if (bytes == 0) {
// 文件读取结束处理
break;
}
recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
}
循环读取文件内容并发送,每次发送后等待服务器确认。文件读取完毕后发送空包通知服务器。
UDP包大小限制:
UDP单次传输的最大理论大小为65535字节,但实际受MTU限制通常更小。本案例使用4096字节的缓冲区是安全的选择。
可靠性问题:
UDP不保证可靠传输,本案例通过简单的确认机制实现了基本可靠性:
提示:在实际项目中,可以考虑使用更完善的确认机制,如序列号、超时重传等,来增强UDP传输的可靠性。
聊天程序的服务端采用多线程架构:
c复制void* th1 (void*arg) {
while (1) {
recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
printf("from cli:%s",buf);
}
}
持续接收客户端消息并打印,遇到"#quit"命令时退出程序。
c复制void* th2 (void*arg) {
TH_ARG *tmp = (TH_ARG*)arg;
while (1) {
fgets(buf, sizeof(buf), stdin);
sendto(tmp->fd, buf, strlen(buf), 0, (SA)&tmp->cli, len);
}
}
从标准输入读取消息并发送给客户端,同样支持"#quit"退出命令。
客户端同样采用双线程设计:
c复制void* th1 (void*arg) {
while (1) {
recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
printf("from ser:%s", buf);
}
}
持续接收并打印服务器消息。
c复制void* th2(void *arg) {
while (1) {
fgets(buf, sizeof(buf), stdin);
sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, len);
}
}
从标准输入读取消息并发送给服务器。
注意:在多线程环境下使用标准I/O函数要小心竞争条件。本案例中因为收发线程分别使用不同的I/O通道(网络和终端),所以不会出现问题。
c复制// 错误
ser.sin_addr.s_addr = "127.0.0.1";
// 正确
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
c复制int buf_size = 1024 * 1024; // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
c复制struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
c复制// 加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
在实际项目中,我曾遇到过因为未设置接收超时导致程序挂起的问题。后来通过添加SO_RCVTIMEO选项解决了这个问题,这也提醒我们在网络编程中必须考虑所有可能的异常情况。