当前位置: 首页 > news >正文

【C++实战(63)】C++ 网络编程实战:UDP客户端与服务端的奥秘之旅

目录

  • 一、UDP 网络编程的基础概念
    • 1.1 UDP 协议的特点
    • 1.2 UDP 与 TCP 的适用场景对比
    • 1.3 UDP 编程的核心 API
  • 二、UDP 服务端的实现
    • 2.1 服务端初始化流程
    • 2.2 数据接收与客户端地址管理
    • 2.3 数据发送逻辑
  • 三、UDP 客户端的实现
    • 3.1 客户端初始化流程
    • 3.2 数据收发与可靠性优化
    • 3.3 UDP 客户端的异常处理
  • 四、实战项目:实时音视频传输客户端(UDP 基础版)
    • 4.1 项目需求
    • 4.2 UDP 数据报收发与超时重传实现核心功能
    • 4.3 传输延迟与丢包率测试(对比 TCP 传输)


一、UDP 网络编程的基础概念

1.1 UDP 协议的特点

UDP(User Datagram Protocol)即用户数据报协议,是一种无连接的传输层协议。UDP 最大的特点就是发送数据之前不需要在发送方和接收方之间建立连接,就像寄信一样,只要知道对方的地址(IP 地址和端口号)就可以直接把数据发送出去,没有三次握手的过程,减少了网络延迟,所以传输效率高,特别适合对实时性要求较高的应用场景,如实时视频流、音频流传输等。

UDP 是不可靠的传输协议,它不保证数据包的可靠传输。如果数据包在传输过程中丢失、重复或者乱序,UDP 协议本身不会进行任何补救措施,比如重传机制,也不会对数据包进行排序。这意味着应用程序需要自己处理这些问题,以确保数据的正确性和完整性,但在一些对数据完整性要求不高、能容忍少量丢包的场景中,如在线游戏中玩家位置信息的更新,偶尔丢失几个数据包对游戏的整体体验影响不大,UDP 的这种特性反而能发挥优势,减少传输开销,提高传输效率。

UDP 是面向数据报的协议,每个 UDP 数据报都是独立的,包含完整的源端口、目的端口、长度、校验和等信息,它不会将多个数据包合并成一个较大的数据报进行传输,也不会将一个大数据包拆分成多个小数据包进行传输。应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并。例如用 UDP 传输 100 个字节的数据,如果发送端调用一次 sendto 发送 100 个字节,那么接收端也必须调用对应的一次 recvfrom 接收 100 个字节,而不能循环调用 10 次 recvfrom 每次接收 10 个字节。

1.2 UDP 与 TCP 的适用场景对比

TCP(Transmission Control Protocol)是面向连接的、可靠的传输层协议,而 UDP 是无连接的、不可靠的传输层协议,它们的特性决定了各自不同的适用场景。

从实时性角度来看,UDP 因为不需要建立连接,没有复杂的确认和重传机制,所以数据可以快速地发送出去,延迟较低,适用于对实时性要求极高的场景。例如在实时视频会议中,参与者更希望看到流畅的画面和听到连续的声音,即使偶尔丢失一些数据帧,画面和声音出现短暂卡顿,也比因为等待重传数据而产生长时间延迟要好得多;在线游戏也是如此,玩家的操作指令需要及时传达给服务器,服务器返回的游戏状态信息也需要快速反馈给玩家,UDP 的低延迟特性可以满足这些需求,保证游戏的流畅性和交互性。

从可靠性角度出发,TCP 通过三次握手建立连接,传输过程中有确认应答、超时重传、流量控制和拥塞控制等机制,能确保数据的可靠传输,保证数据不会丢失、重复或乱序,适用于对数据准确性要求很高的场景。比如文件传输,如果文件在传输过程中出现数据丢失或错误,可能导致文件无法正常打开或使用,因此需要使用 TCP 协议来保证文件完整无误地传输;电子邮件的发送和接收也需要可靠性保障,否则邮件内容可能会不完整,影响信息的传达。

1.3 UDP 编程的核心 API

