Socket 编程基础:从概念到实战

Socket 编程,说白了就是让两个设备通过网络互相通信的接口。我刚开始接触嵌入式网络时,总觉得 Socket 是个很玄乎的东西——直到我在一个工业数据采集项目里,用 Zephyr 的 Socket API 把传感器数据发到服务器,才真正理解了它的本质。

嗯,咱们今天就把 Socket 这个概念拆开揉碎了讲清楚。你想想看,网络通信和打电话其实很像:你要先有根电话线(网络接口),知道对方的号码(IP 地址和端口),拨号(连接),然后才能说话(收发数据)。Socket 就是那个电话听筒。

Socket 是什么?

Socket 是一个抽象层,它屏蔽了底层网络协议的复杂性。在 Zephyr 里,Socket 本质上是一个文件描述符——对,就是和你在文件系统里操作文件用的那个东西差不多。

核心要点:Socket 是应用层与传输层之间的接口。你往 Socket 里写数据,它帮你封装成 TCP 或 UDP 报文发出去;你从 Socket 里读数据,它帮你把收到的报文解出来。

我在项目中遇到过一个问题:有人把 Socket 和网络接口混为一谈。其实 Socket 只是一个软件抽象,它不关心底层用的是以太网、Wi-Fi 还是蜂窝网络。你只要把数据交给 Socket,它自己会找路出去。

Socket 的三种类型

Zephyr 支持三种 Socket 类型,每种对应不同的通信场景。我整理了一个表格,方便你对比:

类型 协议 特点 典型场景
SOCK_STREAM TCP 面向连接、可靠、有序、无边界 文件传输、网页浏览、远程控制
SOCK_DGRAM UDP 无连接、不可靠、有边界、低延迟 实时音视频、传感器数据上报
SOCK_RAW IP/ICMP 直接操作网络层、需要特权 Ping 工具、路由协议、网络诊断

你可能会问:什么时候用 TCP,什么时候用 UDP?我的经验是:如果数据丢了会出大问题(比如控制指令),用 TCP;如果丢几个包无所谓但延迟必须低(比如音频流),用 UDP。至于 RAW Socket,我在调试网络协议栈时用过几次,平时很少碰。

Zephyr Socket API 概览

Zephyr 的 Socket API 基本遵循 POSIX 标准,但做了一些嵌入式场景下的优化。我建议你记住这几个核心函数:

  • socket() — 创建 Socket,返回文件描述符
  • bind() — 绑定本地地址和端口
  • listen() — 监听连接请求(仅 TCP 服务端)
  • accept() — 接受连接(仅 TCP 服务端)
  • connect() — 发起连接(仅 TCP 客户端)
  • send() / recv() — 收发数据
  • close() — 关闭 Socket

小提示:Zephyr 的 Socket API 默认是阻塞模式。如果你需要非阻塞操作,可以用 fcntl() 设置 O_NONBLOCK 标志。我在一个低功耗设备上用过非阻塞模式,配合 poll() 轮询,效果不错。

Socket 的创建与关闭

咱们直接看代码。创建一个 TCP Socket 的典型写法是这样的:

#include <zephyr/net/socket.h>

int sock;

sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) {
    printk("Socket 创建失败,错误码: %d\n", errno);
    return -1;
}

// ... 使用 Socket ...

close(sock);

这里有几个参数要注意:

  • AF_INET:地址族,IPv4 用这个。IPv6 用 AF_INET6。
  • SOCK_STREAM:Socket 类型,TCP 用这个。UDP 用 SOCK_DGRAM。
  • IPPROTO_TCP:协议,TCP 用这个。UDP 用 IPPROTO_UDP。

注意:创建 Socket 后一定要检查返回值。我曾经在一个项目中忘了检查,结果 Socket 创建失败,后续所有操作都基于一个无效的文件描述符,调试了整整一天才找到问题。

关闭 Socket 时,close() 会立即返回,但内核可能还在发送缓冲区里的数据。如果你需要确保数据全部发送完毕再关闭,可以用 shutdown() 先关闭写端:

// 先关闭写端,确保数据发送完毕
shutdown(sock, SHUT_WR);

// 然后关闭 Socket
close(sock);

嗯,这里要提一个我踩过的坑:在 Zephyr 里,close() 和 POSIX 标准略有不同。Zephyr 的 close() 会释放所有资源,但如果你在多线程环境下使用同一个 Socket,记得加锁保护。

Socket 生命周期流程图

为了让你更直观地理解 Socket 从创建到关闭的完整流程,我画了一张图:

Socket 生命周期流程图 socket() 创建 bind() 绑定地址 TCP 服务端 TCP 客户端 / UDP listen() 监听 accept() 接受连接 connect() 连接 send() / recv() close() 关闭 说明 UDP 不需要 connect() 直接 sendto()/recvfrom()

从这张图你可以看到,TCP 服务端和客户端的流程差异很大。服务端需要先绑定地址、监听、然后接受连接;客户端直接发起连接就行。UDP 更简单,连连接都不需要,直接收发数据。

常见错误与避坑指南

我在 Zephyr 上做 Socket 编程时,遇到过几个典型问题,分享给你:

  • Socket 创建失败:最常见的原因是内存不足或 Socket 数量超限。Zephyr 默认的 Socket 数量有限,可以在 prj.conf 里调整 CONFIG_NET_SOCKETS_POSIX_NAMES=y 和 CONFIG_NET_MAX_SOCKETS。
  • bind() 返回 EADDRINUSE:端口被占用了。我建议你在调试时用 SO_REUSEADDR 选项:setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
  • close() 后资源未释放:Zephyr 的 Socket 实现里,如果还有线程在阻塞等待该 Socket 的 I/O,close() 可能不会立即释放资源。最好先确保所有 I/O 操作都结束了再关闭。

曾经我踩过的坑:在一个需要长时间运行的设备上,我忘了关闭不再使用的 Socket,结果系统运行几天后 Socket 资源耗尽,网络完全瘫痪。从那以后,我养成了一个习惯:每个 Socket 创建后,都在代码里明确标注它在哪个位置关闭。

好了,Socket 的基础概念、类型、API 概览以及创建关闭的细节,咱们都过了一遍。这些是后续所有网络编程的基石。你可以在 Zephyr 的 samples/net/sockets/ 目录下找到很多现成的例子,边看边练,效果最好。