12 嵌入式操作系统 | UDP 服务器编程

嵌入式操作系统 | UDP 服务器编程

模块四 · 2 学时 · 环境:VMware + Ubuntu 20.04

一句话概述:用 C 写一个 UDP 服务器(socket / bind / recvfrom / sendto 回包),讲清服务器为什么必须 bind、INADDR_ANY 是什么意思,并用 Python 写的测试客户端发包验证;文档给出启动顺序、命令与运行结果,配套课堂练习与课后作业。

本节目标

  1. 能说出服务器和客户端的区别,解释服务器为什么必须 bind
  2. 会用 socket / bind / recvfrom / sendto 写出一个收包并回包的 UDP 服务器
  3. 会先把 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("测试结束")
相关推荐
青梅味猪大肠1 小时前
【操作系统-24】双标志先检查
linux·运维·服务器
华允物联-HUAIOT1 小时前
带隔离的串口服务器有必要买吗?隔离作用对比
服务器·物联网
嵌入式小能手2 小时前
飞凌嵌入式ElfBoard-Shell编程应用实例-使用WiFi拨号上网
linux·服务器·网络
..Dauntless..2 小时前
【Linux】环境变量
linux·运维·服务器
极光通讯2 小时前
服务器CPU现货选型:Intel Xeon Scalable vs AMD EPYC平台适配指南
运维·服务器
怪奇云呼军3 小时前
从 ElevenLabs 看工具调用:闪电智能 Voice Agent 的企业集成验收设计
android·大数据·运维·服务器·网络·人工智能·kotlin
VidDown3 小时前
文件没问题但网页播不了:播放器接入实战与那些“环境问题
python·网络协议·音视频·视频编解码·视频
liangshanbo12154 小时前
面试题:HTTP/2有哪些升级
网络·网络协议·http
兴通物联科技4 小时前
对俄出口诚实标识 2026 时间节点与产线赋码采集技术方案
大数据·服务器·单片机·嵌入式硬件·深度学习·计算机视觉