前言
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的、不可靠的、面向数据报的传输层协议。它广泛应用于需要高实时性且对数据传输可靠性要求不高的场景,如实时音视频传输、在线游戏等。本文将详细介绍UDP通讯协议的原理、各关键函数的作用及其实现实例,并最后进行总结。
- 前言
- UDP协议原理
- 无连接性
- 不可靠性
- 面向数据报
- 相应函数
- socket()
- bind()
- sendto()
- recvfrom()
- 示例
- 字符串整体发送示例
- 服务端
- 客户端
- 运行结果
- 字符串分包发送示例
- 服务端
- 客户端
- 运行结果
- 总结
UDP协议原理
无连接性
UDP是无连接的协议,通信双方在传输数据之前不需要建立连接,也不需要维护连接状态。这种特性使得UDP在处理实时数据和快速数据传输时非常高效。
不可靠性
UDP不提供数据包的确认和重传机制,也不保证数据包的顺序性,因此数据传输过程中可能会出现丢包、重复或乱序等情况。UDP的设计目标之一就是降低通信的开销,以支持高吞吐量和低延迟的通信需求。
面向数据报
UDP以数据报为基本单位进行通信,每个数据报是一个独立的、完整的消息,具有独立的头部和数据部分。UDP数据报格式包括源端口号、目的端口号、长度和校验和等字段。
-源端口号(16 bits):标识发送端的端口号。
-目的端口号(16 bits):标识接收端的端口号。
-长度(16 bits):表示UDP数据报的总长度,包括UDP头部和数据部分。
-校验和(16 bits):用于检测数据报的完整性,以确保数据在传输过程中没有被损坏。
-数据(可变长度):包含了应用程序要传输的实际数据。
相应函数
socket()
socket()函数用于创建一个套接字,它是网络通信的端点。对于UDP通信,socket()函数的第三个参数应设置为SOCK_DGRAM,表示创建的是数据报套接字。
int socket(int domain, int type, int protocol);
作用:创建一个新的套接字。
参数说明:
-domain:指定协议族,对于IPv4使用AF_INET。
-type:指定套接字类型,对于UDP使用SOCK_DGRAM。
-protocol:通常设置为0,自动选择协议。
bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:将套接字绑定到一个特定的地址和端口上。
参数说明:
-sockfd:由socket()返回的套接字描述符。
-addr:指向sockaddr结构的指针,包含地址和端口信息。
-addrlen:地址结构体的长度。
sendto()
函数原型:
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:指向目标地址的sockaddr结构指针。
-addrlen:目标地址结构体的长度。
recvfrom()
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:指向sockaddr结构的指针,用于存储发送方的地址信息。
-addrlen:地址结构体的长度指针。
示例
本实例在windows 11环境下的vs2019中运行,ubuntu环境中的头文件与此不同。但各函数功能基本一样。在同一台电脑中进行发送和接受,进行两个实例。
字符串整体发送示例
服务端
#include <iostream>
#include <cstring>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "12345" // 你可以根据需要更改这个端口号,但要确保它没有被其他应用占用
int main() {
WSADATA wsaData;
SOCKET sock = INVALID_SOCKET;
struct addrinfo hints, * res = NULL, * p = NULL;
int iResult;
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
struct sockaddr_storage their_addr;
socklen_t addr_size = sizeof(their_addr);
// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
std::cerr << "WSAStartup failed with error: " << iResult << std::endl;
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET; // 使用IPv4
hints.ai_socktype = SOCK_DGRAM; // UDP
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE; // 允许绑定到任意地址
// 解析监听地址和端口
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &res);
if (iResult != 0) {
std::cerr << "getaddrinfo failed with error: " << iResult << std::endl;
WSACleanup();
return 1;
}
// 创建套接字
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock == INVALID_SOCKET) {
std::cerr << "socket failed with error: " << WSAGetLastError() << std::endl;
freeaddrinfo(res);
WSACleanup();
return 1;
}
// 绑定套接字到地址和端口
iResult = bind(sock, res->ai_addr, (int)res->ai_addrlen);
if (iResult == SOCKET_ERROR) {
std::cerr << "bind failed with error: " << WSAGetLastError() << std::endl;
freeaddrinfo(res);
closesocket(sock);
WSACleanup();
return 1;
}
freeaddrinfo(res); // 不再需要地址信息
std::cout << "UDP Server listening on port " << DEFAULT_PORT << std::endl;
// 接收数据
while (true) {
iResult = recvfrom(sock, recvbuf, recvbuflen, 0,
(struct sockaddr*)&their_addr, &addr_size);
if (iResult == SOCKET_ERROR) {
std::cerr << "recvfrom failed with error: " << WSAGetLastError() << std::endl;
closesocket(sock);
WSACleanup();
return 1;
}
recvbuf[iResult] = '\0'; // 添加空字符以创建字符串
std::cout << "Received: " << recvbuf << std::endl;
}
// 注意:上面的while循环是无限循环,实际使用中你可能需要添加一些退出条件
// 清理(虽然在这个例子中永远不会到达)
closesocket(sock);
WSACleanup();
return 0;
}
客户端
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET sock = INVALID_SOCKET;
struct addrinfo hints, * res = NULL, * p = NULL;
int iResult;
char sendbuf[1024] = "Hello, UDP!";
int sendbuflen = strlen(sendbuf) + 1;
// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
std::cerr << "WSAStartup failed with error: " << iResult << std::endl;
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET; // 使用IPv4
hints.ai_socktype = SOCK_DGRAM; // UDP
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE; // 对于发送端,这通常不是必需的,但也没有害处
// 解析目标地址和端口
iResult = getaddrinfo("127.0.0.1", "12345", &hints, &res);
if (iResult != 0) {
std::cerr << "getaddrinfo failed with error: " << iResult << std::endl;
WSACleanup();
return 1;
}
// 创建套接字
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock == INVALID_SOCKET) {
std::cerr << "socket failed with error: " << WSAGetLastError() << std::endl;
freeaddrinfo(res);
WSACleanup();
return 1;
}
while (true)
{
// 发送数据
iResult = sendto(sock, sendbuf, sendbuflen, 0, res->ai_addr, (int)res->ai_addrlen);
std::cout << "Bytes Sent: " << iResult << std::endl;
std::cout << "(int)res->ai_addrlen: " << (int)res->ai_addrlen<< "res->ai_addr: " << res->ai_addr << std::endl;
Sleep(100);
}
if (iResult == SOCKET_ERROR) {
std::cerr << "sendto failed with error: " << WSAGetLastError() << std::endl;
closesocket(sock);
freeaddrinfo(res);
WSACleanup();
return 1;
}
std::cout << "Bytes Sent: " << iResult << std::endl;
// 清理
closesocket(sock);
freeaddrinfo(res);
WSACleanup();
return 0;
}
运行结果
字符串分包发送示例
服务端
#include <iostream>
#include <string>
#include <vector>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT "12345"
#define BUFFER_SIZE 6
int main() {
WSADATA wsaData;
int iResult;
SOCKET recvSocket = INVALID_SOCKET;
struct addrinfo* result = NULL, * ptr = NULL, hints;
char recvbuf[BUFFER_SIZE];
int recvbuflen = BUFFER_SIZE;
std::vector<std::string> packets;
// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
std::cerr << "WSAStartup failed with error: " << iResult << std::endl;
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
// 获取本地地址信息
iResult = getaddrinfo(NULL, PORT, &hints, &result);
if (iResult != 0) {
std::cerr << "getaddrinfo failed with error: " << iResult << std::endl;
WSACleanup();
return 1;
}
// 遍历地址信息列表
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// 创建套接字
recvSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (recvSocket == INVALID_SOCKET) {
std::cerr << "socket failed with error: " << WSAGetLastError() << std::endl;
continue;
}
// 绑定套接字到地址
iResult = bind(recvSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
std::cerr << "bind failed with error: " << WSAGetLastError() << std::endl;
closesocket(recvSocket);
continue;
}
break;
}
if (recvSocket == INVALID_SOCKET) {
std::cerr << "Unable to create socket for receiving." << std::endl;
freeaddrinfo(result);
WSACleanup();
return 1;
}
// 接收数据
sockaddr_storage their_addr;
int addr_len = sizeof(their_addr);
while (true) {
iResult = recvfrom(recvSocket, recvbuf, recvbuflen, 0,
(struct sockaddr*)&their_addr, &addr_len);
if (iResult > 0) {
std::string packet(recvbuf, iResult);
packets.push_back(packet);
// 这里可以添加逻辑来检查是否接收到了完整的数据
// 例如,如果每个包都包含长度信息或使用了某种协议来标记结束
// 假设我们简单地等待足够的数据来重新组合字符串
// 注意:这个示例没有实现真正的结束检测
// 打印接收到的数据包(可选)
std::cout << "Received " << iResult << " bytes: " << packet << std::endl;
// 假设我们已经接收到了所有包(这里需要实现更复杂的逻辑)
// 重新组合字符串(这里只是示例,没有实现)
// std::string reassembledMessage = ...;
// 如果需要,可以在这里处理reassembledMessage
}
else if (iResult == 0) {
std::cout << "Connection closing..." << std::endl;
break;
}
else {
std::cerr << "recvfrom failed with error: " << WSAGetLastError() << std::endl;
break;
}
}
}
客户端
#include <iostream>
#include <string>
#include <cstring>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <algorithm>
#pragma comment(lib, "ws2_32.lib")
#define PORT "12345"
#define MAX_PACKET_SIZE 6
#define min(a,b) (((a) < (b)) ? (a) : (b))
int main() {
WSADATA wsa;
SOCKET sock = INVALID_SOCKET;
struct addrinfo hints, * res;
int iResult;
// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsa);
if (iResult != 0) {
std::cerr << "WSAStartup failed with error: " << iResult << std::endl;
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
// 获取本地地址信息
iResult = getaddrinfo("127.0.0.1", PORT, &hints, &res);
if (iResult != 0) {
std::cerr << "getaddrinfo failed with error: " << iResult << std::endl;
WSACleanup();
return 1;
}
// 创建套接字
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock == INVALID_SOCKET) {
std::cerr << "socket failed with error: " << WSAGetLastError() << std::endl;
freeaddrinfo(res);
WSACleanup();
return 1;
}
std::string message = "这是一个CSDN中展示的示例,将这段文字分成多个包进行发送,每个包为6字节。";
sockaddr_in their_addr;
int addr_len = sizeof(their_addr);
// 填充对方的地址信息
//ZeroMemory(&their_addr, sizeof(their_addr));
//their_addr.sin_family = AF_INET;
//their_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 或者使用getaddrinfo获取
//their_addr.sin_port = htons(std::stoi(PORT));
// 发送数据
int offset = 0;
while (offset < message.length()) {
int bytesToSend = min(MAX_PACKET_SIZE, static_cast<int>(message.length() - offset));
std::string packet = message.substr(offset, bytesToSend);
iResult = sendto(sock, packet.c_str(), packet.length(), 0,
res->ai_addr, addr_len);
if (iResult == SOCKET_ERROR) {
std::cerr << "sendto failed with error: " << WSAGetLastError() << std::endl;
closesocket(sock);
WSACleanup();
return 1;
}
offset += bytesToSend;
std::cout << "Sent " << bytesToSend << " bytes\n";
}
closesocket(sock);
WSACleanup();
return 0;
}
运行结果
从接收端的结果可以看出,在最后一包的接受结果乱码了,主要原因是我们以6个字节为一包,最后只剩下5字节,造成的。
同时,也可以发现发送端发送之后就停止了,而接收端还在阻塞状态,等待着接收信息。
总结
UDP协议因其简单、高效的特点,在实时性要求高且可以容忍少量数据丢失的应用场景中得到了广泛应用。通过C++中的套接字API,我们可以方便地实现UDP通信。
笔者主要从事计算机视觉方面研究和开发,包括实例分割、目标检测、追踪等方向,进行算法优化和嵌入式平台开发部署。欢迎大家沟通交流、互帮互助、共同进步。