在 UDP 编程中,有几个核心的 API 函数,它们是实现 UDP 通信的基础。

  • socket 函数:用于创建一个套接字,是 UDP 编程的第一步。它的函数原型为int socket(int domain, int type, int protocol);,其中domain指定协议族,如AF_INET表示 IPv4 协议;type指定套接字类型,对于 UDP,通常使用SOCK_DGRAM,表示无连接、不可靠的数据报服务;protocol一般设为 0,表示使用默认协议。例如int sockfd = socket(AF_INET, SOCK_DGRAM, 0);,执行成功后会返回一个文件描述符sockfd,后续的 UDP 操作都将基于这个描述符进行。
  • bind 函数:将套接字绑定到一个特定的地址和端口上。函数原型是int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,sockfd是之前通过socket函数创建的套接字描述符;addr是一个指向struct sockaddr结构体的指针,该结构体包含了要绑定的 IP 地址和端口号等信息,不过在实际使用中,通常会使用struct sockaddr_in结构体来设置地址信息,然后将其强制转换为struct sockaddr类型传递给bind函数;addrlen是addr结构体的长度。对于 UDP 服务器,绑定地址和端口是必要的,这样客户端才能知道往哪里发送数据。例如:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网络接口
servaddr.sin_port = htons(SERVER_PORT); // SERVER_PORT为自定义的服务器端口号
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  • sendto 函数:用于向指定的地址发送 UDP 数据报。函数原型为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是指向目标地址(包含目标 IP 地址和端口号)的结构体指针;addrlen是目标地址结构体的长度。例如:
char sendbuf[BUFFER_SIZE]; // BUFFER_SIZE为自定义的缓冲区大小
memset(sendbuf, 0, sizeof(sendbuf));
strcpy(sendbuf, "Hello, UDP Server!");
struct sockaddr_in cliaddr;
memset(&cliaddr, 0, sizeof(cliaddr));
cliaddr.sin_family = AF_INET;
cliaddr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 目标IP地址
cliaddr.sin_port = htons(CLIENT_PORT); // 目标端口号
sendto(sockfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
  • recvfrom 函数:用于从 UDP 套接字接收数据报,并获取发送方的地址信息。函数原型为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是一个指向src_addr结构体长度的指针,传入时为该结构体的初始长度,返回时为实际接收到的地址长度。例如:
char recvbuf[BUFFER_SIZE];
memset(recvbuf, 0, sizeof(recvbuf));
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &clilen);

通过这些核心 API 函数的组合使用,就可以实现 UDP 客户端和服务器之间的数据通信。

二、UDP 服务端的实现

2.1 服务端初始化流程

UDP 服务端的初始化是建立通信的基础,主要包括创建 socket 和绑定地址这两个关键步骤。

创建 socket 是 UDP 服务端编程的第一步,通过调用socket函数来完成。在 C++ 中,代码如下:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>const int SERVER_PORT = 8888;
const int BUFFER_SIZE = 1024;int main() {// 创建UDP socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {std::cerr << "Socket creation failed" << std::endl;return -1;}

上述代码中,socket函数的第一个参数AF_INET指定使用 IPv4 协议,第二个参数SOCK_DGRAM表示创建的是 UDP 套接字,第三个参数 0 表示使用默认协议。如果创建成功,sockfd将是一个有效的文件描述符,后续的 UDP 操作都将基于此进行;若创建失败,sockfd的值为 - 1 ,并通过std::cerr输出错误信息。

创建 socket 后,需要将其绑定到一个特定的地址和端口,以便接收客户端发送的数据。绑定地址使用bind函数,示例代码如下:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网络接口
servaddr.sin_port = htons(SERVER_PORT); // 绑定到指定端口if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {std::cerr << "Bind failed" << std::endl;close(sockfd);return -1;
}

在这段代码中,首先定义了一个struct sockaddr_in类型的变量servaddr,用于存储服务器的地址信息。通过memset函数将其初始化为 0 ,然后设置servaddr的各个字段:sin_family设置为AF_INET,表示 IPv4 地址族;sin_addr.s_addr设置为htonl(INADDR_ANY),htonl函数将主机字节序转换为网络字节序,INADDR_ANY表示绑定到服务器的所有网络接口,这样服务器可以接收来自任何网络接口的数据包;sin_port设置为htons(SERVER_PORT),将自定义的服务器端口号SERVER_PORT从主机字节序转换为网络字节序。最后调用bind函数将 socket 绑定到指定的地址和端口,如果绑定失败,bind函数返回 - 1 ,输出错误信息,并关闭 socket。

