嵌入式操作系统 | uloop 事件循环:TCP 客户端
本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git
课件、示例代码与验收脚本都在该仓库,可直接
git clone或下载 ZIP 使用。
模块五 · 2 学时 · 环境:VMware + Ubuntu 20.04
一句话概述:把 uloop 用到 TCP 上:先 connect 连上服务器,再把已连接的套接字交给 uloop 监视,定时往服务器发消息并打印回复;重点是看懂 recv 返回 0(服务器关闭连接)这个现象,一步一步给出命令与运行结果,配套课堂练习与课后作业。
本节目标
- 能说清 uloop 版 TCP 客户端与 UDP 版写法的区别(要先 connect,之后只用这一个 fd)
- 会写"连接 + 交给 uloop 监视 + 定时发送"的 TCP 客户端
- 能看懂服务器关闭连接时
recv返回 0 的现象,并知道该怎么处理
一、讲解内容
1.1 知识讲解
TCP 比 UDP 多一步 connect。 UDP 谁都能发;TCP 必须先 connect() 连上服务器(三次握手),成功后拿到一个已连接的套接字,之后所有收发都用它,再也不用写对方地址。
连接也要交给 uloop。 连上以后把这个 fd 挂给 uloop(uloop_fd_add(&ufd, ULOOP_READ)),服务器发数据过来时,uloop 就调用我们的回调去 recv。
recv 的返回值一定要会看(本课重点现象):
| 返回值 | 含义 | 该怎么办 |
|---|---|---|
> 0 |
收到了这么多字节 | 打印/处理数据 |
0 |
对方关闭了连接(不是出错) | 关掉 fd、结束循环或重连 |
< 0 |
出错 | perror 打印后处理 |
定时发送用 uloop 定时器。 本课让客户端每 3 秒发一条消息,用的是 uloop_timeout_set(&tick, 3000)。定时器的细节下节课讲,这里先会用。
为什么不自己写 while 循环。 如果写成 while (1) { sleep(3); send(); recv(); },睡眠期间服务器发来的数据要等醒来才能处理;交给 uloop,收发和定时各走各的回调,互不耽误------这就是事件驱动的写法。
1.2 操作步骤(一条命令一步,每步都有现象)
第 1 步:编译
shell
cd ~/lab/课时17-uloop的TCP客户端
gcc -Wall -Wextra -o uloop_tcp uloop_tcp.c -lubox
现象:无输出即成功。编译要链接 libubox(-lubox),因为 uloop 在 libubox 里。
第 2 步:先启动 Python 回显服务器(终端 A,必须先开)
shell
python3 tcp_echo_server.py 9999
现象:
text
TCP 回显服务器已启动,监听 9999 端口,Ctrl+C 退出
第 3 步:终端 B 运行 C 客户端(后开)
shell
./uloop_tcp 127.0.0.1 9999
现象(等 3 秒开始发第一条,每 3 秒一条;本机实测):
text
① uloop 初始化完成
② 已连接到 127.0.0.1:9999
③ 连接已交给 uloop 监视
④ 进入 uloop_run():每 3 秒发一条,收到回复就打印(按 Ctrl+C 退出)
→ 已发送(15 字节):第 1 条消息
← 收到服务器回复(20 字节):ECHO:第 1 条消息
→ 已发送(15 字节):第 2 条消息
← 收到服务器回复(20 字节):ECHO:第 2 条消息
终端 A(Python 服务器)对应的现象:
text
客户端 127.0.0.1:40080 已连接
收到: 第 1 条消息
收到: 第 2 条消息
第 4 步:把服务器关掉,看客户端反应(重点现象)
在终端 A 按 Ctrl+C 停掉 Python 服务器,回到终端 B 观察:
text
✗ 服务器关闭了连接(recv 返回 0),客户端退出
⑤ uloop 结束,客户端退出
这就是 recv 返回 0 的现场:对方关闭连接属于正常事件,不是错误,程序应当把它当成"该收摊了"(真实项目里通常是重连)。
第 5 步:收工 + 顺序口诀。 服务先开、客户端后开、服务最后关。
1.3 常见错误与排查
| 现象 | 原因 | 解决 |
|---|---|---|
connect(服务器没开?...): Connection refused |
服务器没启动,或端口写错 | 先确认终端 A 打印了"已启动",核对端口 |
Connection timed out |
IP 不通(不同网段/防火墙) | 先 ping 对端IP |
| 连上了但一直没消息 | 定时器还没到 3 秒 | 等 3 秒;本课就是 3 秒一条 |
| 客户端不发数据 | 忘记设置 tick.cb 或没 uloop_timeout_set |
检查第 ④ 步那两行代码 |
| 服务器关了客户端没反应 | 回调里没判断 n <= 0 |
按 1.1 的返回值表处理 |
| 想一直重连却没实现 | 本课只做"断开就退出" | 见作业选做题:用 uloop_timeout_set 3 秒后重新 connect |
二、学生练习
本节命令速查表:
| 命令 | 作用 | 示例 |
|---|---|---|
| gcc ... -lubox | 编译 uloop 程序 | gcc -Wall -Wextra -o uloop_tcp uloop_tcp.c -lubox |
| python3 tcp_echo_server.py 端口 | 启动 Python 回显服务(先开) | python3 tcp_echo_server.py 9999 |
| ./uloop_tcp IP 端口 | 运行 C 客户端 | ./uloop_tcp 127.0.0.1 9999 |
| ss -tnp | grep 端口 | 看 TCP 连接状态(ESTAB) | `ss -tnp |
| Ctrl+C(服务器端) | 停服务器,观察客户端 recv 0 现象 | --- |
| 编号 | 任务 | 提示 | 预期结果 |
|---|---|---|---|
| L17.1 | 按步骤跑通一次 | 先服务器后客户端 | 客户端每 3 秒发一条并收到 ECHO |
| L17.2 | 观察 recv 返回 0 | 服务器端 Ctrl+C | 客户端打印"服务器关闭了连接(recv 返回 0)"并退出 |
| L17.3 | 用 ss 看连接 |
客户端运行时执行 | 能看到 ESTAB 状态的连接 |
| L17.4 | 改发送周期为 1 秒 | 把两处 3000 改成 1000 |
每秒一条 |
| L17.5 | 连同学的虚拟机 | IP 换成同学 IP,两边端口一致 | 同学的服务器打印你发的消息 |
| L17.6 | 服务器没开就运行 | 直接运行客户端 | 报 Connection refused |
三、作业
- 必做:完成 L17.1、L17.2,提交两个终端截图(客户端 + 服务端)与
recv 返回 0的现象截图,并回答:① TCP 客户端为什么要先connect?②recv返回 0 是什么意思,程序应该怎么处理?③ 为什么要把 fd 交给 uloop,而不是自己写 while 循环? - 选做:让客户端在断开后自动重连 :在
recv返回 0 的分支里用uloop_timeout_set(&retry, 3000),3 秒后重新connect(提示:把连接代码抽成一个函数反复调用) - 提交:截图 + 文字回答,命名
课时17-作业截图.png
四、本节小结与下节预告
本节把 uloop 用到 TCP 上:connect 一次 → 交给 uloop 监视 → 定时发送 + 回调收数据,并第一次看到 recv 返回 0 这个"对方关闭"的现场。下节专门讲 uloop 定时器:周期任务怎么做、和一次性任务有什么区别,并把"定时采集 + 网络上报"跑起来。
附录:怎么运行 + 完整代码
一个课件就够用:先按附录 A 的顺序敲命令(服务端/对端先开,自己的程序后开),再按附录 B 抄代码。行尾请用 LF。
附录 A|怎么运行(注意先后顺序)
shell
gcc -Wall -Wextra -o uloop_tcp uloop_tcp.c -lubox
python3 tcp_echo_server.py 9999 # 终端A:先启动回显服务
./uloop_tcp 127.0.0.1 9999 # 终端B:再运行客户端(Ctrl+C 停服务器看 recv 0 现象)
附录 B|文件 uloop_tcp.c
c
/*
* uloop_tcp.c ------ 课时 17:用 uloop 写一个 TCP 客户端
*
* 编译:gcc -Wall -Wextra -o uloop_tcp uloop_tcp.c -lubox
* 运行:./uloop_tcp 127.0.0.1 9999
*
* 三件事:
* ① connect() 连上服务器(阻塞式,简单直接)
* ② 把 fd 交给 uloop 监视:服务器发回来什么,就打印什么
* ③ 每 3 秒主动发一条消息(计数器),演示"定时 + 收发"在同一个事件循环里
* 服务器关掉后,recv 返回 0,客户端打印断开信息并退出。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <libubox/uloop.h>
#define BUF_SIZE 512
static struct uloop_fd ufd;
static struct uloop_timeout tick; /* 每 3 秒发一条消息 */
static int counter = 0;
/* 服务器发数据来了 */
static void on_readable(struct uloop_fd *f, unsigned int events)
{
char buf[BUF_SIZE + 1];
ssize_t n;
(void)events;
n = recv(f->fd, buf, BUF_SIZE, 0);
if (n > 0) {
buf[n] = '\0';
printf("← 收到服务器回复(%zd 字节):%s\n", n, buf);
return;
}
/* recv 返回 0:对方关闭了连接;返回负数:出错 */
if (n == 0) {
printf("✗ 服务器关闭了连接(recv 返回 0),客户端退出\n");
} else {
perror("recv");
}
uloop_fd_delete(&ufd);
close(f->fd);
uloop_end();
}
/* 定时器:每 3 秒往服务器发一条 */
static void on_tick(struct uloop_timeout *t)
{
char msg[64];
ssize_t n;
(void)t;
counter++;
snprintf(msg, sizeof(msg), "第 %d 条消息", counter);
n = send(ufd.fd, msg, strlen(msg), 0);
if (n < 0) {
perror("send");
} else {
printf("→ 已发送(%zd 字节):%s\n", n, msg);
}
uloop_timeout_set(&tick, 3000); /* 重新排下一次(3 秒后) */
}
int main(int argc, char *argv[])
{
struct sockaddr_in server;
int port;
int fd;
if (argc < 3) {
printf("用法: %s <服务器IP> <端口>\n", argv[0]);
printf("例: %s 127.0.0.1 9999\n", argv[0]);
return 1;
}
port = atoi(argv[2]);
uloop_init();
printf("① uloop 初始化完成\n");
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, argv[1], &server.sin_addr) != 1) {
printf("IP 地址不对:%s\n", argv[1]);
return 1;
}
/* ② 连接服务器(这一步是三次握手,和课时 13 一样) */
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
if (connect(fd, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("connect(服务器没开?IP/端口对不对?)");
close(fd);
return 1;
}
printf("② 已连接到 %s:%d\n", argv[1], port);
/* ③ 把已连接的 fd 交给 uloop 监视读事件 */
ufd.fd = fd;
ufd.cb = on_readable;
uloop_fd_add(&ufd, ULOOP_READ);
printf("③ 连接已交给 uloop 监视\n");
/* ④ 3 秒后开始周期性发送 */
tick.cb = on_tick;
uloop_timeout_set(&tick, 3000);
printf("④ 进入 uloop_run():每 3 秒发一条,收到回复就打印(按 Ctrl+C 退出)\n");
uloop_run();
uloop_done();
printf("⑤ uloop 结束,客户端退出\n");
return 0;
}
附录 C|文件 tcp_echo_server.py
python
#!/usr/bin/env python3
"""课时 17 测试工具:TCP 回显服务器(收到一行就回一行)
用法:python3 tcp_echo_server.py [端口] # 端口默认 9999,Ctrl+C 退出
提示:按 Ctrl+C 停掉它,正好能观察 C 客户端打印"服务器关闭了连接"。
"""
import socket
import sys
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9999
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", port))
srv.listen(5)
print(f"TCP 回显服务器已启动,监听 {port} 端口,Ctrl+C 退出")
try:
while True:
conn, addr = srv.accept()
print(f"客户端 {addr[0]}:{addr[1]} 已连接")
with conn:
while True:
data = conn.recv(1024)
if not data:
break
text = data.decode("utf-8", "replace")
print(f"收到: {text}")
conn.sendall(f"ECHO:{text}".encode())
print(f"客户端 {addr[0]}:{addr[1]} 断开")
except KeyboardInterrupt:
print("\n服务器退出")
finally:
srv.close()