> 技术文档 > 深入了解UDP套接字:构建高效网络通信

深入了解UDP套接字:构建高效网络通信


个人主页:chian-ocean

文章专栏-NET

深入了解UDP套接字:构建高效网络通信

    • 个人主页:chian-ocean
    • 文章专栏-NET
  • 前言:
  • UDP
    • UDP 特点:
    • UDP的应用
  • 套接字地址
    • IP地址(Internet Protocol Address)
      • IP地址的点分十进制表示
    • 端口
      • 端口的作用
      • 知名端口
  • UDP套接字`API`
    • 创建套接字(`socket`)
        • 参数说明:
    • `sockaddr`结构体
    • 网络字节序(Network Byte Order)
      • 为什么需要网络字节序?
      • 网络字节序与主机字节序的区别
      • 网络字节序和主机字节序的转换
    • `recvfrom` 和 `sendto` 函数
        • 1. `recvfrom` 函数
        • 参数:
        • 2. `sendto` 函数
        • 参数:
        • 返回值:
  • UDP网络编程
    • 网络服务端
      • 代码结构及功能说明:
    • 网络客户端
      • `Linux OS`
        • 代码结构与功能说明:
      • `WIndows OS`
        • 代码结构与功能说明:
  • 源码

前言:

TCP/IP中有两个具有代表性的传输层协议,他们分别是TCP和UDP。TCP提供可靠的通信传输,而UDP则经常被用于让广播和细节控制交给应用层传输。总之,根据通信特征,选择合适的传输层协议是非常重要的。

深入了解UDP套接字:构建高效网络通信

UDP

UDP(User Datagram Protocol,用户数据报协议) 是一种简单的、无连接的网络协议,属于传输层协议。

UDP 特点:

  • 无连接:UDP 不需要先建立连接(无三次握手过程),每次发送数据时,发送方只需要提供目标地址和端口信息。
  • 不可靠性:UDP 不保证数据包的顺序、完整性或可靠性。数据报可能会丢失、重复或乱序。应用层需要自行处理这些问题(如果需要的话)。
  • 面向数据报:每个数据报是独立的,发送时与接收时无关,不能保证数据顺序,也不做数据流的管理。

UDP的应用

  • 包总量较少的通信(DNS,SNMP等)
  • 视频、音频等多媒体通信(实时通信)
  • 限定与LAN等特定网络中的应用通信
  • 广播通信(广播、多播)

套接字地址

​ 应用在使用TCP或者UDP的时候,会时使用OS提供的类库,始终库一般称作为API(Application Programming Interface,应用程序编程接口)。

​ 在使用TCP或者UDP通信的时候,又会广泛的使用到套接字(socket)的API。套接字原本是BSD UNIX 开发的,后面应用到了Windows的Winsock以及嵌入式操作系统中。

​ 应用程序应用套接字,可以设置端口号的IP地址、端口号、并且实现书库的接受和发送。

深入了解UDP套接字:构建高效网络通信

IP地址(Internet Protocol Address)

IP 地址(Internet Protocol Address,互联网协议地址)是计算机网络中用来唯一标识一台设备(如计算机、路由器、服务器、打印机等)在网络中的位置的地址。IP 地址可以类比为每台计算机的“身份证”,它确保数据在网络中能够正确地传输到目标设备。

  • IP地址( IPv4地址 ) 由32位正整数来表示。TCP/IP通信要求将这样的IP地址分配给每一个参与通信的主机。

IP地址的点分十进制表示