2.2 数据接收与客户端地址管理

完成初始化后,UDP 服务端就可以接收客户端发送的数据了,同时还需要管理客户端的地址信息,以便后续与客户端进行通信。

使用recvfrom函数来接收数据,示例代码如下:

char recvbuf[BUFFER_SIZE];
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);while (true) {memset(recvbuf, 0, sizeof(recvbuf));ssize_t n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &clilen);if (n == -1) {std::cerr << "Receive failed" << std::endl;continue;}recvbuf[n] = '\0';std::cout << "Received from client: " << recvbuf << std::endl;

在上述代码中,定义了一个接收缓冲区recvbuf,用于存储接收到的数据。还定义了一个struct sockaddr_in类型的变量cliaddr,用于存储客户端的地址信息,以及一个socklen_t类型的变量clilen,用于记录cliaddr的长度。在一个无限循环中,调用recvfrom函数接收数据。recvfrom函数的第一个参数是 socket 描述符sockfd;第二个参数是接收缓冲区recvbuf;第三个参数是缓冲区的大小sizeof(recvbuf);第四个参数一般设为 0 ,表示默认选项;第五个参数是指向存储客户端地址信息的结构体指针(struct sockaddr *)&cliaddr;第六个参数是指向存储客户端地址长度的指针&clilen。如果接收成功,n将返回接收到的数据长度,将接收到的数据末尾加上字符串结束符’\0’,然后输出接收到的数据。若接收失败,n的值为 - 1 ,输出错误信息,并继续下一次循环。

为了管理客户端地址,通常可以使用一个容器(如std::vector或std::unordered_map)来记录客户端的地址信息。例如,使用std::vector记录客户端地址列表:

#include <vector>std::vector<struct sockaddr_in> client_list;while (true) {// 接收数据代码...bool is_new_client = true;for (auto& client : client_list) {if (memcmp(&client, &cliaddr, sizeof(cliaddr)) == 0) {is_new_client = false;break;}}if (is_new_client) {client_list.push_back(cliaddr);std::cout << "New client connected" << std::endl;}

在这段代码中,定义了一个std::vector<struct sockaddr_in>类型的变量client_list,用于存储客户端地址列表。在每次接收到数据后,遍历client_list,通过memcmp函数比较当前接收到的客户端地址cliaddr与列表中已有的客户端地址是否相同。如果相同,表示该客户端已经在列表中,将is_new_client设为false;如果遍历完列表都没有找到相同的地址,表示是新的客户端,将is_new_client设为true,并将该客户端地址添加到client_list中,同时输出提示信息。

2.3 数据发送逻辑

UDP 服务端不仅要接收数据,还需要向客户端发送数据,包括广播和组播等方式。
广播是将数据发送到网络中的所有设备,在 UDP 中,实现广播需要设置 socket 的选项,并指定广播地址。示例代码如下:

// 设置socket为广播模式
int broadcast = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) == -1) {std::cerr << "Set broadcast option failed" << std::endl;close(sockfd);return -1;
}char sendbuf[BUFFER_SIZE] = "Broadcast message";
struct sockaddr_in broadcast_addr;
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_addr.s_addr = inet_addr("255.255.255.255"); // 广播地址
broadcast_addr.sin_port = htons(SERVER_PORT);if (sendto(sockfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr)) == -1) {std::cerr << "Broadcast send failed" << std::endl;
}

上述代码中,首先通过setsockopt函数设置 socket 的SO_BROADCAST选项,使其支持广播功能。setsockopt函数的第一个参数是 socket 描述符sockfd;第二个参数SOL_SOCKET表示设置 socket 层的选项;第三个参数SO_BROADCAST表示设置为广播模式;第四个参数是指向要设置的值的指针&broadcast;第五个参数是要设置的值的长度sizeof(broadcast)。如果设置失败,输出错误信息,并关闭 socket。然后定义了一个广播地址255.255.255.255,将数据通过sendto函数发送到该广播地址。

