本篇主线
| 环节 | 要解决的问题 | 结论 |
|---|---|---|
| 地址准备 | IP 怎么写进 sockaddr_in |
字符串 → 4 字节 → 网络序,inet_addr 一步完成 |
| 创建 | 网络文件怎么打开 | socket() 返回一个文件描述符 |
| ★ 关键认知 | 填完结构体就算设置好了吗 | 没有 。填的只是自己栈上的变量,必须 bind 才交给内核 |
| 服务端骨架 | 为什么程序不能退出 | 服务器必须 7×24 死循环 |
| 收 | 数据怎么上来、怎么知道是谁发的 | recvfrom,地址是出参 |
| 发 | 怎么回给对方 | sendto,地址是入参 |
| 客户端 | 要不要 bind |
要,但不显式绑 ,由 OS 首次 sendto 时随机分配 |
| ★ 地址坑 | 服务端该绑哪个 IP | 不绑特定 IP ,用 INADDR_ANY(0.0.0.0) |
一、起点:报文里必须带上自己的地址
前一篇的结论是:IP + Port 才能定位全网唯一的一个进程。落到代码上,这条结论立刻变成一个硬要求:
| 方向 | 报文里必须携带 |
|---|---|
| 客户端 → 服务端 | 自己的 IP、自己的端口 |
| 服务端 → 客户端 | 自己的 IP、自己的端口 |
原因很直接:对方将来要拿着这两个值把消息发回来。如果不带,对方就不知道该往哪儿回。
于是填充 sockaddr_in 变成两件事:
| 字段 | 含义 | 要点 |
|---|---|---|
sin_family |
网络通信还是本地通信 | 地址类型(协议家族) |
sin_port |
端口号 | 2 字节 16 位 → htons() 主机序转网络序 |
sin_addr |
IP号 | 4 字节 IP → 也要转网络序 |
💡 注意端口号是 16 位、IP 地址是 32 位,所以用的转换函数不同:htons 对 16 位,htonl 对 32 位。而 IP 地址的转换在库函数里被合并成一个接口,见下文。
二、把点分十进制 IP 变成 4 字节
✔ 结论:这套转换自己写完全可行,但没必要 ------ 系统已经提供了现成接口。
系统接口:inet_addr
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
| 项 | 内容 |
|---|---|
| 输入 | 点分十进制字符串 |
| 输出 | in_addr_t(本质就是 uint32_t),已经是网络序 |
| 效果 | 上文的两步(字符串 → 4 字节、4 字节 → 网络序)一步完成 |
| 失败 | 返回 INADDR_NONE |
💡 也就是说,前面费力气讲的拆分与大小端,目的不是「以后自己写」,而是理解这个接口内部在干什么 。理解了,后面看到 inet_addr 就不会觉得它是魔法。
常见的三个 IP 转换接口分工:
| 接口 | 作用 | 备注 |
|---|---|---|
inet_addr() |
字符串 → 网络序 4 字节 | 最常用;无法区分 255.255.255.255 与出错 |
inet_aton() |
字符串 → struct in_addr |
有独立的返回值,更稳妥 |
inet_ntoa() |
4 字节 → 字符串 | 打印地址时用 |
三、★ 填结构体 ≠ 设置给内核
这是本讲最容易出错、也最值得单独记住的一个认知。
写完下面这段代码后,一个自然的问题是:到这里,网络文件是不是已经拥有 IP 和端口了?
c
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
✔ 答案:没有。
原因很干脆:
struct sockaddr_in只是系统提供的一种数据类型;struct sockaddr_in local;是在自己的用户栈(自己的虚拟地址空间)上开辟的一个变量;- 往
local里填字段,改的只是自己栈上这个变量的内容; - 整个过程没有一次和内核发生交互。
✔ 一句话:填结构体只是「准备好一张地址表」,真正把这张表交给内核的动作是
bind。
这个区分非常重要,因为它解释了为什么必须有 bind 这个系统调用 ------ 否则内核根本不知道「哪个网络文件对应哪个地址」。
四、socket():创建套接字
第一阶段叫「创建套接字」,用一句话说就是:打开一个网络文件。
c
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
| 参数 | 取值 | 含义 |
|---|---|---|
| 地址家族 | AF_INET |
网络通信(IP 协议家族) |
| 套接字类型 | SOCK_DGRAM |
数据报套接字 → 对应 UDP |
| 协议 | 0 |
默认,由前两个参数决定 |
💡 如果第二个参数写 SOCK_STREAM,对应的就是 TCP 的字节流套接字。
从系统层面理解这个 sockfd,其实一点都不陌生:
| 步骤 | 发生的事 |
|---|---|
| ① | 进程的文件描述符表 中打开一个 struct file 对象 |
| ② | 把该对象的地址填入文件描述符表 |
| ③ | 返回一个合法的数组下标 ,这就是 sockfd |
✔ 所以套接字在系统层面和普通文件是同一个模型 ------ 只不过它背后接的不是磁盘,而是协议栈。
返回值判断:sockfd < 0 表示失败,打印 errno 并退出(退出码习惯上给 2,便于区分是创建失败还是绑定失败)。
五、bind():把地址绑定到套接字
5.1 三个参数
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数 | 传什么 | 说明 |
|---|---|---|
sockfd |
上一步创建的 fd | 要绑定的那个网络文件 |
addr |
(struct sockaddr *)&local |
类型不匹配,必须强转 |
addrlen |
sizeof(local) |
该结构体的长度 |
为什么要强转? 这是前一篇 sockaddr 继承体系的直接应用:
- 接口只认「基类指针」
struct sockaddr *; - 实际填的是「子类」
struct sockaddr_in; - 子类的首字段保留了基类的 16 位地址类型,所以强转之后,函数内部按
sa_family分发,能正确还原成sockaddr_in来解释。
5.2 返回值与错误处理
| 返回值 | 含义 |
|---|---|
0 |
成功 |
-1 |
失败,errno 被设置 |
绑定失败要不要继续?不能继续。
绑不上就意味着这个服务器既没有 IP 也没有端口 ------ 在全网范围内它根本无法被定位,起来也没有意义。
所以标准处理是:打印 errno → exit(2)。
5.3 服务端初始化只有两步
走到这里,UDP 服务端的「初始化」就全部完成了:
| 步骤 | 动作 | 结果 |
|---|---|---|
| ① | socket() |
有了一个网络文件(fd) |
| ② | bind() |
这个文件与本地 IP + 端口建立关联 |
✔ 就这么两步。 不需要连接、不需要握手、不需要监听队列 ------ 因为 UDP 不面向连接。
六、★ 服务器必须是一个死循环
6.1 一个被忽略的差异
平时写的 C/C++ 程序,跑起来一瞬间就结束、结果打印出来,进程退出。但真正在用的软件启动之后默认不退出,除非用户主动关闭:
| 软件 | 行为 |
|---|---|
| 直播客户端 | 不关就一直开着 |
| 画图板 / VSCode / 终端 / 音乐软件 | 不关就一直开着 |
| 手机上的抖音、快手、拼多多 | 同上 |
| 某些后台软件 | 开机自启,关机才退 |
6.2 服务器是 7×24 的
对服务器而言这不是习惯,而是要求:
- 半夜能刷抖音、逛淘宝,是因为对端服务一直在跑;
- 云服务器买到手几乎不会关机 ------ 服务器永远不退,除非报废;
- 一旦部署了服务,从启动到下线,整个过程都不能退出。
所以服务端主循环写成:
cpp
bool is_running = true;
while (is_running) {
// 收消息
// 处理
// 发消息
}
💡 死循环怎么杀掉?靠信号让它响应(信号部分后续再展开)。所以「死循环」不是缺陷,而是服务端程序的正常形态。
七、recvfrom():收数据,顺手记下对方是谁
c
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
7.1 六个参数逐一看
| # | 参数 | 类型 | 谁填 | 含义 |
|---|---|---|---|---|
| 1 | sockfd |
int |
传 | 自己创建的套接字 |
| 2 | buf |
void * |
传 | 用户自定义的接收缓冲区 |
| 3 | len |
size_t |
传 | 缓冲区有多大 |
| 4 | flags |
int |
传 | 通常 0 → 阻塞读 |
| 5 | src_addr |
struct sockaddr * |
内核回填 | 对端(谁发来的)地址 |
| 6 | addrlen |
socklen_t * |
出入参 | 结构体大小 / 实际读到的信息大小 |
第 2、3 个参数的作用,用以前写文件读取的经验就能类推:要读数据,总得自己准备一块缓冲区。区别只在于 ------ 这次数据是从内核协议栈里读上来的,而不是从磁盘。
7.2 阻塞式 IO
flags = 0 表示阻塞式 IO。
| 概念 | 含义 |
|---|---|
| 阻塞等待 | 条件不满足就一直等(进程等待时学过) |
| 进程状态 | 有阻塞态、阻塞挂起(进程状态时学过) |
| 本次的具体表现 | 对方不发数据,进程就停在这个函数里等 |
💡 这个行为和 scanf 完全一样:从键盘读不到数据时,scanf 就卡在那里。网络收不到数据时,recvfrom 也卡在那里。
一旦数据到达,函数立刻返回,并把数据从内核缓冲区拷贝到用户缓冲区。
7.3 ★ 为什么必须要 src_addr
先用一个现实场景把问题问清楚:服务端收到一条「你好」,需不需要知道是谁发的?
答案是需要,而且必须知道。理由有两层:
| 层次 | 理由 |
|---|---|
| 业务层 | 一个服务器同时会服务很多客户端,必须区分来源 |
| 技术层 | 要回消息,就必须知道往哪个地址回 |
过年收礼的类比 :家里会来很多客人,各自带来礼物。收下礼物只是第一件事;还必须记住每一份礼物是谁带来的 ------ 因为将来要去还礼。如果不知道谁送的,回礼时就出问题了。
于是 recvfrom 的第 5、6 个参数承担这件事:
src_addr → 内核把报文的源 IP + 源端口填进来
addrlen → 内核把「实际写了多少字节」填回来
✔ 这就是 UDP 的必然设计:因为不面向连接,服务端事先不可能知道谁会发来数据,只能「收完之后从报文里把对方地址读出来」。
7.4 addrlen 为什么是「出入参」
| 阶段 | 它的值 | 作用 |
|---|---|---|
| 调用前 | sizeof(peer) |
告诉内核:这块缓冲区有多大(防止写越界) |
| 调用后 | 实际写入的字节数 | 告诉调用者:真正填了多少信息 |
在纯网络场景下这两个值往往相等,但接口必须留出这个位置 ------ 因为同一套接口还要支持本地套接字(sockaddr_un),那时结构体长度可能不同。
✔ 使用上的硬规矩:每次调用前都要重新赋值为 sizeof(peer),不能只赋一次。
7.5 读完之后的处理
c
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0) {
buf[s] = 0; // 当字符串看,手动补结尾,否则打印乱码
// 业务处理 + 日志
}
| 返回值 | 含义 |
|---|---|
> 0 |
实际收到的字节数 |
-1 |
出错,errno 被设置 |
💡 缓冲区传 sizeof(buf) - 1 是为了给结尾的 '\0' 留一个位置。
八、sendto():发数据,必须指明发给谁
c
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
8.1 与 recvfrom 的参数对照
| 位置 | recvfrom |
sendto |
差别 |
|---|---|---|---|
| 1 | sockfd |
sockfd |
同一个 |
| 2 | void *buf |
const void *buf |
多了 const |
| 3 | len |
len |
同上 |
| 4 | flags |
flags |
同上 |
| 5 | src_addr 出参 |
dest_addr 入参 |
方向相反 |
| 6 | addrlen 出入参 |
addrlen 入参 |
发送时不需要回填 |
✔ 唯一的本质差别:收的时候地址是「事后拿到」,发的时候地址是「事先必须知道」。
返回值同样是「实际发送的字节数」,失败返回 -1 并设置 errno。

