在嵌入式Linux网络编程中,UDP协议是实现快速、简单通信的关键。与TCP的复杂机制不同,UDP采用无连接方式,直接发送数据报。本文将深入解析UDP编程的每个环节,基于文档内容提供完整实现。
UDP核心概念与特性
UDP是用户数据报协议,位于TCP/IP模型的传输层。与TCP相比,UDP具有以下特点:
-
资源开销小,机制简单
-
无连接,无需建立和释放连接
-
传输不安全、不可靠,可能丢包、乱序
-
适合实时性要求高的场景
UDP通信完整流程
UDP通信采用简单的发送-接收模式:
cpp
发送端:socket() -> sendto() -> close()
接收端:socket() -> bind() -> recvfrom() -> close()
UDP包头结构分析
UDP包头固定8个字节,包含以下字段:
-
源端口:2字节,发送方端口号
-
目的端口:2字节,接收方端口号
-
长度:2字节,UDP头部和数据的总长度
-
校验和:2字节,用于错误检测
关键函数深度解析
socket函数
cpp
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
-
domain:AF_INET表示IPv4协议族
-
type:SOCK_DGRAM表示数据报套接字
-
protocol:0表示UDP通信
返回值:成功返回文件描述符,失败返回-1
sendto函数
cpp
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:向另一个套接字发送信息
参数:
-
sockfd:套接字文件描述符
-
buf:发送内容的首地址
-
len:发送数据的长度
-
flags:发送属性,默认0
-
dest_addr:目的地址结构体指针
-
addrlen:目的地址长度
返回值:成功返回发送字节数,失败返回-1
recvfrom函数
cpp
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:从套接字接收信息
参数:
-
sockfd:套接字文件描述符
-
buf:接收缓冲区首地址
-
len:缓冲区最大长度
-
flags:接收属性,默认0
-
src_addr:存放源地址的结构体指针
-
addrlen:源地址长度指针
返回值:成功返回接收字节数,失败返回-1
字节序转换函数
网络通信使用大端字节序,本地主机可能使用小端字节序,需要进行转换:
cpp
uint32_t htonl(uint32_t hostlong); // 主机到网络长整型
uint16_t htons(uint16_t hostshort); // 主机到网络短整型
uint32_t ntohl(uint32_t netlong); // 网络到主机长整型
uint16_t ntohs(uint16_t netshort); // 网络到主机短整型
-
h:host本地
-
n:net网络
-
l:long长整型,用于IP地址
-
s:short短整型,用于端口号
IP地址转换函数
cpp
in_addr_t inet_addr(const char *cp); // 字符串IP转32位IP
char *inet_ntoa(struct in_addr in); // 32位IP转字符串IP
int inet_pton(int af, const char *src, void *dst); // 字符串IP转二进制
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 二进制IP转字符串
bind函数详解
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将地址与套接字绑定
参数:
-
sockfd:套接字文件描述符
-
addr:要绑定的地址信息
-
addrlen:地址长度
返回值:成功返回0,失败返回-1
注意事项:
-
只能绑定自己的IP地址
-
端口号不能重复绑定
-
服务器端必须调用bind,客户端通常不调用
UDP服务器端完整实现
cpp
#include "head.h"
int main(void)
{
int fp = 0;
int sockfd = 0;
int ret = 0;
ssize_t nret = 0;
struct sockaddr_in recvaddr;
struct sockaddr_in sendaddr;
char tmpbuff[256] = {0};
socklen_t addrlen = sizeof(sendaddr);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
recvaddr.sin_family = AF_INET;
recvaddr.sin_port = htons(50000);
recvaddr.sin_addr.s_addr = inet_addr("192.168.0.167");
ret = bind(sockfd, (struct sockaddr *)&recvaddr, sizeof(recvaddr));
if(-1 == ret)
{
perror("fail to bind");
return -1;
}
nret = recvfrom(sockfd, tmpbuff, sizeof(tmpbuff), 0, (struct sockaddr *)&sendaddr, &addrlen);
if(-1 == nret)
{
perror("fail to recvfrom");
return -1;
}
fp = open(tmpbuff, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (-1 == fp)
{
perror("fail to open");
return -1;
}
while(1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recvfrom(sockfd, tmpbuff, sizeof(tmpbuff), 0, NULL, NULL);
if(-1 == nret)
{
perror("fail to recvfrom");
return -1;
}
if (0 == strcmp(tmpbuff, "____QUIT____"))
{
break;
}
write(fp, tmpbuff, nret);
}
close(sockfd);
close(fp);
printf("接受成功\n");
return 0;
}
UDP客户端完整实现
cpp
#include "head.h"
int main(void)
{
int sockfd = 0;
int fd = 0;
char filename[256] = {0};
char tmpbuff[1300] = {0};
ssize_t nret = 0;
struct sockaddr_in recvaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
printf("请输入要发送文件:\n");
gets(filename);
recvaddr.sin_family = AF_INET;
recvaddr.sin_port = htons(50000);
recvaddr.sin_addr.s_addr = inet_addr("192.168.0.169");
nret = sendto(sockfd, filename, strlen(filename), 0, (struct sockaddr *)&recvaddr, sizeof(recvaddr));
if (-1 == nret)
{
perror("fail to sendto");
return -1;
}
fd = open(filename, O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
nret = read(fd, tmpbuff, sizeof(tmpbuff));
if (nret <= 0)
{
break;
}
nret = sendto(sockfd, tmpbuff, nret, 0, (struct sockaddr *)&recvaddr, sizeof(recvaddr));
if (-1 == nret)
{
perror("fail to sendto");
return -1;
}
usleep(10000);
}
close(fd);
sprintf(tmpbuff, "____QUIT____");
nret = sendto(sockfd, tmpbuff, strlen(tmpbuff), 0, (struct sockaddr *)&recvaddr, sizeof(recvaddr));
if (-1 == nret)
{
perror("fail to sendto");
return -1;
}
close(sockfd);
return 0;
}
结构体sockaddr_in详解
cpp
struct sockaddr_in {
sa_family_t sin_family; // 地址族,AF_INET
in_port_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 填充
};
struct in_addr {
in_addr_t s_addr; // 32位IP地址
};
UDP文件传输实现
基于文档中的练习要求,实现UDP文件传输:
cpp
// 发送端发送文件关键代码
FILE *fp = fopen("test.jpg", "rb");
char buffer[1024];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
sendto(sockfd, buffer, bytes_read, 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
usleep(1000); // 避免发送过快
}
fclose(fp);
// 接收端接收文件关键代码
FILE *fp = fopen("received.jpg", "wb");
while (1) {
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
if (n <= 0) break;
fwrite(buffer, 1, n, fp);
}
fclose(fp);
网络配置与调试
虚拟机网络模式
-
桥接模式
-
Ubuntu与Windows网络独立
-
可作为局域网服务器
-
服务器端必须使用桥接模式
-
-
NAT模式
-
Ubuntu网络依赖Windows
-
无法作为局域网服务器
-
客户端优先选择NAT模式
-
网络配置命令
cpp
ifconfig # 查看网卡信息
ping 8.8.8.8 # 测试网络连通性
ping www.baidu.com # 测试域名解析
wireshark抓包工具
cpp
sudo apt-get install wireshark # 安装
sudo wireshark # 启动
常用过滤规则:
-
udp:只显示UDP数据包
-
udp.port == 50000:显示指定端口
-
ip.addr == 192.168.0.165:显示指定IP
UDP与TCP的关键区别
-
连接性
-
UDP:无连接,直接发送
-
TCP:面向连接,需三次握手
-
-
可靠性
-
UDP:不可靠,可能丢包乱序
-
TCP:可靠,有确认重传机制
-
-
复杂度
-
UDP:简单,包头8字节
-
TCP:复杂,包头至少20字节
-
-
适用场景
-
UDP:音视频流、DNS查询、实时游戏
-
TCP:文件传输、网页浏览、邮件
-
常见问题与解决方案
-
数据包丢失
-
实现应用层确认机制
-
添加序列号和重传逻辑
-
-
数据包乱序
-
添加序列号重新排序
-
设置合理超时时间
-
-
发送速度过快
-
添加流量控制
-
使用usleep控制发送间隔
-
-
缓冲区溢出
-
调整接收缓冲区大小
-
及时处理接收数据
-
总结
UDP协议以其简单高效的特点,在嵌入式Linux网络编程中占有重要地位。掌握socket、bind、sendto、recvfrom等核心函数,理解字节序转换和地址处理,是构建UDP应用的基础。
在实际开发中,应根据应用需求选择协议:对实时性要求高、可容忍少量丢包的选择UDP;对可靠性要求高的选择TCP。通过深入理解UDP的工作原理,可以更好地优化网络性能,构建高效稳定的嵌入式网络应用。