news 2026/10/29 11:11:50

【Linux网络】UDP 服务器怎么写?从 socket、bind 到 recvfrom/sendto,手写完整回显服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux网络】UDP 服务器怎么写?从 socket、bind 到 recvfrom/sendto,手写完整回显服务器

文章目录

  • 前言
  • 一、先看 UDP 服务器的工作流程
  • 二、创建 UDP Socket
    • 2.1 `socket` 的三个参数
    • 2.2 为什么创建以后还要绑定
  • 三、服务器显式调用 `bind`
    • 3.1 `bind` 的参数
    • 3.2 为什么服务器必须显式绑定
    • 3.3 为什么通常绑定 `INADDR_ANY`
  • 四、接收数据:`recvfrom`
    • 4.1 为什么接收数据时还要拿到对端地址
    • 4.2 端口转换不能漏掉
  • 五、发送数据:`sendto`
  • 六、完整的 UDP 回显服务器
    • 6.1 `InetAddr.hpp`
    • 6.2 `UdpServer.hpp`
    • 6.3 服务器入口
  • 七、完整的 UDP 客户端
    • 7.1 客户端为什么没有调用 `bind`
    • 7.2 为什么服务器不能也这样做
    • 7.3 Makefile 与运行方式
  • 八、几个容易写错的地方
    • 8.1 地址结构没有清零
    • 8.2 端口忘记转换字节序
    • 8.3 `recvfrom` 没有预留结束符
    • 8.4 忘记初始化 `addrlen`
    • 8.5 把 `INADDR_ANY` 理解成远端地址
  • 总结

前言

上一篇把 IP、端口、网络字节序和sockaddr_in这些预备知识串了起来。这一篇开始真正写 UDP 程序。

我们先完成一个最简单的 Echo Server:客户端发送什么,服务器就原样返回什么。程序虽然简单,但会把 UDP 编程最核心的四个接口全部走一遍:

socket -> bind -> recvfrom -> sendto

本文先把回显程序写完整,重点放在 UDP 接口行为、服务器和客户端的绑定差异,以及一份数据报怎样完成往返。下一篇再把网络收发封装起来,改造成英译汉服务器。


一、先看 UDP 服务器的工作流程

UDP 不需要像 TCP 那样先建立连接。服务器的基本流程可以直接整理成四步:

  1. 调用socket创建 UDP Socket;
  2. 调用bind绑定本地端口;
  3. 调用recvfrom接收客户端数据,同时取得客户端地址;
  4. 调用sendto,根据刚得到的客户端地址返回响应。

服务器通常放在循环中不断执行第 3、4 步。一个请求处理完以后,它继续等待下一份数据报。

客户端的流程更短:创建 Socket,填写服务器地址,调用sendto发送,再调用recvfrom接收响应。客户端也需要本地 IP 和端口,但通常不必自己显式绑定,操作系统会在第一次发送时完成临时绑定。

二、创建 UDP Socket

2.1socket的三个参数

函数原型如下:

#include<sys/types.h>#include<sys/socket.h>intsocket(intdomain,inttype,intprotocol);

UDP IPv4 程序通常这样调用:

intsockfd=socket(AF_INET,SOCK_DGRAM,0);

三个参数分别表示:

  • AF_INET:使用 IPv4 地址族;
  • SOCK_DGRAM:创建面向数据报的 Socket,也就是 UDP;
  • 0:让系统根据前两个参数选择默认协议。

调用成功后返回一个非负文件描述符,失败返回-1并设置errno。这个返回值就是应用程序后续访问内核 Socket 的入口。

intsockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){std::cerr<<"socket error: "<<strerror(errno)<<std::endl;return1;}

这里再区分一次:IP + port是网络地址,Socket 是内核创建的通信对象,sockfd是进程访问这个对象的文件描述符。三者有关联,但不能直接画等号。

2.2 为什么创建以后还要绑定

socket()只完成了通信对象的创建。此时内核知道它是一个 IPv4 UDP Socket,却还不知道服务器要在哪个本地端口接收数据。

这就像打开了一个网络文件,但还没有告诉系统“发往本机哪个端口的数据应该交给我”。服务器接下来要调用bind,把本地地址信息写入内核 Socket。

三、服务器显式调用bind

3.1bind的参数

函数原型如下:

intbind(intsockfd,conststructsockaddr*addr,socklen_t addrlen);