8.2 ★ 全双工
recvfrom 和 sendto 用的是同一个 sockfd。这一点和普通文件不同:
| 对象 | 读 / 写能力 |
|---|---|
| 普通文件 | 以读方式打开只能读、以写方式打开只能写;读写方式打开还要处理读写位置 |
| UDP 套接字 | 同一个 fd 既能读又能写 |
✔ 结论:UDP 通信是全双工的。
回顾「半双工 / 全双工」这一对概念(讲管道时出现过):
| 模式 | 定义 | 现实类比 |
|---|---|---|
| 半双工 | 任一时刻只允许一方发、另一方收 | 日常对话:一方说,另一方听 |
| 全双工 | 双方可以同时收发 | 吵架:两个人同时输出,谁也没听对方 |
💡 补充:管道其实是特殊的半双工。
九、Echo Server 的完整实现
9.1 什么是 Echo
客户端发什么,服务器就回什么。 一句来、一句回,用来验证网络链路是否通。
| 角色 | 动作 |
|---|---|
| 客户端 | 输入一段文字 → 发给服务器 → 等服务器回显 |
| 服务端 | 收到文字 → 原样(可加前缀)→ 发回给来源 |
9.2 为什么第一份网络代码要写 Echo
| 理由 | 说明 |
|---|---|
| UDP 最简单 | 不建连接、不管可靠性、不处理字节流边界,注意力可以集中在接口本身 |
| 语义最好验证 | 收到什么就该是什么,一眼看出对错 |
| 最小可运行骨架 | 换成别的业务,只需要改「处理」那一步 |