IP 地址的点分十进制表示法 是将 IPv4 地址(32 位二进制)分成四个字节(每个字节 8 位),并将每个字节转换为十进制数,用 点(. 分隔开来。这个格式使得人们更容易理解和记住 IP 地址。

二进制 IP 地址10101100.00010100.00000001.00000001

十进制转换

  • 10101100172
  • 0000100020
  • 000000011
  • 000000011

深入了解UDP套接字:构建高效网络通信

端口

在计算机网络中,端口是用来标识应用程序或网络服务的数字标识符,它帮助计算机识别不同的网络服务和应用程序。端口号与 IP 地址 一起构成了唯一的通信标识符,用于网络中数据包的正确传递。

端口的作用

  1. 区分不同的服务和应用程序
    • 计算机网络中,不同的服务和应用程序通过不同的端口号来区分。每个网络应用程序都会监听某个端口,操作系统通过端口号将接收到的数据分配给相应的应用程序。
  2. 实现多路复用
    • 多路复用指的是通过不同的端口号使同一台计算机能够同时处理多个网络连接。比如,Web 服务器通常通过端口 80(HTTP)接收请求,同时,FTP 服务器可以通过端口 21 处理文件传输。
  3. 标识网络协议
    • 端口号与 TCPUDP 等传输层协议配合使用,用于区分不同协议的数据流。TCP 和 UDP 使用不同的端口号来处理数据包。

知名端口

端口号的 0 到 1023 范围是 知名端口,通常由 IANA(互联网数字分配局)分配并保留给常见的协议和服务。这些端口号通常不允许由普通用户或应用程序使用,因为它们被特定的系统服务和协议占用。

  • 端口 0:端口 0 是一个特殊的保留端口,通常不能用于正常通信。它通常用于表示 “无效” 或 “未定义的端口”。
  • 端口 1 到 1023:这些端口被广泛用于操作系统和标准网络协议,因此普通用户和应用程序不能使用它们。举例来说:
    • 22:SSH(安全外壳协议)
    • 80:HTTP(Web 服务)
    • 443:HTTPS(加密的 Web 服务)
    • 25:SMTP(邮件传输协议)

在试图绑定80端口号,会出错返回(Permission denied)。

深入了解UDP套接字:构建高效网络通信

UDP套接字API

创建套接字(socket)

深入了解UDP套接字:构建高效网络通信

int socket(int domain, int type, int protocol);
参数说明:
  • domain:指定协议族,通常使用 AF_INET 表示 IPv4,AF_INET6 表示 IPv6,AF_UNIX 表示 UNIX 域套接字。
  • type:指定套接字类型,通常使用:
    • SOCK_STREAM:流式套接字,适用于 TCP 协议(面向连接)。
    • SOCK_DGRAM:数据报套接字,适用于 UDP 协议(无连接)。
  • protocol:指定协议类型,通常设为 0,表示自动选择合适的协议。

demo

int sockfd_ = socket(AF_INET,SOCK_DGRAM,0);//创建套接字if(sockfd_ < 0){ lg(fatal,\"Socket error,errno: %d,error: %s\",errno,strerror(errno));//打印日志}lg(info,\"Socket Success\");//打印日志

sockaddr结构体

C 语言 的网络编程中,sockaddr 是一个结构体,表示网络通信中的地址信息。它用于存储与套接字(socket)相关的地址信息,例如 IP 地址和端口号。sockaddr 是一个通用的结构体,用于支持不同类型的地址族(例如 IPv4IPv6Unix 域套接字)的地址。

sockaddr_in(用于 IPv4 地址)

  • sockaddr_insockaddr 的一个扩展,专门用于存储 IPv4 地址的信息。它包含了 IP 地址、端口号等信息。
struct sockaddr_in { short sin_family; // 地址族,通常是 AF_INET unsigned short sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IP 地址(网络字节序) char sin_zero[8]; // 填充字节,通常不使用};

网络字节序(Network Byte Order)

网络字节序(Network Byte Order)是指在网络通信中,数据以 大端字节序(Big Endian) 的格式进行传输的约定。它规定了多字节数据(如整数、浮点数等)在传输过程中,应该按照高位字节存储在低地址位置(大端格式),从而确保不同计算机之间的数据传输能够正确解析。

为什么需要网络字节序?

不同的计算机架构(如 x86PowerPC)使用不同的字节序来存储多字节数据。常见的字节序有:

  1. 大端字节序(Big Endian):高位字节存储在内存的低地址位置。
  2. 小端字节序(Little Endian):低位字节存储在内存的低地址位置。

为了确保网络中不同架构的计算机能够正确地交换数据,网络协议规定所有的数据传输必须使用 网络字节序,即 大端字节序。这使得不同平台之间的通信可以统一,并避免字节序不一致带来的数据解释错误。

网络字节序与主机字节序的区别

  • 主机字节序:是计算机系统内部使用的字节序,依赖于计算机的体系结构。常见的计算机体系结构有:
    • x86/x64 架构:使用小端字节序(Little Endian)。
    • 某些 RISC 架构:可能使用大端字节序(Big Endian)。
  • 网络字节序:是网络通信的标准格式,统一为大端字节序。

网络字节序和主机字节序的转换

这些函数主要用于将不同字节序的数据进行转换。由于不同的计算机架构使用不同的字节序(例如,大端字节序和小端字节序),而网络传输规定采用大端字节序(网络字节序),因此这些函数的作用就是将数据从主机字节序转换为网络字节序,或者从网络字节序转换为主机字节序。

深入了解UDP套接字:构建高效网络通信

uint32_t htonl(uint32_t hostlong);//将主机字节序的 32 位长整型转换为网络字节序。uint16_t htons(uint16_t hostshort);//将主机字节序的 16 位短整型转换为网络字节序。uint32_t ntohl(uint32_t netlong);//将网络字节序的 32 位长整型转换为主机字节序。uint16_t ntohs(uint16_t netshort);//将网络字节序的 16 位短整型转换为主机字节序。

深入了解UDP套接字:构建高效网络通信

//将点分十进制的 IPv4 地址字符串(如 `\"192.168.1.1\"`)转换为网络字节序的二进制格式,并存储在 in_addr 结构体中。int inet_aton(const char *cp, struct in_addr *inp);//将点分十进制的 IPv4 地址字符串(如 `\"192.168.1.1\"`)转换为 32 位的网络字节序整数格式。in_addr_t inet_addr(const char *cp);//将点分十进制的网络地址字符串转换为网络字节序的整数(通常是 IP 地址的网络部分)。in_addr_t inet_network(const char *cp);//将网络字节序的二进制格式的 IPv4 地址转换为点分十进制的字符串格式。char *inet_ntoa(struct in_addr in);//根据网络号和主机号生成网络字节序的完整 IP 地址。struct in_addr inet_makeaddr(int net, int host);//提取 in_addr 结构体中的主机部分。in_addr_t inet_lnaof(struct in_addr in);//提取 `in_addr` 结构体中的网络部分。in_addr_t inet_netof(struct in_addr in);

recvfromsendto 函数

recvfromsendto 是用于 UDP(无连接协议)原始套接字(raw socket) 的函数,允许应用程序在网络上传输数据。这些函数可以在不同的网络地址和端口之间进行数据的发送和接收。

1. recvfrom 函数

recvfrom 用于接收来自指定地址的数据包。它不仅接收数据,还能够获取数据来源的信息(如发送者的 IP 地址和端口号),非常适用于 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:指向 sockaddr 结构体的指针,接收者的地址信息(如 IP 地址和端口)。
  • addrlensrc_addr 结构体的大小。调用后,addrlen 会被设置为实际填充的地址长度。
2. sendto 函数

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:指向目标地址的 sockaddr 结构体,指定数据包的目标地址和端口。
  • addrlendest_addr 结构体的大小。
返回值:
  • 成功时,返回发送的字节数。
  • 失败时,返回 -1,errno 会被设置为错误代码。

UDP网络编程

网络服务端

#pragma once#include #include #include #include #include #include #include #include #include \"log.hpp\"// 日志对象,用于记录日志信息Log lg;// 默认端口号为 8080#define defaultport 8080// 默认缓冲区大小为 1024 字节int size = 1024;// 默认 IP 地址为 \"0.0.0.0\",表示监听所有可用的网络接口std::string defaultip = \"0.0.0.0\";// UdpServer 类定义:用于创建和管理 UDP 服务器class UdpServer{public: // 构造函数,默认套接字文件描述符为 0,默认端口为 8080,默认 IP 为 \"0.0.0.0\" UdpServer(int sockfd = 0,uint16_t port = defaultport,std::string ip = defaultip) :sockfd_(sockfd), port_(port), ip_(ip) {} // 初始化服务器:创建套接字,绑定到指定的 IP 和端口 void Init() { // 创建 UDP 套接字 sockfd_ = socket(AF_INET,SOCK_DGRAM,0); if(sockfd_ < 0) { // 如果创建套接字失败,记录日志并返回 lg(fatal,\"Socket error,errno: %d,error: %s\",errno,strerror(errno)); } lg(info,\"Socket Success\"); // 配置本地地址(IPv4 地址) struct sockaddr_in local; bzero(&local,sizeof(local)); // 将地址结构清零 local.sin_family = AF_INET; // 地址族为 IPv4 local.sin_port = htons(port_); // 将端口号转换为网络字节序 local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将 IP 地址转换为网络字节序 // 绑定套接字到指定的地址和端口 int n = bind(sockfd_,(struct sockaddr*)&local,sizeof(local)); if(n < 0) { // 如果绑定失败,记录日志并返回 lg(fatal,\"Bind error,errno: %d ,errno: %s\",errno,strerror(errno)); } lg(info,\"Bind Success\"); } // 服务器的主运行函数:接收客户端消息,并将消息回复给客户端 void Run() { is_running_ = true; while(is_running_) { char inbuffer[size]; // 用于接收数据的缓冲区 struct sockaddr_in client; // 客户端地址信息 bzero(&client,sizeof(client)); // 将客户端地址结构清零 socklen_t clientSize = sizeof(client); // 客户端地址的长度 // 使用 recvfrom 接收客户端发送的数据 ssize_t n = recvfrom(sockfd_,inbuffer,sizeof(inbuffer),0,(struct sockaddr*)&client,&clientSize); if(n < 0) { // 如果接收数据失败,记录日志并继续等待其他数据 lg(warning,\"recvfrom error, errno: %d, err string: %s\", errno, strerror(errno)); continue; } inbuffer[n] = 0; // 将接收到的数据字符串终结符设置为 NULL std::string Info = inbuffer; // 将接收到的数据转为字符串 std::cout << Info << std::endl; // 输出接收到的消息 // 使用 sendto 发送数据回客户端 sendto(sockfd_,Info.c_str(),Info.size(),0,(struct sockaddr*)&client,sizeof(client)); } } // 析构函数:关闭套接字,释放资源 ~UdpServer() { if(sockfd_ > 0) close(sockfd_); }private: int sockfd_; // 套接字文件描述符 uint16_t port_; // 服务器端口 std::string ip_; // 服务器 IP 地址 bool is_running_ = false; // 服务器运行状态};

代码结构及功能说明:

  1. 构造函数:初始化类的成员变量,包括套接字文件描述符、端口号和 IP 地址。默认值为端口 8080 和 IP 地址 0.0.0.0,这意味着它将绑定到所有可用的网络接口。
  2. Init():初始化函数,用于创建 UDP 套接字并绑定到指定的 IP 地址和端口。如果创建套接字或绑定失败,函数会记录错误日志并停止执行。
  3. Run():主循环函数,服务器在此函数中运行。它不断接收客户端的消息,并将接收到的消息发送回客户端。数据是通过 recvfrom()sendto() 函数进行接收和发送的。
  4. ~UdpServer():析构函数,用于关闭套接字,释放相关资源。

网络客户端

Linux OS

#include #include #include #include #include #include #include #include #include \"log.hpp\"// 创建一个日志对象,用于记录日志信息Log lg;// 缓冲区大小设为 1024 字节size_t size = 1024;// 使用说明函数,显示如何使用客户端程序void Usage(std::string proc){ std::cout << \"\\n\\rUsage: \" << proc << \" serverip serverport\\n\"<< std::endl;}int main(int argc ,char* argv[]){ // 参数检查:需要两个参数,服务器的 IP 地址和端口号 if(argc != 3) { Usage(argv[0]); return 1; } // 从命令行参数获取 IP 地址和端口号 std::string ip = argv[1]; std::uint16_t port = std::stoi(argv[2]); // 将端口号从字符串转换为整数 // 配置服务器的 sockaddr_in 结构体 struct sockaddr_in server; bzero(&server,sizeof(server)); // 清空结构体 server.sin_family = AF_INET; // 地址族为 IPv4 server.sin_port = htons(port); // 端口号转换为网络字节序 server.sin_addr.s_addr = inet_addr(ip.c_str()); // 将 IP 地址转换为网络字节序 // 创建 UDP 套接字 int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(sockfd < 0) { // 如果套接字创建失败,记录日志并返回 lg(fatal,\"sock error: errno:%d,error:%s\",errno,strerror(errno)); return 1; } lg(info,\"sock success\"); // 创建套接字成功 std::string message; // 存储用户输入的消息 while(true) { message.clear(); // 清空消息内容 std::cout << \"Please Enter@: \"; // 提示用户输入 getline(std::cin, message); // 获取用户输入 // 配置临时的客户端地址结构 struct sockaddr_in tmp; bzero(&tmp,sizeof(tmp)); // 清空结构体 socklen_t tmplen = sizeof(tmp); // 发送数据到服务器 sendto(sockfd, message.c_str(), size, 0, (struct sockaddr*)&server, sizeof(server)); // 接收服务器的响应 char buffer[size]; // 接收数据的缓冲区 ssize_t s = recvfrom(sockfd, &buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen); if(s > 0) { buffer[s] = \'\\0\'; // 确保接收到的数据以 NULL 终结 std::cout << buffer << std::endl; // 输出服务器的响应 } } // 关闭套接字 close(sockfd); return 0;}
代码结构与功能说明:
  1. Usage() 函数:如果程序没有正确传入 IP 地址和端口号参数,该函数会提示用户如何使用该程序。它显示了程序的运行方式。
  2. main() 函数
    • 命令行参数处理:获取用户输入的服务器 IP 地址和端口号。
    • server 配置:创建并配置 sockaddr_in 结构体来存储服务器的地址信息(IP 地址和端口)。
    • 套接字创建:使用 socket() 创建一个 UDP 套接字。如果创建失败,则记录错误并退出。
    • sendto()recvfrom()
      • sendto() 用于将消息发送到服务器。
      • recvfrom() 用于接收从服务器返回的数据。接收到的数据会被存储在 buffer 中,并输出到终端。
  3. UDP 套接字
    • 客户端不需要调用 bind() 来绑定端口,因为 UDP 是无连接的。发送数据时,UDP 套接字会自动选择一个临时端口。
  4. 消息发送与接收
    • 用户输入的消息通过 sendto() 发送到服务器。
    • 程序等待并接收服务器的响应,通过 recvfrom() 来获取数据,成功接收数据后输出。
  5. 日志系统
    • 代码使用 Log 类记录重要的信息、错误、成功状态等,便于调试和追踪。

WIndows OS

//#define _WINSOCK_DEPRECATED_NO_WARNINGS#include  // 包含 Winsock2 库,用于套接字编程#include  // 包含 Windows API,提供操作系统相关的功能#include  // 包含输入输出流库,用于显示信息#include  // 包含字符串操作函数#include  // 包含字符串处理函数#pragma warning(disable:4996) // 禁用有关不推荐使用的函数的编译器警告#pragma comment(lib, \"ws2_32.lib\") // 链接 Winsock 库,确保使用 Winsock 2.2 APIint main() { std::cout << \"hello client\" << std::endl; // 输出客户端信息 WSADATA wsaData; // 用于存储 Winsock 初始化数据 SOCKET udpSocket; // 用于存储创建的 UDP 套接字 sockaddr_in serverAddr; // 用于存储服务器的地址信息 int len = sizeof(serverAddr); // 地址结构的大小 const char* serverIP = \"119.3.219.187\"; // 服务器的 IP 地址 int serverPort = 8080; // 服务器的端口号 // 初始化 Winsock 库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << \"Winsock initialization failed!\" << std::endl; // 如果初始化失败,输出错误信息 return 1; } // 创建 UDP 套接字 udpSocket = socket(AF_INET, SOCK_DGRAM, 0); // 使用 IPv4 地址族和 UDP 协议 if (udpSocket == INVALID_SOCKET) { // 如果创建套接字失败 std::cerr << \"Socket creation failed!\" << std::endl; WSACleanup(); // 清理 Winsock 库资源 return 1; } // 配置服务器的地址信息 serverAddr.sin_family = AF_INET; // 地址族设置为 IPv4 serverAddr.sin_port = htons(serverPort); // 将端口号转换为网络字节序 serverAddr.sin_addr.s_addr = inet_addr(serverIP); // 将服务器 IP 地址转换为网络字节序 // 发送数据 std::string message; // 用于存储用户输入的消息 while (true) { // 获取用户输入的消息 std::cout << \"Please Enter#: \"; getline(std::cin, message); // 从标准输入读取消息 struct sockaddr_in tmp; // 临时结构体,用于接收数据 int tmplen = sizeof(tmp); // 地址结构的大小 // 使用 sendto 函数发送 UDP 数据包到指定的服务器地址和端口 sendto(udpSocket, message.c_str(), message.size(), 0, (struct sockaddr*)&serverAddr, len); // 接收来自服务器的响应 char buffer[1024]; // 用于存储接收到的数据 int s = recvfrom(udpSocket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen); // 接收数据 if (s > 0) // 如果接收到数据 { buffer[s] = \'\\0\'; // 确保接收到的字符串以 NULL 终止 std::cout << buffer << std::endl; // 输出接收到的消息 } } std::cout << \"Message sent to server!\" << std::endl; // 输出消息已发送的提示信息 // 清理资源,关闭套接字并清理 Winsock 库 closesocket(udpSocket); WSACleanup(); return 0;}
代码结构与功能说明:
  1. 初始化 Winsock 库
    • 使用 WSAStartup() 初始化 Winsock 库,初始化失败时输出错误信息并退出程序。
  2. 创建 UDP 套接字
    • 使用 socket() 创建一个 UDP 套接字。若创建失败,则输出错误信息并退出。
  3. 配置服务器地址
    • 设置服务器的地址族为 AF_INET(IPv4 地址族),将端口号转换为网络字节序,并将服务器的 IP 地址转换为网络字节序。
  4. 发送数据
    • 程序进入一个无限循环,等待用户输入消息并发送给服务器。用户输入的消息通过 sendto() 发送到指定的服务器地址和端口。
  5. 接收数据
    • 使用 recvfrom() 接收服务器返回的响应。接收到的数据存储在缓冲区 buffer 中,然后输出到终端。
  6. 资源清理
    • 使用 closesocket() 关闭套接字,使用 WSACleanup() 清理 Winsock 库资源。

源码