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