《OpenHarmony物联网开发应用教程》 · 第6章OpenHarmony网络编程

原创2026-08-21 10:41:41浏览0
0
Star

第6章讲了四个socket案例:TCP客户端、TCP服务器、UDP客户端、UDP服务器。放在一起看,刚好是传输协议和角色的2x2组合。

另外这一章用的是Adam Dunkels写的LwIP协议栈,十几KB RAM就能跑完整TCP/IP。还列了一串特性:IP转发、ICMP、DHCP、Berkeley socket API等等。不过四个案例真正用到的接口其实不多,有8个(socket、connect、bind、sendto、recvfrom、getsockopt、setsockopt、close)。

这章也覆盖了Wi-Fi的AP模式和STA模式(6.2节),但那部分更偏硬件配置层,我这次主要聚焦在6.3节的socket编程上。

先看TCP这条线

TCP客户端的流程是:连上Wi-Fi,建socket,connect服务器,然后循环send和recv。代码不长,但有两处让我停下来想了想。

第一处是send和recv之间有个sleep(2)。TCP服务器那边也有类似的结构——accept之后循环recv和send。读的时候我一开始以为这是什么同步机制,后来才反应过来可能只是减速。

第二处是TCP服务器里closesocket的那行。客户端断开之后,代码break出内层循环,然后执行了closesocket(g_listen_fd)。我盯着这行看了半天——g_listen_fd是监听socket?客户端断开应该关的是client_fd才对。而且后面直接return 0,整个函数就结束了。这让我有点困惑:这是有意设计,演示完一个客户端就退出。但对实际应用来说,这里需要回外层循环继续accept下一个连接。

/* TCP客户端的核心循环 */
connect(client_fd, &send_addr, sizeof(send_addr));

while (1) {

    send(client_fd, sendBuf, strlen(sendBuf)+1, 0);

    sleep(2);  /* 教学减速,不是同步机制 */

    recv(client_fd, recvBuf, sizeof(recvBuf), 0);

}

再看UDP这条线

UDP客户端的流程更短:建socket,sendto发数据报,sleep(10),recvfrom收回复。不需要connect——UDP是无连接的,直接往地址扔数据就行。UDP服务器也简单:bind之后循环recvfrom和sendto,不用listen也不用accept。

对比一下就能看出TCP和UDP的差异不在于代码量,而在于「连接」这个概念。TCP多了connect和accept这组握手,背后是协议栈帮你管着连接状态、重传、顺序。UDP把这些全丢掉了,换来的是更少的开销和更快的速度。四份代码摆在一起,差异最明显的地方恰好就是connect/accept这对函数的有无——这个对比比单独讲TCP或UDP更直观。

UDP客户端有个sleep(10)让我注意到了——10秒在物联网场景里可不短。如果是做实时上报,这个延迟几乎不可接受。当然教学代码不需要考虑这个,真机部署的话这里应该换成recvfrom加超时(setsockopt设SO_RCVTIMEO),让程序在没数据时也不干等。

四份代码放在一起看

四份案例的结构高度相似:Wi-Fi连接,建socket,设参数,循环收发。这种相似性本身就是一种教学——你不需要记四套不同的流程,记住一个框架,然后在connect/accept还是sendto/recvfrom这个分叉点做选择就行。

SO_REUSEADDR的设置出现在TCP服务器里,setsockopt(g_listen_fd, SOL_SOCKET, SO_REUSEADDR, ...)。这个选项让端口在TIME_WAIT状态时也能被重新绑定,服务器重启时不会因为端口占用而bind失败。虽然只是几行代码,但这种防御性写法读着让人踏实。

不过四份代码的重复度确实高。Wi-Fi连接、socket创建、收发循环的框架几乎一样。从教学角度这是好事——重复让人记住结构;但如果要在实际项目里用,这些公共逻辑抽一个基类出来会更清爽。

TCP和UDP之间还有一个不那么显眼的差异:TCP的recv是流式的,一次recv不一定收完一帧;UDP的recvfrom是消息式的,一次recvfrom对应一次sendto。这个差异在教学代码里看不出来(因为收发节奏被sleep控制了),但在真实场景里它会影响到你怎么设计消息边界。

读完之后我写了一份Python实现,核心想法是把四份代码的公共逻辑抽到一个基类NetworkEndpoint里。基类管socket的生命周期和收发循环,子类只需要告诉它「我是TCP还是UDP」。这样四份代码变成了一棵小继承树,读起来比四份平行的文件要紧凑一些。

另外做了几个延伸。TCP服务器那段,我试了关client_fd然后回外层循环继续accept,还给每个客户端起了独立线程。UDP那边sleep(10)换成了recvfrom带超时。客户端加了断线自动重连,用指数退避避免连得太频繁。

最有意思的延伸是把第5章的GPS坐标数据通过TCP传到远端。GPS数据封装成JSON发过去,服务器收到解析显示。算是把5.7的采集和6.3的传输连了一条线——虽然只是模拟,但跑起来的时候确实有点「物联网」的感觉了。

# 基类统一四份代码的公共逻辑
class NetworkEndpoint(ABC):
    @abstractmethod
    def _create_socket(self):
        pass  # 子类只需说:TCP还是UDP

# TCP服务器修正后的循环结构
while self._running:
    client_fd, addr = server.accept()
    Thread(target=self._handle_client,
           args=(client_fd, addr)).start()
    # 处理完关client_fd,回外层继续accept

整理了一张对照表:

书本的C代码

我的Python实现

在做什么

WifiConnect(SSID, PWD)

PC直接有网络,省略

联网

socket(AF_INET, SOCK_STREAM)

socket.socket(...)

建socket

connect/send/recv

connect/send/recv

TCP通信

bind/listen/accept

bind/listen/accept

TCP服务端

sendto/recvfrom

sendto/recvfrom

UDP通信

sleep(10)后recvfrom

recvfrom加超时

UDP接收

return 0(退出函数)

循环继续accept+自动重连

生命周期

跑起来长什么样

同时启动TCP服务器和GPS数据上报客户端。服务器监听8888端口,客户端连上后每隔一会发一次坐标数据:

[TCP Server] 监听 0.0.0.0:8888
[TCP Client] 连接 127.0.0.1:8888
[TCP Client] 发送: gps数据 lat:22.54312 lon:114.05791
[TCP Server] 收到GPS数据 纬度:22.54312N
[TCP Client] 连接断开,3秒后重连...

这一章最让我有收获的地方不是某个具体的API,而是四份代码放在一起时那种「同构感」。TCP和UDP的差异,被压缩成了connect/accept这对函数的有无,以及send/recv与sendto/recvfrom的切换。教学上这种控制变量的排列方式确实比逐个讲更高效——你不用记住四套流程,只需记住一个框架加一个分叉点。

但同构感也带来一个隐患:太整齐的排列容易让人觉得差异只在代码层面。实际上TCP的流式recv和UDP的消息式recvfrom背后是两种完全不同的数据模型,在真实场景里它会决定你怎么设计消息边界。这个差异在教学代码里被sleep掩盖了,只有当你去掉sleep、面对真实的收发节奏时才会暴露出来。

另外把第5章的GPS数据用第6章的TCP传出去之后,我突然意识到这两章合起来已经走完了一条完整的物联网链路:传感器采集数据、本地处理、网络传输到远端。虽然只是模拟,但这个「完整通路」的感觉比单独读一章要强烈得多。