嵌入式操作系统 | UDP 服务器编程
模块四 · 2 学时 · 环境:VMware + Ubuntu 20.04
一句话概述:用 C 写一个 UDP 服务器(socket / bind / recvfrom / sendto 回包),讲清服务器为什么必须 bind、INADDR_ANY 是什么意思,并用 Python 写的测试客户端发包验证;文档给出启动顺序、命令与运行结果,配套课堂练习与课后作业。
本节目标
- 能说出服务器和客户端的区别,解释服务器为什么必须 bind
- 会用 socket / bind / recvfrom / sendto 写出一个收包并回包的 UDP 服务器
- 会先把 C 服务器跑起来,再用 Python 工具发包测试 ,并会用
ss确认端口在监听
一、讲解内容
1.1 知识讲解
服务器与客户端的区别。 客户端是"主动去发"的一方,服务器是"一直等着收"的一方。上节课的客户端不写 bind(系统自动给临时端口),而服务器必须 bind 到固定的端口,因为别人只有知道端口才能找到它------就像快递必须有个固定地址。
服务器四步:
| 步骤 | 函数 | 在干嘛 |
|---|---|---|
| 1 | socket(AF_INET, SOCK_DGRAM, 0) |
创建 UDP 套接字 |
| 2 | bind(...) |
绑定自己的 IP 和端口,之后别人才能发数据给你 |
| 3 | recvfrom(...) |
收数据;它会把"是谁发的"(对方的 IP 和端口)一起带回来 |
| 4 | sendto(...) |
用第 3 步带回来的对方地址把结果回过去 |
INADDR_ANY 是什么。 bind 时填 htonl(INADDR_ANY)(等于 0.0.0.0),意思是"本机所有网卡都收":不管是 127.0.0.1 还是虚拟机网的 192.168.x.x,只要有包发到这个端口,服务器都收得到。如果只绑 127.0.0.1,那只有本机自己能连上。
为什么可以一直循环收。 服务器写成 while (1) { recvfrom ... sendto ... }:没有包时 recvfrom 会睡着不占 CPU,来一个包就处理一个。这就是最朴素的"网络服务"模型(下节课的 TCP 服务器也用这个思路,只是多了连接管理)。
为什么服务器也要能回包。 实际项目里,客户端发完数据往往要等一个"收到"的确认,这样才知道数据没丢。本课让服务器回一句"服务器已收到 N 字节:xxx",方便你在客户端那侧直接看到效果。
1.2 怎么运行(两个终端,服务器先开、测试工具后开)
text
终端 A(C 服务器) 终端 B(Python 测试客户端)
① 第 1 个启动,打印"已启动" ──────► ② 看到启动信息后,再发包
③ 打印收到的每个包 ③ 打印已发送/收到的回复
④ 按 Ctrl+C 停止 ④ 发完自动退出
第 1 步:在终端 A 编译并启动 C 服务器(先做这一步):
shell
gcc -Wall -Wextra -g udp_server.c -o udp_server # 编译(无输出=成功)
./udp_server 9999 # 启动,监听 9999 端口
看到这一行才算启动成功:
text
UDP 服务器已启动,监听 9999 端口,按 Ctrl+C 退出
第 2 步:在终端 B 确认端口在监听(不是必须,但很有用,出问题时靠它定位):
shell
ss -ulnp | grep 9999
正常会看到类似 udp UNCONN 0 0 0.0.0.0:9999 0.0.0.0:* 的输出,说明端口已经打开。
第 3 步:在终端 B 用 Python 工具发包(服务器开着的时候才做):
shell
python3 udp_sender.py 127.0.0.1 9999 3 # 发 3 条并等回复
第 4 步:对照两个终端的输出(本机真实运行结果)。
终端 B(Python 测试客户端):
text
已发送: sensor-1: 温度 21.5 C
收到回复: 服务器已收到 23 字节:sensor-1: 温度 21.5 C
已发送: sensor-2: 温度 22.5 C
收到回复: 服务器已收到 23 字节:sensor-2: 温度 22.5 C
已发送: sensor-3: 温度 23.5 C
收到回复: 服务器已收到 23 字节:sensor-3: 温度 23.5 C
测试结束
终端 A(C 服务器):
text
UDP 服务器已启动,监听 9999 端口,按 Ctrl+C 退出
收到 127.0.0.1:37085 的 23 字节:sensor-1: 温度 21.5 C
收到 127.0.0.1:37085 的 23 字节:sensor-2: 温度 22.5 C
收到 127.0.0.1:37085 的 23 字节:sensor-3: 温度 23.5 C
一边发、一边收,两边的字节数一样,就说明服务器工作正常。
第 5 步:再玩两个花样。 ① 用客户端来测:如果课时 11 的 C 客户端还在,./udp_client 127.0.0.1 9999 你好 也能收到本课服务器的回包;② 故意先把服务器 Ctrl+C 停掉,再用 Python 发包,会看到每条都打印"2 秒内没有收到回复"------这就是"服务器没开"的现象。
第 6 步:收工。 回到终端 A 按 Ctrl+C 停服务器。顺序口诀:服务器先开、测试工具后开;服务器最后关。
1.3 常见错误与排查
| 现象 | 原因 | 解决 |
|---|---|---|
bind(端口被占用?...): Address already in use |
上一次的服务器没关,端口被占着 | Ctrl+C 停掉旧的,或换个端口(如 8888) |
2 秒内没有收到回复 |
服务器没启动,或端口不一致 | 先看终端 A 有没有打印"已启动",再核对端口号 |
| 本机能通,别的机器发不进来 | 绑到了 127.0.0.1 |
用 htonl(INADDR_ANY) 绑所有网卡 |
permission denied |
用了 1024 以下的端口(如 80) | 换 1024 以上的端口(本课用 9999) |
| 服务器收到乱码 | 客户端发的不是 UTF-8 | 统一用 UTF-8 编码发送和显示 |
二、学生练习
本节命令速查表:
| 命令 | 作用 | 示例 |
|---|---|---|
| gcc 源文件 -o 程序 | 编译 C 服务器 | gcc -Wall -Wextra -g udp_server.c -o udp_server |
| ./udp_server 端口 | 启动服务器(终端 A,先开) | ./udp_server 9999 |
| ss -ulnp | grep 端口 | 查看 UDP 端口监听情况 | `ss -ulnp |
| python3 udp_sender.py IP 端口 条数 | 用 Python 工具发包(终端 B) | python3 udp_sender.py 127.0.0.1 9999 3 |
| Ctrl+C | 停止服务器 | 在终端 A 按 |
| 编号 | 任务 | 提示 | 预期结果 |
|---|---|---|---|
| L12.1 | 按文档跑通一次:先服务器、后发包 | 两个终端,注意顺序 | 服务器打印 3 个包,客户端收到 3 条回复 |
| L12.2 | 换端口 8888 再来一次 | 服务器参数和 Python 参数都要改 | 依然收发成功 |
| L12.3 | 停掉服务器再发包 | 先按 Ctrl+C,再运行 Python | 每条都提示"2 秒内没有收到回复" |
| L12.4 | 用 ss 看监听状态 |
启动服务器前后各执行一次 | 启动后能看到 9999 端口 |
| L12.5 | 服务器改成回包内容不同(例如回"OK") | 改 snprintf 那一行,重新编译 |
客户端收到的回复变成新的内容 |
| L12.6 | 和课时 11 的 C 客户端联调 | 服务器本课写的,客户端用课时 11 的 | 两边输出互相对应 |
三、作业
- 必做:完成 L12.1、L12.4,提交两个终端截图(服务器输出 + Python 客户端输出)与
ss -ulnp | grep 9999的截图,并回答:① 服务器为什么必须 bind,客户端为什么不用?②INADDR_ANY是什么意思? - 选做:把服务器改成"收到什么就原样回什么"(回显服务器),再和课时 11 的客户端互测
- 提交:截图 + 文字回答,命名
课时12-作业截图.png
四、本节小结与下节预告
本节写出了 UDP 服务器:socket → bind → 循环 recvfrom → sendto 回包,并学会用 ss 检查端口、用 Python 工具当客户端做测试,再次确认了"服务先开、测试后开"的顺序 。下节进入 TCP:同样是客户端,但先要 connect 完成"三次握手",可靠但不丢数据。
附录:怎么运行 + 完整代码
一个课件就够用:先看附录 A 的运行顺序(服务端先开、客户端后开 ),
再把附录 B/C 的代码按文件名新建到同一个目录里。行尾请用 LF。
附录 A|怎么编译运行(注意先后顺序)
shell
# 编译 C 服务器
gcc -Wall -Wextra -g udp_server.c -o udp_server
# 第 1 步:终端 A 先启动服务器(必须先做,不能颠倒)
./udp_server 9999
# 看到 "UDP 服务器已启动,监听 9999 端口" 才算成功
ss -ulnp | grep 9999 # (可选)确认端口在监听
# 第 2 步:终端 B 用 Python 工具发包并等回复
python3 udp_sender.py 127.0.0.1 9999 3
# 第 3 步:对照两个终端的输出
# 收工:回到终端 A 按 Ctrl+C 停止服务器
附录 B|文件 udp_server.c
c
/*
* udp_server.c ------ 课时 12:UDP 服务器(收包 → 打印 → 回包)
*
* 编译:gcc -Wall -Wextra -g udp_server.c -o udp_server
* 运行:./udp_server # 默认监听 9999 端口
* ./udp_server 8888 # 也可以指定端口
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 512
int main(int argc, char *argv[])
{
int sock;
int port = 9999;
char buf[BUF_SIZE];
char reply[BUF_SIZE + 32];
struct sockaddr_in addr;
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
ssize_t n;
if (argc >= 2) {
port = atoi(argv[1]);
}
/* 1. 创建 UDP 套接字 */
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
/* 2. 绑定自己的地址和端口:服务器必须 bind,客户端不用
* INADDR_ANY 表示本机所有网卡(0.0.0.0)都收包 */
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((uint16_t)port);
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind(端口被占用?换个端口试试)");
close(sock);
return 1;
}
printf("UDP 服务器已启动,监听 %d 端口,按 Ctrl+C 退出\n", port);
fflush(stdout); /* 输出重定向到文件时是块缓冲,不刷新就看不到 */
/* 3. 循环收包:recvfrom 会告诉我们是哪一个客户端发来的 */
while (1) {
memset(buf, 0, sizeof(buf));
n = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&peer, &peer_len);
if (n < 0) {
perror("recvfrom");
continue;
}
printf("收到 %s:%d 的 %zd 字节:%s\n",
inet_ntoa(peer.sin_addr), ntohs(peer.sin_port), n, buf);
fflush(stdout); /* 实时看到收到的包 */
/* 4. 回一句话给客户端:地址用 recvfrom 带回来的 peer */
snprintf(reply, sizeof(reply), "服务器已收到 %zd 字节:%s", n, buf);
if (sendto(sock, reply, strlen(reply), 0,
(struct sockaddr *)&peer, peer_len) < 0) {
perror("sendto");
}
}
close(sock);
return 0;
}
附录 C|文件 udp_sender.py
python
#!/usr/bin/env python3
"""课时 12 测试工具:UDP 测试客户端(给 C 服务器发包并等回复)
用法:python3 udp_sender.py [IP] [端口] [条数]
例:python3 udp_sender.py 127.0.0.1 9999 3
"""
import socket
import sys
import time
ip = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 9999
count = int(sys.argv[3]) if len(sys.argv) > 3 else 3
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
for i in range(1, count + 1):
msg = f"sensor-{i}: 温度 {20 + i}.5 C"
s.sendto(msg.encode(), (ip, port))
print(f"已发送: {msg}")
try:
data, addr = s.recvfrom(1024)
print(f"收到回复: {data.decode('utf-8', 'replace')}")
except socket.timeout:
print("2 秒内没有收到回复")
time.sleep(0.5)
s.close()
print("测试结束")