9.3 服务端流程
| 阶段 | 动作 |
|---|---|
| 初始化 | socket() → 填写地址结构 → bind() |
| 主循环 | recvfrom() → 判断 → 拼回显字符串 → sendto() → 回到 recvfrom() |
cpp
// 初始化
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { /* 打印 errno,exit(2) */ }
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY; // 关键,见第十一节
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) {
/* 打印 errno,exit(2) */
}
// 主循环
bool is_running = true;
while (is_running) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buf[1024];
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0) {
buf[s] = 0;
// 日志:打印收到的内容
// 日志:打印对端地址(需用 inet_ntoa 转成字符串)
std::string echo = "echo server: ";
echo += buf;
sendto(sockfd, echo.c_str(), echo.size(), 0,
(struct sockaddr *)&peer, len);
}
}
peer 这个变量虽然本讲没有做后续处理,但参数不能省 ------ 一个客户端未来完全可能同时访问多台服务器,所以「这次是谁回的」必须由接口回填。
9.4 命令行参数
服务端启动形式:
bash
./udp_server <server_port>
| 检查 | 处理 |
|---|---|
argc != 2 |
打印 usage,return 1 |
| 端口号 | argv[1] 是字符串,需 atoi 转成整数 |
💡 参数数量必须数准:程序名占 argv[0],所以「一个端口号」对应的 argc 是 2。
十、客户端
10.1 ★ 客户端怎么知道服务器的 IP 和端口
这是很多人第一次写网络程序时的疑问:服务器在别人机房,地址从哪儿来?
答案只有一句话:
✔ 客户端和服务器,是一家公司写的。
| 场景 | 地址从哪来 |
|---|---|
| 抖音客户端 | 内置了字节跳动的服务器 IP 和端口 |
| 淘宝客户端 | 内置了淘宝的服务器地址 |
Linux 的 apt / yum |
软件源地址写在配置里,本质上也是内置 |
| 自己写的客户端 | 启动时由命令行参数传入 |
✔ 所以客户端知道服务器地址一点都不奇怪 ------ 它是随软件一起发布的内置信息。
这也是课堂上能把编译好的客户端分发给所有人、直接连演示用服务器的原因:客户端本身就带着目标地址。
10.2 ★ 客户端要不要 bind
这个问题要分两半回答。
| 问题 | 答案 |
|---|---|
| 客户端需不需要有 IP 和端口? | 需要 ------ 否则无法在全网被定位,对方没法回消息 |
客户端需不需要显式 bind? |
不需要 ,而且强烈不建议 |
那它的地址从哪来?
✔ 首次调用
sendto时,由操作系统自动完成绑定 :IP 用本机 IP,端口从空闲端口里随机取一个。
为什么要随机?
因为「一个端口只能被一个进程占用」。一台手机上装了几十个甚至上百个 App,如果每个都显式绑一个固定端口:
| 情形 | 结果 |
|---|---|
| 京东 App 绑了 6666 | 占用成功 |
| 淘宝 App 也想绑 6666 | 绑不上,起不来 |
| 两家公司不可能事先商量「一方绑 6666、另一方绑 7777」 | ------ |
✔ 结论:客户端用随机端口,是为了避免本机多个客户端之间的端口冲突。
由此可得两条推论:
- 客户端的端口号是几并不重要,唯一就行;
- 同一台机器上所有 App 的 IP 是一样的,区别只在端口。
对照服务端:
| 角色 | 为什么要这样 |
|---|---|
| 服务端 | 会被大量客户端访问,IP 和端口必须众所周知且不能改,一改客户端就找不到 |
| 类比 | 110 / 119 / 120 这类公共服务号码绝不能变;个人手机号则随便换 |

