title: 网络基础与socket编程 tags: [
嵌入式Linux,
Linux应用编程,
网络编程,
socket,
TCP,
UDP,
OSI,
IP地址,
端口号,
字节序,
htonl,
inet_pton,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[04-网络编程与项目实战/02-CAN应用编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/14. socket网络编程基础]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/16. UDP编程]];深入 → [[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]
网络通信本质上是进程间通信(IPC),只不过通信双方位于不同主机上,因此又称 socket IPC。Linux 内核把复杂的 TCP/IP 协议族隐藏在 socket 接口之后,应用层按固定套路调用几个函数即可完成跨主机通信。本篇先补齐网络基础知识(模型、IP、端口、协议),再系统讲解 socket 编程 API,最后落到可编译运行的 TCP 服务端/客户端,并延伸 UDP 与并发服务器。
网络通信分三个层次:
| 层次 | 角色 | 说明 |
|---|---|---|
| 硬件层 | 网卡设备 | 收发网络数据的物理基础 |
| 驱动层 | 网卡驱动 | 内核中的网卡驱动,向应用层提供 socket 接口 |
| 应用层 | 应用程序 | 基于 socket 接口编程,或使用 http、网络控件等更高封装 |
硬件上两台主机各有网卡,通过传输媒介相连(有线:双绞线、光纤;无线:WiFi、蓝牙、ZigBee、4G/5G)。内核驱动底层网卡并暴露 socket 接口;应用层调用 socket 接口即所谓网络编程。
flowchart TB
A["应用层:HTTP / MQTT / 自定义协议(调用 socket 接口)"] --> B["内核层:网卡驱动(提供 socket 接口)"]
B --> C["硬件层:网卡设备"]
C <-->|"传输媒介:网线 / 光纤 / 无线"| D["对端主机的网卡 / 内核 / 应用"]
OSI(Open System Interconnection)是国际标准化组织(ISO)制定的网络互连标准体系,从上到下依次为:
| 层 | 名称 | 主要功能 | 典型协议 / 设备 |
|---|---|---|---|
| 7 | 应用层 | 为用户提供应用接口与网络服务 | HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP |
| 6 | 表示层 | 数据编码/转换、压缩/解压缩、加密/解密 | — |
| 5 | 会话层 | 建立、管理和终止通信会话 | — |
| 4 | 传输层 | 定义端口号、端到端流控和差错校验 | TCP、UDP |
| 3 | 网络层 | 逻辑地址寻址、路由选择(IP 层) | IP(IPv4/IPv6)、ICMP、IGMP |
| 2 | 数据链路层 | 成帧、硬件地址寻址、差错检测(分 LLC / MAC 子层) | 以太网、MAC |
| 1 | 物理层 | 为数据链路层提供物理连接,透明传输比特流 | 集线器、中继器、网线、双绞线、同轴电缆 |
分层的意义:网络通信极其复杂,分层后每层只解决一类问题,层间通过接口协作,便于设计、实现与排错。
TCP/IP 模型是 OSI 的简化版本:
| OSI 七层 | TCP/IP 五层 | TCP/IP 四层 |
|---|---|---|
| 应用层、表示层、会话层 | 应用层 | 应用层 |
| 传输层 | 传输层 | 传输层 |
| 网络层 | 网络层 | 网络层 |
| 数据链路层 + 物理层 | 数据链路层、物理层 | 网络接口层 |
五层模型专为介绍网络原理而设计;四层模型(应用层、传输层、网络层、网络接口层)是实际工程所用。
flowchart LR
subgraph OSI["OSI 七层"]
A7["应用层"] --- A6["表示层"] --- A5["会话层"] --- A4["传输层"] --- A3["网络层"] --- A2["数据链路层"] --- A1["物理层"]
end
subgraph TCPIP["TCP/IP 四层"]
B4["应用层"] --- B3["传输层"] --- B2["网络层"] --- B1["网络接口层"]
end
A7 -.-> B4
A6 -.-> B4
A5 -.-> B4
A4 -.-> B3
A3 -.-> B2
A2 -.-> B1
A1 -.-> B1
发送时数据自上层向下层交付,每层加一个首部(封装);接收时方向相反,逐层去除首部(拆封)。
flowchart LR
U["用户数据"] --> AP["应用层首部 + 用户数据"]
AP --> TP["TCP/UDP 首部 + 数据"]
TP --> NP["IP 首部 + 数据"]
NP --> DP["以太网首部 + 数据 + 尾部"]
DP --> PHY["网卡 → 物理链路上的电平信号"]
应用层(MQTT、HTTP)加应用层首部 → 传输层加 TCP/UDP 首部 → 网络层加 IP 首部 → 链路层加以太网首部后交网卡发到物理链路。接收方执行相反的拆封过程。
Internet 上每个节点依靠唯一的 IP 地址相互区分。IP 是软件地址,标识主机身份;MAC 是硬件地址,存于网卡,用于局域网内寻址。
192.168.1.1;| 类别 | 第一字节范围 | 网络/主机地址 | 默认子网掩码 | 高位特征 | 用途 |
|---|---|---|---|---|---|
| A | 1 ~ 126 | 1 字节网络 + 3 字节主机 | 255.0.0.0 | 最高位 0 | 特大网络 |
| B | 128 ~ 191 | 2 字节网络 + 2 字节主机 | 255.255.0.0 | 最高位 10 | 中型网络 |
| C | 192 ~ 223 | 3 字节网络 + 1 字节主机 | 255.255.255.0 | 最高位 110 | 小型网络、局域网 |
| D | 224 ~ 239 | 不分网络/主机 | — | 高四位 1110 | 多播 Multicast |
| E | 240 ~ 255 | 不分网络/主机 | — | 高五位 11110 | 保留,暂未使用 |
三类的私有地址(公网不可用,仅局域网):A 类 10.0.0.0 ~ 10.255.255.255;B 类 172.16.0.0 ~ 172.31.255.255;C 类 192.168.0.0 ~ 192.168.255.255。另外 A 类 127.X.X.X 为环回保留地址,B 类 169.254.X.X 为自动获取 IP 失败时的保留地址。
| 名称 | 地址形式 | 说明 |
|---|---|---|
| 直接广播地址 | 主机号全 1,如 192.168.0.255 |
向某网络内所有主机发报文 |
| 受限广播地址 | 255.255.255.255(32 位全 1) |
本网络内部广播,启动时不知网络号也可用 |
| 多播地址 | D 类 224.0.0.1 ~ 239.255.255.254 |
一对多通信,只能作目的地址 |
| 环回地址 | 127.X.X.X |
测试网络协议、本机进程通信;不能作主机 IP |
0.0.0.0 |
32 位全 0 | 表示本网络上的本主机,只能作源地址;不能被 ping 通 |
服务器中 0.0.0.0(INADDR_ANY)表示监听本机所有 IPv4 地址的该端口。
网络标识 = IP 地址 & 子网掩码,网络标识相同即同一网络。
192.168.1.50 & 255.255.255.0 = 192.168.1.0
192.168.1.100 & 255.255.255.0 = 192.168.1.0 → 同一网段
一台主机通常只有一个 IP,但会运行多个联网进程。IP 只能区分主机,无法区分进程,于是引入端口号:它是用来在主机中唯一标识一个能上网进程的数字编号,取值范围 0 ~ 65535,通过「IP + 端口号」区分主机内不同进程。
| 服务 | 端口 | 说明 | 服务 | 端口 | 说明 |
|---|---|---|---|---|---|
| HTTP | 80 | 超文本传输协议 | SSH | 22 | 安全外壳协议 |
| FTP | 21 | 文件传输协议 | Telnet | 23 | 终端远程登录 |
| SMTP | 25 | 简单邮件传输协议 | POP3 | 110 | 邮局协议 v3 |
| TFTP | 69 | 简单文件传输协议 | — | — | — |
自编服务器端口不能与其它服务冲突,不常用端口通常大于 5000。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。(原书仅在
bind()示例中说明htons/htonl用于避免大小端问题。)
| 概念 | 含义 |
|---|---|
| 大端序 Big-Endian | 低地址存放高位字节(网络字节序即大端) |
| 小端序 Little-Endian | 低地址存放低位字节(x86、多数 ARM 的主机字节序) |
| 网络字节序 | TCP/IP 规定的统一字节序,等同于大端序 |
以 32 位数 0x12345678 为例,内存低地址到高地址:大端为 12 34 56 78,小端为 78 56 34 12。
| 函数 | 含义 | 方向 | 宽度 |
|---|---|---|---|
htons |
host to network short | 主机 → 网络 | 16 位(端口) |
htonl |
host to network long | 主机 → 网络 | 32 位(IP/整数) |
ntohs |
network to host short | 网络 → 主机 | 16 位 |
ntohl |
network to host long | 网络 → 主机 | 32 位 |
使用要点:sin_port 必须用 htons();inet_pton()/inet_addr() 得到的地址已是网络字节序,无需再 htonl;若手工给 s_addr 赋主机字节序整数则需 htonl();INADDR_ANY 为 0,转换后仍为 0。
server_addr.sin_port = htons(SERVER_PORT); // 端口:主机序 → 网络序
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意本地 IP
TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于 IP 的传输层协议:对上服务 socket 接口,对下调用 IP 层。
| 特点 | 说明 |
|---|---|
| 面向连接 | 通信前必须三次握手建立连接 |
| 确认与重传 | 每个报文段需对端 ACK;超时未收到应答则重传 |
| 全双工 | 连接建立后数据双向独立流通 |
| 基于字节流 | 按字节编号,接收端用 ACK 确认,保证有序完整 |
| 流量控制 | 滑动窗口:接收端告知发送端自己的缓存大小 |
| 差错控制 | 校验和检验有效性,丢弃重复、重组乱序、请求重传 |
| 拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 |
保证可靠传输的三个机制:① 发送应答机制——每个报文段须得到接收方应答;② 超时重传机制——发送后启动定时器,超时未应答就重发;③ 重排整理机制——IP 数据报可能乱序、重复,TCP 重排去重后再交付应用层。
流量控制是接收方控制流量;拥塞控制是发送方感知网络拥塞后控制流量,二者不同。
TCP 报文 = TCP 首部 + 数据区,首部通常 20 字节。
| 字段 | 长度 | 说明 |
|---|---|---|
| 源端口号 / 目标端口号 | 各 2 字节 | 标识收发端应用进程 |
| 序号 Sequence Number | 4 字节 | 本报文段第一个数据字节的编号 |
| 确认序号 Acknowledge Number | 4 字节 | 期望收到的下一个字节编号,仅 ACK=1 时有效 |
| 首部长度 | 4 bit | 以 4 字节为单位,最大 15×4=60 字节 |
| 保留位 | 6 bit | 目前置 0 |
| 标志位 | 6 bit | URG / ACK / PSH / RST / SYN / FIN |
| 窗口大小 | 2 字节 | 接收窗口大小,用于流量控制 |
| 校验和 | 2 字节 | 覆盖首部与数据,强制字段 |
| 紧急指针 | 2 字节 | 紧急数据最后一个字节的序号 |
| 选项 | 最多 40 字节 | 可选 |
| 标志 | 置位含义 |
|---|---|
| URG | 紧急指针字段有效 |
| ACK | 确认序号字段有效 |
| PSH | 接收方应尽快把本报文段交给应用层 |
| RST | 连接出现错误,必须释放连接后重建 |
| SYN | 请求建立连接 |
| FIN | 发送方无数据要发送,要求释放连接 |
小写 ack 指首部中的「确认序号」,大写 ACK 指标志位,二者不同。
sequenceDiagram
participant C as 客户端 Client
participant S as 服务器 Server
Note over C,S: 初始:C=CLOSED, S=LISTEN
C->>S: 第一次握手:SYN=1, seq=J(请求连接)
Note left of C: 进入 SYN_SENT
S->>C: 第二次握手:SYN=1, ACK=1, ack=J+1, seq=K
Note right of S: 进入 SYN_RCVD
C->>S: 第三次握手:ACK=1, ack=K+1
Note over C,S: 双方进入 ESTABLISHED,连接建立成功
| 握手 | 发送方 | 内容 | 状态变化 |
|---|---|---|---|
| 第一次 | 客户端 | SYN=1, seq=J |
客户端 → SYN_SENT |
| 第二次 | 服务器 | SYN=1, ACK=1, ack=J+1, seq=K |
服务器 → SYN_RCVD |
| 第三次 | 客户端 | ACK=1, ack=K+1 |
双方 → ESTABLISHED |
握手完成后双方都获得彼此的窗口大小、序列号;传输阶段 SYN 为 0(只用于发起连接、同步序号)。为什么需要三次握手:类比打电话——A 问「你能听到我吗?」,B 答「我听到了,你能听到我吗?」,A 再答「我能听到」。三次互相确认后,双方才确信「对方能听到自己」,才能开始可靠通信。
TCP 全双工,每个方向必须单独关闭,首次发起关闭的一方为主动关闭,另一方为被动关闭,由任一方调用 close() 触发。
sequenceDiagram
participant C as 客户端(主动关闭)
participant S as 服务器(被动关闭)
Note over C,S: 初始:双方 ESTABLISHED
C->>S: 第一次挥手:FIN=1, seq=u
Note left of C: 进入 FIN_WAIT_1
S->>C: 第二次挥手:ACK=1, ack=u+1
Note right of S: 进入 CLOSE_WAIT
Note left of C: 进入 FIN_WAIT_2
S->>C: 第三次挥手:FIN=1, seq=v
Note right of S: 进入 LAST_ACK
C->>S: 第四次挥手:ACK=1, ack=v+1
Note left of C: 进入 TIME_WAIT,等待 2MSL
Note over C,S: 服务器收到 ACK 后 CLOSED;客户端 2MSL 后 CLOSED
| 挥手 | 发送方 | 内容 | 状态变化 |
|---|---|---|---|
| 第一次 | 主动方 | FIN=1 |
主动方 → FIN_WAIT_1 |
| 第二次 | 被动方 | ACK=1, ack=+1 |
被动方 → CLOSE_WAIT,主动方 → FIN_WAIT_2 |
| 第三次 | 被动方 | FIN=1 |
被动方 → LAST_ACK |
| 第四次 | 主动方 | ACK=1, ack=+1 |
主动方 → TIME_WAIT,等待 2MSL 后 CLOSED |
UDP(User Datagram Protocol,用户数据报协议)是无连接、不可靠的传输层协议。发送端把上层数据加 UDP 首部、简单校验后交给 IP 层;接收端简单校验,出错即丢弃且不通知源主机。到达顺序无法预知,需应用层自行排序。
| 特点 | 说明 |
|---|---|
| 无连接、不可靠 | 不需握手,报文可能丢失、乱序、重复 |
| 无反馈 | 出现差错直接丢弃,不产生应答 |
| 面向报文 | 直接添加 UDP 首部,不拆分合并 |
| 速度快 | 无握手、确认、窗口、重传、拥塞控制 |
UDP 适用于实时性要求高、可容忍少量差错的场景,如直播、网络电话、实时视频。
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传、排序) | 不可靠 |
| 流量/拥塞控制 | 有 | 无 |
| 传输单位 | 字节流 | 数据报 |
| 速度 | 较慢 | 快 |
| 典型应用 | 文件传输、Web、SSH | 直播、VoIP、DNS、广播/多播 |
套接字(socket) 是 Linux 下的进程间通信机制,可用于不同主机的应用程序间通信,也可用于同一主机的不同应用,通常采用客户端 ↔ 服务器模式。socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,在设计模式中是一个门面模式:把复杂的 TCP/IP 协议隐藏在接口之后,用户按 socket 规定编程即可。socket 也是标准(BSD socket),可移植到任何实现该标准的平台,如 LwIP、Windows,甚至 RT-Thread 等 RTOS。
需包含:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
类似 open(),创建一个网络通信端点。成功返回网络文件描述符(socket 描述符),失败返回 -1 并设置 errno。
常用协议族 domain:
| domain | 说明 |
|---|---|
AF_UNIX/AF_LOCAL |
本地通信 |
AF_INET |
IPv4 |
AF_INET6 |
IPv6 |
AF_NETLINK |
内核与用户空间接口 |
AF_PACKET |
底层数据包接口 |
AF_ALG |
内核加密 API 接口 |
套接字类型 type:
| type | 说明 |
|---|---|
SOCK_STREAM |
有序可靠双向、基于连接的字节流,用于 TCP |
SOCK_DGRAM |
固定长度、无连接、不可靠报文,用于 UDP |
SOCK_SEQPACKET |
固定长度、有序、可靠、面向连接的报文 |
SOCK_RAW |
原始套接字,可访问网络层原始数据包 |
SOCK_RDM |
不保证排序的可靠数据报层 |
SOCK_PACKET |
已过时,不应使用 |
protocol 通常为 0,表示使用默认协议(AF_INET+SOCK_STREAM 默认 TCP,AF_INET+SOCK_DGRAM 默认 UDP)。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(-1);
}
close(sockfd);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
把 IP / 端口与套接字绑定。服务器通常绑定到众所周知的地址;客户端一般让内核自动选地址,可不调用 bind()。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14]; /* IP + 端口,用户难以直接赋值 */
};
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 */
in_port_t sin_port; /* 端口号,需 htons() */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8]; /* 保留未用 */
};
两者空间大小一致,可相互强制转换,实际编程用 sockaddr_in:
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
int listen(int sockfd, int backlog);
只能用于服务器,一般在 bind() 之后、accept() 之前调用,不能在已连接的套接字上执行。backlog 为等待连接队列的最大长度;请求过多时内核把连接放入队列按先来后到处理,队列满时新请求可能被拒绝。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
服务器进入监听后用 accept() 获取客户端连接并建立连接。
accept() 阻塞直到有客户端连接;connect() 的客户端,服务器用它与该客户端通信;sockfd 继续监听;addr 为传出参数,返回客户端 IP 与端口,不关心可传 NULL;errno。服务器流程:socket() → bind() → listen() → accept() → recv/send → close()。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于客户端连接远程服务器。对 TCP 会发生三次握手并最终建立连接;对 UDP 只是在 sockfd 中记录服务器 IP 与端口,不发送数据。成功返回 0,失败返回 -1 并设置 errno。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv 标志:
| recv 标志 | 描述 |
|---|---|
MSG_CMSG_CLOEXEC |
为 UNIX 域套接字接收的文件描述符设置执行时关闭标志 |
MSG_DONTWAIT |
非阻塞操作(相当于 O_NONBLOCK) |
MSG_ERRQUEUE |
接收错误信息作为辅助数据 |
MSG_OOB |
如果协议支持,获取带外数据 |
MSG_PEEK |
返回数据包内容但不真正取走 |
MSG_TRUNC |
即使数据包被截断也返回其长度 |
MSG_WAITALL |
等待所有数据可用(仅 SOCK_STREAM) |
send 标志:
| send 标志 | 描述 |
|---|---|
MSG_CONFIRM |
提供链路层反馈以保持地址映射有效 |
MSG_DONTROUTE |
勿将数据包路由出本地网络 |
MSG_DONTWAIT |
允许非阻塞操作 |
MSG_EOR |
如果协议支持,标志记录结束 |
MSG_MORE |
延迟发送数据包,允许写更多数据 |
MSG_NOSIGNAL |
写无连接套接字时不产生 SIGPIPE |
MSG_OOB |
如果协议支持,发送带外数据 |
要点:通常 flags 为 0;对 SOCK_STREAM 一次 recv 可能少于请求字节数,MSG_WAITALL 可阻止;对 SOCK_DGRAM/SOCK_SEQPACKET 一次读取返回整个报文;发送者关闭后数据收完 recv 返回 0;send() 成功不代表对方进程已收到,只保证数据无错误地发到网络驱动。
不再需要套接字描述符时调用 close() 关闭并释放资源。
需包含 <sys/socket.h>、<arpa/inet.h>、<netinet/in.h>。
| 函数 | 作用 | 说明 |
|---|---|---|
inet_aton |
字符串 → 二进制 | 已废弃,旧代码可见 |
inet_addr |
字符串 → 二进制 | 已废弃 |
inet_ntoa |
二进制 → 字符串 | 已废弃 |
inet_pton |
字符串 → 二进制 | 支持 IPv4/IPv6,推荐 |
inet_ntop |
二进制 → 字符串 | 支持 IPv4/IPv6,推荐 |
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_pton:af 为 AF_INET 时 dst 指向 struct in_addr,为 AF_INET6 时指向 struct in6_addr;成功返回 1,字符串无效返回 0,地址族无效返回 -1 且 errno=EAFNOSUPPORT。inet_ntop:把二进制地址转为点分十进制字符串存入 dst,size 为缓冲区大小;成功返回 dst,缓冲区太小返回 NULL 且 errno=ENOSPC。
struct in_addr addr;
char buf[20] = {0};
inet_pton(AF_INET, "192.168.1.222", &addr); // 字符串 → 二进制(网络字节序)
inet_ntop(AF_INET, &addr, buf, sizeof(buf)); // 二进制 → 字符串
flowchart TB
subgraph S["服务端 Server"]
S1["socket() 创建监听套接字"] --> S2["bind() 绑定 IP + 端口"]
S2 --> S3["listen() 进入监听,等待连接"]
S3 --> S4["accept() 阻塞等待客户端"]
S4 --> S5["recv()/send() 用新套接字收发数据"]
S5 --> S6["close() 关闭"]
end
subgraph C["客户端 Client"]
C1["socket() 创建套接字"] --> C2["connect() 发起连接"]
C2 --> C3["send()/recv() 收发数据"]
C3 --> C4["close() 关闭"]
end
C2 -. "三次握手" .-> S3
S4 -. "返回新套接字 connfd" .-> C3
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 // 端口号不能发生冲突,不常用的端口号通常大于 5000
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的IP地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for ( ; ; ) {
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if (0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
| 代码段 | 作用 |
|---|---|
socket(AF_INET, SOCK_STREAM, 0) |
创建 IPv4 TCP 套接字,返回监听套接字 sockfd |
sin_family = AF_INET |
协议族设为 IPv4 |
sin_addr.s_addr = htonl(INADDR_ANY) |
绑定本机所有 IPv4 地址 |
sin_port = htons(8888) |
绑定端口 8888,注意字节序转换 |
bind() |
把套接字与地址、端口关联 |
listen(sockfd, 50) |
进入监听状态,等待队列上限 50 |
accept() |
阻塞等待客户端,返回代表该连接的 connfd |
inet_ntop() |
把客户端 IP 从二进制转为点分十进制字符串 |
recv() 循环 |
不断读取客户端数据;返回 ≤0 表示连接关闭或出错 |
strncmp("exit", recvbuf, 4) |
收到 exit 时结束服务 |
close(connfd) / close(sockfd) |
关闭连接套接字与监听套接字 |
服务器用
%d直接打印client_addr.sin_port时得到的仍是网络字节序,要显示正确端口应先ntohs()。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 // 服务器的端口号
#define SERVER_IP "192.168.1.150" // 服务器的IP地址
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用connect连接远端服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); // IP地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if (0 > ret) {
perror("send error");
break;
}
// 输入了"exit",退出循环
if (0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}
| 代码段 | 作用 |
|---|---|
socket(AF_INET, SOCK_STREAM, 0) |
创建 TCP 套接字 |
sin_port = htons(SERVER_PORT) |
服务器端口,需网络字节序 |
inet_pton(AF_INET, SERVER_IP, ...) |
把点分十进制字符串转为二进制地址 |
connect() |
发起三次握手连接服务器 |
fgets() + send() |
读取用户输入并发送给服务器 |
strncmp("exit", buf, 4) |
输入 exit 时结束 |
SERVER_IP需按实际修改;服务器绑定 8888 端口,客户端也必须用 8888。
把服务器跑在开发板、客户端跑在 Ubuntu:
# 交叉编译服务器(Ubuntu 上执行)
arm-linux-gnueabihf-gcc -o server socket_server.c
# 客户端用本机 gcc 编译
gcc -o client socket_client.c
# 把服务器可执行文件拷贝到开发板
scp server root@192.168.1.150:/home/root/
# 开发板上先运行服务器
./server
# Ubuntu 上运行客户端,输入字符串测试
./client
运行结果:客户端打印 服务器连接成功...;服务器打印 有客户端接入... 及客户端 IP、端口;客户端输入字符串回车后服务器打印 from client: ...;输入 exit 双方退出。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。(原书第三十章只给出 TCP 服务端/客户端例程。)
UDP 不需 listen()/accept()/connect(),用 recvfrom()/sendto() 收发:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
UDP 服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
socklen_t addrlen = sizeof(client_addr);
char buf[512];
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP:SOCK_DGRAM
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
for ( ; ; ) {
memset(buf, 0x0, sizeof(buf));
ret = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addrlen);
if (0 > ret) {
perror("recvfrom error");
break;
}
printf("from client: %s\n", buf);
}
close(sockfd);
exit(EXIT_SUCCESS);
}
UDP 客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
#define SERVER_IP "192.168.1.150"
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
for ( ; ; ) {
memset(buf, 0x0, sizeof(buf));
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
ret = sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("sendto error");
break;
}
if (0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}
目的地址为主机号全 1 的直接广播地址(如 192.168.0.255)或 255.255.255.255。发送前必须打开 SO_BROADCAST:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
server_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST); // 255.255.255.255
sendto(sockfd, buf, len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
组播地址属 D 类(224.0.0.1 ~ 239.255.255.254)。接收方先加入组播组:
struct ip_mreq mreq;
inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr); // 组播组地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本地任意网卡
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
发送方把目的地址设为组播地址后用 sendto() 发送即可;组播地址只能作目的地址。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
每接受一个连接就 fork() 一个子进程专门处理,父进程继续 accept()。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
static void sigchld_handler(int sig)
{
(void)sig;
while (0 < waitpid(-1, NULL, WNOHANG)) // 回收所有已终止子进程,避免僵尸
;
}
static void handle_client(int connfd)
{
char buf[512];
int ret;
for ( ; ; ) {
memset(buf, 0x0, sizeof(buf));
ret = recv(connfd, buf, sizeof(buf), 0);
if (0 >= ret)
break;
printf("from client: %s\n", buf);
if (0 == strncmp("exit", buf, 4))
break;
}
close(connfd);
}
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
socklen_t addrlen = sizeof(client_addr);
int sockfd, connfd, ret;
pid_t pid;
signal(SIGCHLD, sigchld_handler);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
listen(sockfd, 50);
for ( ; ; ) {
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
continue;
}
pid = fork();
if (0 > pid) {
perror("fork error");
close(connfd);
continue;
}
if (0 == pid) { // 子进程:处理客户端
close(sockfd); // 子进程不用监听套接字
handle_client(connfd);
exit(0);
}
close(connfd); // 父进程关闭连接套接字,继续 accept
}
close(sockfd);
exit(EXIT_SUCCESS);
}
select() 在单进程内同时监控监听套接字与所有客户端套接字:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
#define MAX_CLIENTS 64
int main(void)
{
int clientfds[MAX_CLIENTS];
int maxfd, i, nready, sockfd, connfd;
fd_set rset, allset;
char buf[512];
struct sockaddr_in addr = {0};
for (i = 0; i < MAX_CLIENTS; i++)
clientfds[i] = -1;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(SERVER_PORT);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 50);
FD_ZERO(&allset);
FD_SET(sockfd, &allset);
maxfd = sockfd;
for ( ; ; ) {
rset = allset; // 每轮重新赋值:select 会修改集合
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (0 > nready)
continue;
if (FD_ISSET(sockfd, &rset)) { // 有新连接
connfd = accept(sockfd, NULL, NULL);
for (i = 0; i < MAX_CLIENTS; i++) {
if (0 > clientfds[i]) {
clientfds[i] = connfd;
break;
}
}
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
}
for (i = 0; i < MAX_CLIENTS; i++) { // 已有客户端可读
if (0 > clientfds[i] || !FD_ISSET(clientfds[i], &rset))
continue;
memset(buf, 0x0, sizeof(buf));
if (0 >= recv(clientfds[i], buf, sizeof(buf), 0)) {
close(clientfds[i]);
FD_CLR(clientfds[i], &allset);
clientfds[i] = -1;
} else {
printf("from client: %s\n", buf);
}
}
}
close(sockfd);
return 0;
}
select()有描述符数量上限(通常 1024),高并发场景应用epoll。详见 [[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
服务器重启时若前一进程仍处于 TIME_WAIT,bind() 常报 Address already in use。设置 SO_REUSEADDR 可立即复用该地址(应在 bind() 之前调用):
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
| 工具 | 用途 | 示例 |
|---|---|---|
ifconfig |
查看/配置网络接口 IP、状态 | ifconfig eth0 |
ip addr |
查看/配置地址(ifconfig 的现代替代) |
ip addr show eth0 |
ip link |
查看/配置链路层(启用、关闭接口) | ip link set eth0 up |
ping |
测试与目标主机连通性 | ping 192.168.1.150、ping 127.0.0.1 |
netstat |
查看 TCP/UDP 连接、监听状态、路由 | netstat -anp、netstat -tlnp |
ss |
更现代的 socket 统计工具 | ss -tlnp |
tcpdump |
抓包分析网络流量 | tcpdump -i eth0 port 8888 |
ping 127.0.0.1 可测试本机 TCP/IP 协议栈是否正常;用 netstat -an 可在握手/挥手过程中观察 SYN_SENT、ESTABLISHED、FIN_WAIT_2、CLOSE_WAIT、TIME_WAIT 等状态。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
| 维度 | Linux(本教程) | Windows Winsock | RT-Thread / LwIP |
|---|---|---|---|
| 头文件 | <sys/socket.h>、<netinet/in.h> |
<winsock2.h> |
<sys/socket.h> |
| 初始化 | 无需 | 需 WSAStartup() |
无需 |
| 关闭 | close() |
closesocket() |
close() |
| 错误码 | errno |
WSAGetLastError() |
errno |
| 接口风格 | BSD socket | BSD socket(略有差异) | BSD socket(兼容) |
| 非阻塞 | fcntl(O_NONBLOCK) |
ioctlsocket(FIONBIO) |
fcntl |
BSD socket 是事实标准,跨平台移植时主要差异集中在初始化、关闭函数与错误码获取方式上。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
答:三次握手让通信双方都确认「对方能收到并理解自己的报文」,同时交换初始序列号与窗口大小。若只握手两次,服务器无法确认客户端已收到自己的 SYN+ACK;此外网络中滞留的旧连接请求可能在延迟后到达服务器,导致服务器单方面建立无效连接、浪费资源。三次握手使双方都进入 ESTABLISHED 后才开始传输数据,保证可靠。
答:TCP 面向连接、可靠、基于字节流,有确认重传、流量控制、拥塞控制,速度较慢;UDP 无连接、不可靠、面向报文,没有这些控制机制,速度快、实时性好。TCP 适用于文件传输、Web、远程登录等要求数据完整可靠的场景;UDP 适用于直播、网络电话、实时视频、DNS 等对实时性要求高、可容忍少量丢包的场景。
accept() 返回的套接字和 socket() 返回的套接字有什么区别?答:socket() 返回的是监听套接字,用于绑定地址、监听连接,在服务器整个生命周期内存在;accept() 返回的是新的连接套接字,代表服务器与某个具体客户端之间的一条 TCP 连接,专门用于与该客户端收发数据。服务器保留监听套接字继续 accept(),而用每个连接套接字分别服务对应客户端。
bind() 时端口号要用 htons(),而 IP 地址用 inet_pton() 后不用再 htonl()?答:不同主机字节序可能不同,网络传输统一使用大端序。端口号是主机字节序的整数,必须用 htons() 转成网络字节序;而 inet_pton()/inet_addr() 把点分十进制字符串转成二进制时,输出的 struct in_addr 已经是网络字节序,无需再 htonl()。若手工给 s_addr 赋主机字节序整数,则必须 htonl()。
TIME_WAIT 状态的作用是什么?答:TCP 全双工,每个方向必须单独关闭。主动方发 FIN 关闭自己的发送方向(第一次),被动方回 ACK(第二次);被动方可能还有数据要发,待发完后再发 FIN(第三次),主动方回 ACK(第四次),因此是四次。TIME_WAIT 出现在主动关闭方发出最后一个 ACK 之后:一方面确保该 ACK 能被对方收到,若丢失可重发对方的 FIN;另一方面让本次连接的残留报文在网络中消失,避免影响使用相同四元组的新连接。等待时间为 2MSL。
内容来源:《I.MX6U嵌入式Linux C应用编程指南》第二十九章 网络基础知识、第三十章 socket编程基础;例程源码 30_socket/socket_server.c、30_socket/socket_client.c