参数含义:

  • sockfd:socket()返回的文件描述符;
  • addr:要绑定的本地地址;
  • addrlen:地址结构的实际大小。

IPv4 程序填写的是sockaddr_in,传参时转换为通用的sockaddr*:

structsockaddr_inlocal;bzero(&local,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(8888);local.sin_addr.s_addr=htonl(INADDR_ANY);intret=bind(sockfd,reinterpret_cast<structsockaddr*>(&local),sizeof(local));if(ret<0){std::cerr<<"bind error: "<<strerror(errno)<<std::endl;close(sockfd);return2;}

bzero的作用是把结构体内存清零,也可以使用:

memset(&local,0,sizeof(local));

先清零不是为了好看,而是防止结构体中未显式赋值的填充字段残留随机数据。

3.2 为什么服务器必须显式绑定

服务器需要一个稳定入口。客户端程序要提前知道服务器的 IP 和端口,才能把请求发送过来。如果服务器每次启动都使用随机端口,客户端就无法确定目标地址。

这和常用服务号码类似:服务端地址要稳定,访问者才能找到它。所以服务器代码通常从命令行或配置文件中读取端口,再主动执行bind。

服务器显式绑定的核心原因,不是“代码规定必须这样写”,而是服务器必须向客户端提供稳定、可预期的访问入口。

3.3 为什么通常绑定INADDR_ANY

服务器可以绑定某个具体本地 IP,也可以把sin_addr.s_addr设置为INADDR_ANY。

local.sin_addr.s_addr=htonl(INADDR_ANY);

INADDR_ANY的值是0.0.0.0。它表示这个 Socket 接受发往本机任意 IPv4 接口、目标端口匹配的数据。

假设一台服务器同时有环回地址、内网地址和其他网卡地址。如果程序只绑定127.0.0.1,那么它只能接收从环回路径到达该地址的数据;客户端使用内网地址访问时,不会匹配这个绑定。

127.0.0.1是本地环回地址,数据不会真正经过外部网络,适合在同一台机器上测试客户端和服务器。

绑定机器上的内网地址也可以,但此时只有发往这个地址和端口的报文会匹配。

云服务器还有一个容易误解的情况:控制台显示的公网 IP 不一定直接配置在虚拟机网卡上,平台可能通过公网与内网地址映射完成转发。如果这个公网 IP 并不属于本机接口,程序直接bind它会失败。

因此,通用服务器通常绑定INADDR_ANY,再由操作系统和云平台的网络配置决定数据从哪个接口进入。

可以用下面的命令查看 UDP 监听状态:

netstat-anup
  • -a:显示所有相关 Socket;
  • -n:直接显示数字地址和端口;
  • -u:显示 UDP;
  • -p:显示对应进程信息。

四、接收数据:recvfrom

4.1 为什么接收数据时还要拿到对端地址

UDP 没有提前建立连接。服务器收到一份数据报后,不仅要知道数据内容,还要知道它来自哪个客户端,否则无法把响应发回去。

recvfrom正好同时返回这两类信息:

ssize_trecvfrom(intsockfd,void*buf,size_t len,intflags,structsockaddr*src_addr,socklen_t*addrlen);

各参数含义:

  • sockfd:要读取的 UDP Socket;
  • buf:用户缓冲区;
  • len:缓冲区大小;
  • flags:读取选项,当前填0;
  • src_addr:输出对端地址;
  • addrlen:值—结果参数,调用前写结构体容量,返回后表示实际地址长度。

典型写法如下:

charbuffer[1024];structsockaddr_inpeer;socklen_t peer_len=sizeof(peer);ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,reinterpret_cast<structsockaddr*>(&peer),&peer_len);if(n>0){buffer[n]='\0';}

缓冲区预留一个字节,再手动补\0,是为了把收到的内容当作 C 字符串输出。UDP 报文本身并不会自动附带字符串结束符。

默认情况下,recvfrom没有数据可读时会阻塞等待。它不是一直占用 CPU 空转,而是让进程在内核中等待,数据到达后再被唤醒。

4.2 端口转换不能漏掉

peer.sin_port保存的是网络字节序,打印前要转换:

uint16_tport=ntohs(peer.sin_port);

IP 地址可以暂时使用inet_ntoa(peer.sin_addr)转成点分十进制字符串。后面还会讨论它的静态缓冲区问题,并给出更推荐的inet_ntop写法。