10.3 客户端流程
| 步骤 | 动作 | 说明 |
|---|---|---|
| ① | socket() |
创建套接字 |
| ② | 填写服务端 的 sockaddr_in |
sin_port = htons(server_port)、sin_addr = inet_addr(server_ip) |
| ③ | sendto() |
必须带上服务端地址 |
| ④ | recvfrom() |
等回显;地址参数同样不能省 |
| ⑤ | 关闭 | 释放 fd |
cpp
// 1. 创建
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 填写【服务端】地址
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip);
// 3. 发
std::string input;
std::getline(std::cin, input);
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr *)&server, sizeof(server));
// 4. 收
char buf[1024];
//客户端对接受的信息暂时不作处理,这里只要把后续的参数填充就行了
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0) {
buf[s] = 0;
std::cout << buf << std::endl;
}
💡 注意客户端也可以不 bind :第一次 sendto 时内核自动补上本地地址。但 recvfrom 的后两个参数仍然要填,因为将来可能同时访问多台服务器。
10.4 服务端与客户端的动作顺序
| 角色 | 顺序 |
|---|---|
| 客户端 | 先 发 ,后 收 |
| 服务端 | 先 收 ,后 发 |
✔ 这个顺序差异源自角色分工:请求永远由客户端发起,服务端被动处理。