组播是将数据发送到一组特定的设备,这些设备组成一个组播组。实现组播需要加入组播组,并设置相应的 IP 地址和端口。示例代码如下:

struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 组播地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本地网络接口if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {std::cerr << "Join multicast group failed" << std::endl;close(sockfd);return -1;
}char multicast_sendbuf[BUFFER_SIZE] = "Multicast message";
struct sockaddr_in multicast_addr;
memset(&multicast_addr, 0, sizeof(multicast_addr));
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_addr.s_addr = inet_addr("224.0.0.1"); // 组播地址
multicast_addr.sin_port = htons(SERVER_PORT);if (sendto(sockfd, multicast_sendbuf, strlen(multicast_sendbuf), 0, (struct sockaddr *)&multicast_addr, sizeof(multicast_addr)) == -1) {std::cerr << "Multicast send failed" << std::endl;
}

在这段代码中,首先定义了一个struct ip_mreq类型的变量mreq,用于设置组播相关的参数。mreq.imr_multiaddr.s_addr设置为组播地址224.0.0.1,mreq.imr_interface.s_addr设置为htonl(INADDR_ANY),表示使用本地任意网络接口。通过setsockopt函数加入组播组,setsockopt函数的第一个参数是 socket 描述符sockfd;第二个参数IPPROTO_IP表示设置 IP 层的选项;第三个参数IP_ADD_MEMBERSHIP表示加入组播组;第四个参数是指向设置参数的指针&mreq;第五个参数是设置参数的长度sizeof(mreq)。如果加入组播组失败,输出错误信息,并关闭 socket。然后定义了要发送的组播数据和组播地址,通过sendto函数将数据发送到组播地址。

三、UDP 客户端的实现

3.1 客户端初始化流程

UDP 客户端的初始化同样是建立通信的重要基础,主要操作包括创建 socket 以及指定服务端地址。在 C++ 中,利用系统提供的网络编程接口来完成这些操作。

创建 socket 是 UDP 客户端与服务端进行通信的第一步,通过调用系统函数socket来实现。其函数原型为int socket(int domain, int type, int protocol);,对于 UDP 客户端,domain一般设置为AF_INET,表示使用 IPv4 协议;type设置为SOCK_DGRAM,代表这是一个无连接的数据报套接字;protocol通常设为 0,采用默认协议。示例代码如下:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>const int SERVER_PORT = 8888;
const int BUFFER_SIZE = 1024;
const char* SERVER_IP = "192.168.1.100";int main() {// 创建UDP socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {std::cerr << "Socket creation failed" << std::endl;return -1;}

上述代码尝试创建一个 UDP 套接字,若创建失败,sockfd会被赋值为 - 1,同时通过std::cerr输出错误信息并结束程序。

创建 socket 后,需要指定服务端地址,以便后续向服务端发送数据。服务端地址信息存储在struct sockaddr_in结构体中,该结构体包含地址族、IP 地址和端口号等成员。设置服务端地址的示例代码如下:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);
servaddr.sin_port = htons(SERVER_PORT);

这里首先使用memset函数将servaddr结构体清零,然后设置sin_family为AF_INET,表示 IPv4 地址族;通过inet_addr函数将服务端 IP 地址字符串转换为网络字节序的二进制形式,并赋值给sin_addr.s_addr;使用htons函数将服务端端口号从主机字节序转换为网络字节序,赋值给sin_port。经过这样的设置,servaddr结构体就包含了正确的服务端地址信息,为后续的数据发送做好了准备。

3.2 数据收发与可靠性优化

UDP 客户端在完成初始化后,便可以进行数据的收发操作。然而,由于 UDP 本身的不可靠性,在实际应用中常常需要对数据传输进行可靠性优化,如采用超时重传和校验和等方法。

数据发送通过sendto函数实现,其函数原型为ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);,示例代码如下:

char sendbuf[BUFFER_SIZE];
memset(sendbuf, 0, sizeof(sendbuf));
strcpy(sendbuf, "Hello, UDP Server!");
ssize_t n = sendto(sockfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1) {std::cerr << "Send failed" << std::endl;
}