五、发送数据:sendto

sendto的函数原型如下:

ssize_tsendto(intsockfd,constvoid*buf,size_t len,intflags,conststructsockaddr*dest_addr,socklen_t addrlen);

前四个参数描述“通过哪个 Socket 发送什么数据”,后两个参数描述“发给谁”。

ssize_t m=sendto(sockfd,buffer,static_cast<size_t>(n),0,reinterpret_cast<structsockaddr*>(&peer),peer_len);

服务器刚刚通过recvfrom拿到peer,现在把同一个地址交给sendto,响应就能回到原客户端。

同一个 UDP Socket 既可以调用recvfrom,也可以调用sendto。也就是说,UDP 通信支持全双工:通信双方都具备发送和接收能力,读写可以分别进行。

全双工不等于每段代码真的在同一瞬间执行。单线程程序仍然按语句顺序运行;如果客户端希望一边等待输入、一边持续接收消息,可以使用两个线程分别负责发送和接收,第三篇聊天室会用到这个结构。

六、完整的 UDP 回显服务器

这部分按照实际工程拆成服务器头文件、服务器入口、客户端和 Makefile。目录结构如下:

udp_echo/ ├── InetAddr.hpp ├── UdpServer.hpp ├── UdpServer.cc ├── UdpClient.cc └── makefile

UdpServer.hpp负责封装服务器,UdpServer.cc只保留启动逻辑;客户端单独放在UdpClient.cc。这样代码不会全部挤在一个文件里,后面改造成字典服务器时也能直接复用。

先实现一个轻量地址类,把sockaddr_in转成便于打印的 IP 和端口。

6.1InetAddr.hpp

#pragmaonce#include<arpa/inet.h>#include<netinet/in.h>#include<cstdint>#include<string>classInetAddr{public:explicitInetAddr(constsockaddr_in&addr):addr_(addr),port_(ntohs(addr.sin_port)){charip[INET_ADDRSTRLEN]={0};inet_ntop(AF_INET,&addr.sin_addr,ip,sizeof(ip));ip_=ip;}conststd::string&Ip()const{returnip_;}uint16_tPort()const{returnport_;}constsockaddr_in&NetAddr()const{returnaddr_;}std::stringStringAddr()const{returnip_+":"+std::to_string(port_);}private:sockaddr_in addr_{};std::string ip_;uint16_tport_=0;};

这里直接使用inet_ntop,避免把inet_ntoa返回的静态缓冲区指针长期保存下来。

6.2UdpServer.hpp

#pragmaonce#include"InetAddr.hpp"#include<cerrno>#include<cstring>#include<iostream>#include<string>#include<unistd.h>#include<sys/socket.h>classUdpServer{public:explicitUdpServer(uint16_tport):port_(port){}boolInit(){sockfd_=socket(AF_INET,SOCK_DGRAM,0);if(sockfd_<0){std::cerr<<"socket error: "<<strerror(errno)<<std::endl;returnfalse;}sockaddr_in local{};local.sin_family=AF_INET;local.sin_port=htons(port_);local.sin_addr.s_addr=htonl(INADDR_ANY);intret=bind(sockfd_,reinterpret_cast<sockaddr*>(&local),sizeof(local));if(ret<0){std::cerr<<"bind error: "<<strerror(errno)<<std::endl;returnfalse;}returntrue;}voidStart(){charbuffer[1024];for(;;){sockaddr_in peer{};socklen_t peer_len=sizeof(peer);ssize_t n=recvfrom(sockfd_,buffer,sizeof(buffer)-1,0,reinterpret_cast<sockaddr*>(&peer),&peer_len);if(n<0){std::cerr<<"recvfrom error: "<<strerror(errno)<<std::endl;continue;}buffer[n]='\0';InetAddrclient(peer);std::cout<<"["<<client.StringAddr()<<"]# "<<buffer<<std::endl;ssize_t m=sendto(sockfd_,buffer,static_cast<size_t>(n),0,reinterpret_cast<sockaddr*>(&peer),peer_len);if(m<0){std::cerr<<"sendto error: "<<strerror(errno)<<std::endl;}}}~UdpServer(){if(sockfd_>=0){close(sockfd_);}}private:uint16_tport_;intsockfd_=-1;};

这份代码的主线并不复杂:

创建Socket -> 绑定端口 -> 接收数据和来源地址 -> 原样返回