十一、★ bind 的四种情况
代码写完之后,实际运行时出现了四种现象。这一节是整堂课最有价值的部分,因为它把「地址」这件事彻底讲透了。
11.1 现象汇总
| # | server 绑的地址 | client 用哪个地址访问 | 结果 |
|---|---|---|---|
| ① | 公网 IP(如 8.137.19.140) |
------ | 绑定直接失败 |
| ② | 127.0.0.1 |
127.0.0.1 |
✔ 通 |
| ③ | 内网 IP(如 172.18.45.153) |
同一个内网 IP | ✔ 通 |
| ④ | 内网 IP | 127.0.0.1 |
✖ 访问不了(反过来也不行) |
11.2 公网 IP 为什么绑不上
关键观察:在机器上执行 ifconfig / ip addr,根本看不到那个公网 IP。
原因在于云厂商的地址分配方式:
| 步骤 | 发生的事 |
|---|---|
| ① | 公网 IP 由腾讯云 / 阿里云持有,再分配给用户 |
| ② | 但这个地址并没有真正配置到本机网卡上 |
| ③ | 云厂商内部做了一次公网 IP → 内网 IP 的映射 |
| ④ | 外部报文先到云厂商,再由它转发到对应的内网 IP |
✔ bind 的本质是「占用本机某块网卡上的某个地址」。机器上根本没有这个地址,自然绑不上。
💡 附带的好处是安全:机器不直接暴露在公网上,云厂商在中间挡了一层。后面讲代理服务器时,这件事会更清楚。
⚠ 但这带来一个现实问题:绑不上公网 IP,别人怎么跨网络访问? 课堂上留作后续话题。
11.3 绑了哪个 IP,就只能用哪个 IP 访问
用 netstat 可以直观看到原因。
bash
netstat -naup
参数含义(顺序无关):
| 参数 | 含义 |
|---|---|
a |
显示所有 |
u |
只看 UDP |
n |
全部数字化 (不加 n 会把地址反解成主机名、端口反解成服务名) |
p |
显示进程相关信息 |
绑定内网 IP 时,输出是:
Local Address PID/Program name
172.18.45.153:8080 xxxx/udp_server
而客户端访问的是 127.0.0.1:8080。内核在本机上找「127.0.0.1:8080 这个地址」------不存在,所以访问不了。
✔ 结论:一旦显式 bind 了某个 IP,客户端就必须使用那个 IP 访问,哪怕是同一台机器上的另一个可用 IP 也不行。
11.4 127.0.0.1:本地环回
127.0.0.1 是 loopback(本地环回) 地址。
| 特性 | 说明 |
|---|---|
| 数据去向 | 不会被推送到网络 |
| 实际路径 | 在操作系统内部、经过协议栈转一圈,直接交给对端进程 |
| 前提 | 客户端和服务端必须跑在同一台机器上 |
| 典型用途 | 网络代码的本地测试 |
为什么测试要用它?
因为排障要分层。如果把报文直接扔进网络,一旦通信失败,可能是代码问题,也可能是网络问题。而走本地环回:
✔ 本地通了 → 说明代码是对的 ;这时跨机不通,问题一定在网络。
ifconfig 里能看到它:
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
...
11.5 ★ 推荐做法:INADDR_ANY
既然「公网 IP 绑不上」,「绑了哪个就只能用哪个」,那么服务端到底该绑什么?
✔ 结论:服务端不建议手动绑定任何特定 IP,而是绑定
INADDR_ANY。
c
local.sin_addr.s_addr = INADDR_ANY; /* 值就是 0 */
/* 也可以直接写 0 ------ 0 的大小端都是 0,不需要转换 */
两个理由:
| 序号 | 理由 |
|---|---|
| ① | 公网 IP 本来就不允许绑 |
| ② | 绑了特定 IP 就锁死了入口 |
第二条理由更本质。考虑一台服务器配了多张网卡、多个 IP 的情况:
| 情形 | 结果 |
|---|---|
| 机器有 IP1 / IP2 / IP3,客户端分别用三个 IP 访问同一个 8080 端口 | 三个报文都应该到达同一个服务 |
| 若服务端绑了 IP1 | 只能收到发往 IP1 的报文,另外两个全丢 |
✔ 服务端要的是「凡是发到这个端口的所有报文都收下」,而不是「只收某一个 IP 的报文」。
所以 INADDR_ANY 的语义是:
对内核说:从今往后,凡是访问本机的报文,只要目的端口是 8080,全部收下。
实测效果:netstat 显示 0.0.0.0:8080;客户端用 127.0.0.1、内网 IP、公网 IP 三种地址都能访问成功。
| 观察项 | 绑定具体 IP | 绑定 INADDR_ANY |
|---|---|---|
netstat 显示 |
172.18.45.153:8080 |
0.0.0.0:8080 |
| 可访问的地址 | 只有绑定的那一个 | 本机所有地址 |
| 是否推荐 | ✖ | ✔ |
11.6 由此简化出的构造函数
既然 IP 不需要用户指定,服务端构造时只需要一个端口号:
bash
./udp_server 8080
对应 argc != 2 判断;服务端内部把 sin_addr.s_addr 直接写成 INADDR_ANY。
💡 另外,端口号不能取 0--1023(知名端口),在云服务器上绑定会失败 ------ 应使用 1024 之后的端口,常用 8080 做测试。
十二、把整个程序跑起来
12.1 编译
bash
make # 一次生成 udp_client 与 udp_server 两个可执行文件
12.2 启动
bash
# 终端 1:服务端
./udp_server 8080
# 终端 2:客户端(同机测试,走本地环回)
./udp_client 127.0.0.1 8080
12.3 观察
| 位置 | 现象 |
|---|---|
| 服务端 | netstat -naup 中看到 0.0.0.0:8080,PID 与进程名可见 |
| 客户端 | 提示输入 → 输入「你好」→ 收到 echo server: 你好 |
| 服务端日志 | 打印收到内容,以及来源地址(inet_ntoa 转换后的字符串) |
12.4 让客户端能在任何机器上跑
把客户端编译成静态链接:
bash
g++ udp_client.cc -o udp_client -static -std=c++17
| 方式 | 依赖 | 适用 |
|---|---|---|
| 动态链接(默认) | 依赖目标机器上的第三方库 / libstdc++ | 本机、同环境 |
静态链接(-static) |
不依赖任何第三方库 | 发给别人、跨环境运行 |
十三、速查表
| 接口 | 作用 | 关键点 |
|---|---|---|
socket(AF_INET, SOCK_DGRAM, 0) |
创建 UDP 套接字 | 返回 fd,本质是打开网络文件 |
inet_addr(str) |
字符串 → 网络序 4 字节 | 一步完成两步转换 |
htons(p) |
16 位主机序 → 网络序 | 端口号(2字节)用 |
bind(fd, addr, len) |
绑定本地地址 | 必须强转成 sockaddr *;绑不上就起不来 |
recvfrom(...) |
收数据 + 取对端地址 | 地址是出参 ;addrlen 是出入参 |
sendto(...) |
发数据 + 指定目的地址 | 地址是入参 |
inet_ntoa(addr) |
4 字节 → 字符串 | 打印地址用 |