上述代码中,先定义一个发送缓冲区sendbuf,填充要发送的数据,然后调用sendto函数将数据发送给服务端。若发送失败,n的值为 - 1 ,通过std::cerr输出错误信息。

数据接收使用recvfrom函数,函数原型为ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);,示例代码如下:

char recvbuf[BUFFER_SIZE];
memset(recvbuf, 0, sizeof(recvbuf));
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &clilen);
if (n == -1) {std::cerr << "Receive failed" << std::endl;
} else {recvbuf[n] = '\0';std::cout << "Received from server: " << recvbuf << std::endl;
}

这里定义了接收缓冲区recvbuf,调用recvfrom函数接收服务端返回的数据。如果接收失败,n为 - 1 ,输出错误信息;接收成功则将接收到的数据末尾加上字符串结束符’\0’,并输出接收到的数据。

UDP 是不可靠的传输协议,为了提高数据传输的可靠性,可以采用超时重传机制。其实现思路是:在发送数据时启动一个定时器,若在规定时间内没有收到对方的确认信息,则重新发送数据。例如,可以使用alarm函数和信号处理机制来实现简单的超时重传,代码框架如下:

#include <signal.h>
#include <setjmp.h>static sigjmp_buf jmpbuf;static void sig_alrm(int signo) {siglongjmp(jmpbuf, 1);
}// 发送数据并设置超时重传
void send_with_timeout(int sockfd, const char *buf, size_t len, const struct sockaddr *dest_addr, socklen_t addrlen, int timeout) {if (sigsetjmp(jmpbuf, 1) != 0) {// 超时处理,重发数据std::cerr << "Timeout, resend data" << std::endl;sendto(sockfd, buf, len, 0, dest_addr, addrlen);alarm(timeout);} else {sendto(sockfd, buf, len, 0, dest_addr, addrlen);alarm(timeout);}
}

在上述代码中,sig_alrm函数是信号处理函数,当定时器超时触发SIGALRM信号时,会调用该函数,通过siglongjmp跳转到sigsetjmp设置的位置,从而实现超时重传。send_with_timeout函数则封装了发送数据和设置超时的逻辑。

校验和是另一种提高数据可靠性的方法,它通过对数据进行计算得到一个校验值,接收方接收到数据后重新计算校验值并与发送方的校验值进行比较,若一致则说明数据在传输过程中没有被篡改或损坏。在 UDP 协议中,本身就包含了一个校验和字段,在发送数据时,可以使用ntohs和htons等函数来计算和设置校验和,接收数据时进行校验和验证,示例代码如下:

// 计算校验和
unsigned short calculate_checksum(const void *buf, size_t len) {unsigned long sum = 0;const unsigned short *ptr = (const unsigned short *)buf;while (len > 1) {sum += *ptr++;len -= 2;}if (len > 0) {sum += *((const unsigned char *)ptr);}while (sum >> 16) {sum = (sum & 0xFFFF) + (sum >> 16);}return ~sum;
}// 发送数据并计算校验和
void send_with_checksum(int sockfd, const char *buf, size_t len, const struct sockaddr *dest_addr, socklen_t addrlen) {// 假设数据结构中包含校验和字段,这里为简单示例,实际应用中需根据数据结构调整struct {unsigned short checksum;char data[BUFFER_SIZE];} send_data;memcpy(send_data.data, buf, len);send_data.checksum = calculate_checksum(send_data.data, len);sendto(sockfd, &send_data, sizeof(send_data), 0, dest_addr, addrlen);
}// 接收数据并验证校验和
void recv_with_checksum(int sockfd, char *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen) {struct {unsigned short checksum;char data[BUFFER_SIZE];} recv_data;ssize_t n = recvfrom(sockfd, &recv_data, sizeof(recv_data), 0, src_addr, addrlen);if (n == -1) {std::cerr << "Receive failed" << std::endl;return;}unsigned short received_checksum = recv_data.checksum;recv_data.checksum = 0;unsigned short calculated_checksum = calculate_checksum(recv_data.data, n - sizeof(unsigned short));if (received_checksum != calculated_checksum) {std::cerr << "Checksum verification failed" << std::endl;} else {memcpy(buf, recv_data.data, n - sizeof(unsigned short));buf[n - sizeof(unsigned short)] = '\0';std::cout << "Received from server: " << buf << std::endl;}
}

上述代码中,calculate_checksum函数用于计算数据的校验和,send_with_checksum函数在发送数据前计算校验和并将其添加到数据结构中一起发送,recv_with_checksum函数接收数据后验证校验和,若校验和不一致则输出错误信息,一致则将数据存储到接收缓冲区并输出。

3.3 UDP 客户端的异常处理

在 UDP 客户端的运行过程中,可能会遇到各种异常情况,如数据丢失、网络故障等,需要进行相应的处理,以保证程序的稳定性和可靠性。

数据丢失是 UDP 通信中常见的问题,由于 UDP 本身不保证数据的可靠传输,数据包在传输过程中可能会丢失。为了检测数据丢失,可以采用序列号机制,为每个发送的数据包分配一个唯一的序列号,接收方通过检查序列号的连续性来判断是否有数据包丢失。示例代码如下:

#include <vector>// 假设已定义sockfd、servaddr等相关变量
std::vector<int> received_seq;// 发送数据,假设data为要发送的数据,seq为当前序列号
void send_data_with_seq(int sockfd, const char *data, size_t len, const struct sockaddr *servaddr, socklen_t addrlen, int seq) {struct {int seq;char data[BUFFER_SIZE];} send_packet;send_packet.seq = seq;memcpy(send_packet.data, data, len);sendto(sockfd, &send_packet, sizeof(send_packet), 0, servaddr, addrlen);
}// 接收数据并检测丢包
void recv_data_and_check_loss(int sockfd, struct sockaddr *src_addr, socklen_t *addrlen) {struct {int seq;char data[BUFFER_SIZE];} recv_packet;ssize_t n = recvfrom(sockfd, &recv_packet, sizeof(recv_packet), 0, src_addr, addrlen);if (n == -1) {std::cerr << "Receive failed" << std::endl;return;}int received_seq_num = recv_packet.seq;if (received_seq.empty()) {received_seq.push_back(received_seq_num);} else {int last_seq = received_seq.back();if (received_seq_num != last_seq + 1) {std::cout << "Possible packet loss, expected seq: " << last_seq + 1 << ", received seq: " << received_seq_num << std::endl;}received_seq.push_back(received_seq_num);}// 处理接收到的数据,这里省略具体处理逻辑
}

在上述代码中,send_data_with_seq函数在发送数据时将序列号一起封装在数据包中发送,recv_data_and_check_loss函数接收数据后,通过比较当前接收到的序列号与上一个序列号是否连续来检测是否可能存在数据丢失。

当网络出现故障时,如连接中断、服务器不可达等,sendto和recvfrom函数可能会返回错误。可以通过检查函数的返回值来判断是否发生错误,并进行相应的处理。例如,在发送数据时:

ssize_t n = sendto(sockfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1) {int err = errno;if (err == ECONNREFUSED) {std::cerr << "Connection refused, server may be down" << std::endl;} else if (err == EHOSTUNREACH) {std::cerr << "Host unreachable, network may be down" << std::endl;} else {std::cerr << "Send failed with error: " << strerror(err) << std::endl;}
}

这里根据sendto函数返回的错误码errno进行判断,若错误码为ECONNREFUSED,表示连接被拒绝,可能是服务器未启动或端口错误;若为EHOSTUNREACH,表示目标主机不可达,可能是网络故障;其他错误则通过strerror函数输出具体的错误信息。在接收数据时,同样可以通过检查recvfrom函数的返回值和errno来处理类似的网络故障情况。

四、实战项目:实时音视频传输客户端(UDP 基础版)

4.1 项目需求

在当今数字化时代,实时音视频传输的需求日益增长,无论是视频会议、在线直播还是远程教学等场景,都对音视频传输的实时性和稳定性提出了较高要求。本实战项目旨在开发一个基于 UDP 的实时音视频传输客户端,以满足以下关键需求:

  • 实时传输音视频数据:能够快速地将本地采集的音视频数据传输到目标服务器或其他客户端,确保接收端能够及时播放,实现实时互动。例如在视频会议中,参会者的发言和画面需要立即被其他参会者接收,以保证会议的流畅进行。
  • 低延迟:尽量减少音视频数据从发送端到接收端的传输延迟,避免出现卡顿和延迟过高导致的用户体验下降。通常,对于实时音视频传输,延迟应控制在几百毫秒以内,以提供接近实时的交互体验。
  • 简单丢包处理:由于 UDP 本身是不可靠传输协议,在传输过程中可能会出现丢包情况。因此,需要实现简单的丢包处理机制,以提高传输的可靠性。例如,可以采用序列号机制来检测丢包,对于丢失的数据包进行适当的处理,如请求重发或采用前向纠错算法进行补偿 ,以保证音视频播放的连贯性,减少因丢包导致的画面卡顿或声音中断现象。

4.2 UDP 数据报收发与超时重传实现核心功能

为了实现实时音视频传输客户端的核心功能,需要利用 UDP 进行数据报的收发,并实现超时重传机制。以下是关键代码实现:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
#include <signal.h>
#include <setjmp.h>const int BUFFER_SIZE = 1024;
const int SERVER_PORT = 8888;
const char* SERVER_IP = "192.168.1.100";
static sigjmp_buf jmpbuf;// 信号处理函数,用于超时重传
static void sig_alrm(int signo) {siglongjmp(jmpbuf, 1);
}// 发送数据并设置超时重传
void send_with_timeout(int sockfd, const char *buf, size_t len, const struct sockaddr *dest_addr, socklen_t addrlen, int timeout) {if (sigsetjmp(jmpbuf, 1) != 0) {// 超时处理,重发数据std::cerr << "Timeout, resend data" << std::endl;sendto(sockfd, buf, len, 0, dest_addr, addrlen);alarm(timeout);} else {sendto(sockfd, buf, len, 0, dest_addr, addrlen);alarm(timeout);}
}int main() {// 创建UDP socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {std::cerr << "Socket creation failed" << std::endl;return -1;}struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);servaddr.sin_port = htons(SERVER_PORT);// 假设这里是从音视频采集设备获取的数据char video_data[BUFFER_SIZE];memset(video_data, 0, sizeof(video_data));// 模拟填充音视频数据strcpy(video_data, "Sample video data"); // 发送数据并设置超时为5秒send_with_timeout(sockfd, video_data, strlen(video_data), (struct sockaddr *)&servaddr, sizeof(servaddr), 5);char recvbuf[BUFFER_SIZE];memset(recvbuf, 0, sizeof(recvbuf));struct sockaddr_in cliaddr;socklen_t clilen = sizeof(cliaddr);// 接收服务器返回的数据ssize_t n = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &clilen);if (n == -1) {std::cerr << "Receive failed" << std::endl;} else {recvbuf[n] = '\0';std::cout << "Received from server: " << recvbuf << std::endl;}close(sockfd);return 0;
}

在上述代码中,send_with_timeout函数负责发送数据并设置超时重传。通过sigsetjmp和siglongjmp配合信号处理函数sig_alrm实现超时重传逻辑。如果在设定的超时时间内没有收到服务器的响应,sig_alrm函数被触发,通过siglongjmp跳转到sigsetjmp设置的位置,执行重发数据操作。主函数中创建 UDP socket,设置服务器地址,然后模拟发送音视频数据,并接收服务器返回的数据。

4.3 传输延迟与丢包率测试(对比 TCP 传输)

为了评估基于 UDP 的实时音视频传输客户端的性能,进行传输延迟与丢包率测试,并与 TCP 传输进行对比。测试环境设置如下:发送端和接收端通过局域网连接,网络带宽充足,模拟不同的网络负载情况。

对于 UDP 传输延迟测试,可以在发送数据时记录发送时间戳,在接收端记录接收时间戳,通过两者差值计算传输延迟。丢包率测试则通过为每个数据包分配序列号,接收端统计丢失的数据包数量来计算丢包率。

对于 TCP 传输,同样进行类似的测试。使用如下简单的 Python 脚本进行测试:

import socket
import time# UDP测试
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server_addr = ('192.168.1.100', 8888)
total_udp_packets = 1000
lost_udp_packets = 0
udp_total_delay = 0for i in range(total_udp_packets):data = f"UDP Packet {i}".encode()send_time = time.time()udp_sock.sendto(data, udp_server_addr)try:udp_sock.settimeout(1)  # 设置接收超时为1秒_, addr = udp_sock.recvfrom(1024)recv_time = time.time()udp_total_delay += recv_time - send_timeexcept socket.timeout:lost_udp_packets += 1udp_sock.close()
udp_loss_rate = lost_udp_packets / total_udp_packets * 100
udp_avg_delay = udp_total_delay / (total_udp_packets - lost_udp_packets) if total_udp_packets - lost_udp_packets > 0 else 0# TCP测试
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server_addr = ('192.168.1.100', 8889)
tcp_sock.connect(tcp_server_addr)
total_tcp_packets = 1000
lost_tcp_packets = 0
tcp_total_delay = 0for i in range(total_tcp_packets):data = f"TCP Packet {i}".encode()send_time = time.time()tcp_sock.sendall(data)try:tcp_sock.settimeout(1)response = tcp_sock.recv(1024)recv_time = time.time()tcp_total_delay += recv_time - send_timeexcept socket.timeout:lost_tcp_packets += 1tcp_sock.close()
tcp_loss_rate = lost_tcp_packets / total_tcp_packets * 100
tcp_avg_delay = tcp_total_delay / (total_tcp_packets - lost_tcp_packets) if total_tcp_packets - lost_tcp_packets > 0 else 0print(f"UDP丢包率: {udp_loss_rate}%")
print(f"UDP平均延迟: {udp_avg_delay}秒")
print(f"TCP丢包率: {tcp_loss_rate}%")
print(f"TCP平均延迟: {tcp_avg_delay}秒")

测试结果表明,在网络状况良好时,UDP 和 TCP 的丢包率都较低,但 UDP 的平均传输延迟明显低于 TCP,因为 UDP 无需建立连接,数据可以快速发送。然而,当网络出现拥塞时,UDP 的丢包率会显著上升,而 TCP 由于其可靠传输机制,丢包率相对稳定,但传输延迟会大幅增加,因为 TCP 需要进行重传和拥塞控制等操作。这说明 UDP 在实时性要求高、能容忍一定丢包的实时音视频传输场景中具有优势,而 TCP 更适合对数据可靠性要求极高、对延迟不太敏感的场景,如文件传输。

http://www.dtcms.com/a/435482.html

相关文章:

  • [数据分享第八弹]历史人文相关地理数据
  • 河南省建设厅网站中州杯团购小程序
  • 同城可以做别人一样的门户网站吗网址导航总是自动在桌面
  • C语言——循环的嵌套小练习
  • 阿里云买了域名怎么建网站wordpress电源模板
  • 深入解析 Roo Code:提示词的技术结构与工作原理
  • 旅游网站系统建设方案wordpress页面缓慢
  • 杭州软件网站建设广东深圳职业技术学校
  • 树的遍历方式总结
  • excel做网站链接网站流量一直下降
  • VS2017 安装 .NET Core 2.2 SDK 教程(包括 dotnet-sdk-2.2.108-win-x64.exe 安装步骤)​
  • 模拟算法
  • 网站建设需要收集资料吗网络营销中的四种方法
  • 能源经济选题推荐:绿色电网
  • 展览展示搭建设计晋中网站seo
  • 如何让做树洞网站郑州搜索引擎优化公司
  • 网站制作推广电话阿里云安装wordpress
  • 做网站设计电脑买什么高端本好潜江资讯网手机
  • 教育建设网站网络营销课程总结1000字
  • 在百度里面做个网站怎么做wordpress go链接跳转错误
  • h5网站建设建站刚刚做的网站怎么排名
  • 信用中国 网站 支持建设形象设计
  • MySQL 和 Redis 偏移量分页在数据增删场景下的问题与解决方案
  • 赚钱的十大个人网站场口一站式建站哪家公司好
  • 罗源福州网站建设成都 网站建设公司哪家好
  • 雅联网站建设网站怎么被收录
  • 网站公司怎么做运营商做网站和软件哪个挣钱
  • DeepSeek 最新推出 ‌EX 模型
  • C++之二叉树进阶
  • 重庆网站设计案例没有网站怎么做CPC