recvfrom得到的peer不只是为了打印。服务器能回复正确客户端,靠的就是这份源地址信息。

6.3 服务器入口

#include"UdpServer.hpp"#include<cstdlib>#include<iostream>intmain(intargc,char*argv[]){if(argc!=2){std::cerr<<"Usage: "<<argv[0]<<" port"<<std::endl;return1;}uint16_tport=static_cast<uint16_t>(std::stoi(argv[1]));UdpServerserver(port);if(!server.Init()){return2;}server.Start();return0;}

这段代码保存为UdpServer.cc。服务器启动时只接收端口,不要求手动填写本地 IP,因为UdpServer::Init()已经绑定INADDR_ANY。

七、完整的 UDP 客户端

客户端必须知道服务器地址,因此从命令行接收server_ip和server_port。

#include<arpa/inet.h>#include<netinet/in.h>#include<sys/socket.h>#include<unistd.h>#include<cerrno>#include<cstring>#include<iostream>#include<string>intmain(intargc,char*argv[]){if(argc!=3){std::cerr<<"Usage: "<<argv[0]<<" server_ip server_port"<<std::endl;return1;}intsockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){std::cerr<<"socket error: "<<strerror(errno)<<std::endl;return2;}sockaddr_in server{};server.sin_family=AF_INET;server.sin_port=htons(static_cast<uint16_t>(std::stoi(argv[2])));intret=inet_pton(AF_INET,argv[1],&server.sin_addr);if(ret!=1){std::cerr<<"invalid server ip"<<std::endl;close(sockfd);return3;}for(;;){std::string request;std::cout<<"Please Enter# ";if(!std::getline(std::cin,request)){break;}ssize_t n=sendto(sockfd,request.data(),request.size(),0,reinterpret_cast<sockaddr*>(&server),sizeof(server));if(n<0){std::cerr<<"sendto error: "<<strerror(errno)<<std::endl;break;}charbuffer[1024];sockaddr_in peer{};socklen_t len=sizeof(peer);ssize_t m=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,reinterpret_cast<sockaddr*>(&peer),&len);if(m<0){std::cerr<<"recvfrom error: "<<strerror(errno)<<std::endl;break;}buffer[m]='\0';std::cout<<"server echo# "<<buffer<<std::endl;}close(sockfd);return0;}

7.1 客户端为什么没有调用bind

客户端当然也要有本地 IP 和端口,否则服务器无法回复。区别在于:它不需要在代码中显式调用bind。

当客户端第一次调用sendto时,操作系统会根据路由选择合适的本地 IP,并从临时端口范围中选择一个未冲突的端口,完成隐式绑定。

这样做有两个直接好处:

  1. 不同客户端不会因为写死同一个本地端口而互相冲突;
  2. 客户端通常不需要对外提供稳定入口,具体临时端口是多少并不重要。

需要强调的是:客户端不是“不绑定”,而是一般不由应用程序显式绑定。

7.2 为什么服务器不能也这样做

如果服务器也让系统随机选端口,客户端就不知道该向哪里发送请求。客户端端口只需要当前会话中唯一,服务器端口则必须提前对外约定,两者的角色不同。

客户端通常由操作系统隐式绑定临时端口;服务器必须显式绑定稳定端口。两边都需要本地端点,只是分配方式不同。

7.3 Makefile 与运行方式

完整工程可以使用下面的 Makefile:

.PHONY: all clean all: udpclient udpserver udpclient: UdpClient.cc g++ -o $@ $^ -std=c++17 udpserver: UdpServer.cc g++ -o $@ $^ -std=c++17 clean: rm -f udpclient udpserver

编译:

make

先启动服务器:

./udpserver8888

再启动客户端。若在同一台机器上测试,可以访问环回地址:

./udpclient127.0.0.18888

若两端位于同一局域网,客户端应填写服务器实际可达的内网 IP。云服务器还要确认安全组和系统防火墙已经放行对应 UDP 端口。

服务端显示“bind 成功”只说明本地端口已经建立,不能证明远端一定可达。跨主机测试还要同时检查目标 IP、路由、防火墙和云平台安全组。

八、几个容易写错的地方

8.1 地址结构没有清零

声明sockaddr_in后应初始化为零,再填写字段。否则未赋值的字节可能保留栈上的随机内容。

sockaddr_in addr{};

这种 C++ 写法已经完成零初始化,也可以使用memset或bzero。

