17 嵌入式操作系统 | uloop 事件循环:TCP 客户端

嵌入式操作系统 | uloop 事件循环:TCP 客户端

本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git

课件、示例代码与验收脚本都在该仓库,可直接 git clone 或下载 ZIP 使用。
模块五 · 2 学时 · 环境:VMware + Ubuntu 20.04

一句话概述:把 uloop 用到 TCP 上:先 connect 连上服务器,再把已连接的套接字交给 uloop 监视,定时往服务器发消息并打印回复;重点是看懂 recv 返回 0(服务器关闭连接)这个现象,一步一步给出命令与运行结果,配套课堂练习与课后作业。

本节目标

  1. 能说清 uloop 版 TCP 客户端与 UDP 版写法的区别(要先 connect,之后只用这一个 fd)
  2. 会写"连接 + 交给 uloop 监视 + 定时发送"的 TCP 客户端
  3. 能看懂服务器关闭连接时 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()
相关推荐
要吃这碗饭2 小时前
深入理解 TCP 协议:从三次握手到可靠传输
网络·网络协议·tcp/ip
会员果汁3 小时前
函数调用和变长参数
c语言
大侠归来4 小时前
TCP/IP 网络编程:多播与广播
网络·tcp/ip·智能路由器
白山编程大哥6 小时前
Lua 中使用 C 语言的用户自定义类型——userdata
c语言·junit·lua
无敌贵点大王6 小时前
RTThread学习记录13——关于要使用一个外设,RTThread与cubemx到底要怎么配合?
c语言·stm32·学习·rtthread
phltxy6 小时前
C 语言动态内存管理:从申请空间到安全释放
c语言·开发语言·ui
上位机妹子7 小时前
C 语言 数组删除指定元素(快慢指针法)
c语言·数据结构·算法
心中有你02147 小时前
【路径规划】A*寻路算法最通俗易懂讲解(C语言完整实现+详细注释)
c语言·开发语言·算法
大圣编蚕8 小时前
C语言入门:基本语法汇总
c语言·开发语言