8.2 端口忘记转换字节序

下面的写法是错误的:

local.sin_port=8888;

正确写法是:

local.sin_port=htons(8888);

从地址结构中读取端口时再使用ntohs。

8.3recvfrom没有预留结束符

如果准备把数据当字符串输出,缓冲区大小应传sizeof(buffer) - 1,成功后再写入buffer[n] = '\0'。

8.4 忘记初始化addrlen

recvfrom的最后一个参数不是单纯输出参数。调用前要设置:

socklen_t len=sizeof(peer);

内核返回时再把实际长度写回去。

8.5 把INADDR_ANY理解成远端地址

INADDR_ANY用于服务器本地绑定,表示接收发往本机任意 IPv4 接口、指定端口的数据。客户端不能把它当成一个正常的远端服务器地址去访问。

总结

这一篇完成了 UDP 编程的第一条完整链路:

  1. socket(AF_INET, SOCK_DGRAM, 0)创建 UDP Socket;
  2. 服务器使用bind绑定稳定端口,通常绑定INADDR_ANY;
  3. recvfrom同时取得请求数据和客户端地址;
  4. sendto根据目标地址发送响应;
  5. 客户端通常由操作系统在首次发送时隐式绑定临时端口;
  6. 回显程序已经把 UDP 的创建、绑定、接收和发送连成了一条完整链路。

到这里,一个客户端和一个服务器之间已经可以完成请求—响应。下一篇继续封装 Socket 和服务器框架,把回显业务替换成网络词典,并补充 IPv4 地址转换接口。

本文对应的完整 Socket 工程可以在代码仓库中查看:Socket 编程代码。

往期博客:
【Linux网络】网络基础下篇:数据到底怎么从进程 A 到进程 B?从 MAC、IP、端口到 Socket 一次理清
【Linux】线程安全进阶:单例模式、可重入函数与死锁排查
【Linux网络】Socket 编程前要搞懂什么?从 IP、端口到字节序与 sockaddr,一次理清网络通信基础

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 11:11:39

obs录屏

【干货】把 OBS 拆到骨头&#xff1a;手把手带你用 libobs 撸一个 40MB 的轻量录屏工具 收藏吃灰警告 ⚠️&#xff1a;本文不是「怎么用 OBS」&#xff0c;而是「怎么把 OBS 当库用」的保姆级拆解&#xff0c;含 5 个让你少走弯路的踩坑经验。建议先点赞收藏&#xff0c;慢慢看…

作者头像 李华
网站建设 2026/10/29 11:11:37

物理AI的核心挑战在于获取符合物理规律的训练数据

2026年10月24日至25日&#xff0c;CSDN在长沙世界计算长沙智谷举办了"2026长沙1024程序员日暨AI构建者大会"&#xff0c;主题为"具身同行&#xff0c;智构未来"&#xff0c;线下参会人数超1500人。 大会以具身智能与物理AI为主线&#xff0c;设置了开幕式暨…

作者头像 李华
网站建设 2026/10/29 11:10:47

VCP-CLIP

文章目录AbstractIntroductionRelated WorkPrompt learningZero-shot anomaly segmentationMethodProblem DefinitionThe design of baseline、The design of VCP-CLIPPre-VCPPost-VCPTraining and InferenceLoss FunctionInferenceExperimentExperimental SetupComparison wit…

作者头像 李华
网站建设 2026/10/29 11:10:33

和弦图分析

和弦图分析&#xff1a;学生成绩数据一、什么是和弦图和弦图是一种用“环状节点 连接弦线”来展示多个对象之间关系的图表。通常所有对象均匀分布在一个圆周上&#xff0c;任意两个对象之间若有联系&#xff0c;就用一条从一方弧向另一方的弦连接起来。和弦图常用来呈现相关性…

作者头像 李华
网站建设 2026/10/29 11:09:24

港口机械视觉实战:从集装箱箱号OCR到岸桥吊装监控的完整方案设计

港口机械视觉实战&#xff1a;从集装箱箱号OCR到岸桥吊装监控的完整方案设计 机器视觉选型笔记 行业应用实战第 11 篇 **阅读提示&#xff1a;**本文约 6300 字&#xff0c;预计阅读 16 分钟。如果你正在做码头岸桥/场桥的箱号识别、闸口 OCR 或者吊装安全监控方案&#xff0…

作者头像 李华