01-网络基础与socket编程.md 49 KB


title: 网络基础与socket编程 tags: [

嵌入式Linux,
Linux应用编程,
网络编程,
socket,
TCP,
UDP,
OSI,
IP地址,
端口号,
字节序,
htonl,
inet_pton,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章 网络基础知识、第三十章 socket编程基础"

网络基础与socket编程

💡 关联知识:[[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 与并发服务器。


1. 网络通信概述

网络通信分三个层次:

层次 角色 说明
硬件层 网卡设备 收发网络数据的物理基础
驱动层 网卡驱动 内核中的网卡驱动,向应用层提供 socket 接口
应用层 应用程序 基于 socket 接口编程,或使用 http、网络控件等更高封装

硬件上两台主机各有网卡,通过传输媒介相连(有线:双绞线、光纤;无线:WiFi、蓝牙、ZigBee、4G/5G)。内核驱动底层网卡并暴露 socket 接口;应用层调用 socket 接口即所谓网络编程。

flowchart TB
    A["应用层:HTTP / MQTT / 自定义协议(调用 socket 接口)"] --> B["内核层:网卡驱动(提供 socket 接口)"]
    B --> C["硬件层:网卡设备"]
    C <-->|"传输媒介:网线 / 光纤 / 无线"| D["对端主机的网卡 / 内核 / 应用"]

2. 网络互连模型

2.1 OSI 七层模型

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 物理层 为数据链路层提供物理连接,透明传输比特流 集线器、中继器、网线、双绞线、同轴电缆

分层的意义:网络通信极其复杂,分层后每层只解决一类问题,层间通过接口协作,便于设计、实现与排错。

2.2 TCP/IP 四层 / 五层模型

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

2.3 数据的封装与拆封

发送时数据自上层向下层交付,每层加一个首部(封装);接收时方向相反,逐层去除首部(拆封)。

flowchart LR
    U["用户数据"] --> AP["应用层首部 + 用户数据"]
    AP --> TP["TCP/UDP 首部 + 数据"]
    TP --> NP["IP 首部 + 数据"]
    NP --> DP["以太网首部 + 数据 + 尾部"]
    DP --> PHY["网卡 → 物理链路上的电平信号"]

应用层(MQTT、HTTP)加应用层首部 → 传输层加 TCP/UDP 首部 → 网络层加 IP 首部 → 链路层加以太网首部后交网卡发到物理链路。接收方执行相反的拆封过程。


3. IP 地址

Internet 上每个节点依靠唯一的 IP 地址相互区分。IP 是软件地址,标识主机身份;MAC 是硬件地址,存于网卡,用于局域网内寻址。

  • 传统 IPv4 地址是 32 位二进制,由 4 个 8 位字段组成,交互时用点分十进制表示,如 192.168.1.1
  • IPv6 采用 128 位地址长度,由 8 个 16 位字段组成;
  • 32 位分为网络地址主机地址两部分,由子网掩码确定各占多少位。

3.1 IP 地址分类

类别 第一字节范围 网络/主机地址 默认子网掩码 高位特征 用途
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 失败时的保留地址。

3.2 特殊 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.0INADDR_ANY)表示监听本机所有 IPv4 地址的该端口。

3.3 判断两个 IP 是否同一网段

网络标识 = 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   → 同一网段

4. 端口号

一台主机通常只有一个 IP,但会运行多个联网进程。IP 只能区分主机,无法区分进程,于是引入端口号:它是用来在主机中唯一标识一个能上网进程的数字编号,取值范围 0 ~ 65535,通过「IP + 端口号」区分主机内不同进程。

服务 端口 说明 服务 端口 说明
HTTP 80 超文本传输协议 SSH 22 安全外壳协议
FTP 21 文件传输协议 Telnet 23 终端远程登录
SMTP 25 简单邮件传输协议 POP3 110 邮局协议 v3
TFTP 69 简单文件传输协议

自编服务器端口不能与其它服务冲突,不常用端口通常大于 5000


5. 字节序与转换函数

⚠️ 来源说明:本节不属于《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

6. TCP 协议

TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于 IP 的传输层协议:对上服务 socket 接口,对下调用 IP 层。

6.1 核心特点

特点 说明
面向连接 通信前必须三次握手建立连接
确认与重传 每个报文段需对端 ACK;超时未收到应答则重传
全双工 连接建立后数据双向独立流通
基于字节流 按字节编号,接收端用 ACK 确认,保证有序完整
流量控制 滑动窗口:接收端告知发送端自己的缓存大小
差错控制 校验和检验有效性,丢弃重复、重组乱序、请求重传
拥塞控制 慢启动、拥塞避免、快重传、快恢复

保证可靠传输的三个机制:① 发送应答机制——每个报文段须得到接收方应答;② 超时重传机制——发送后启动定时器,超时未应答就重发;③ 重排整理机制——IP 数据报可能乱序、重复,TCP 重排去重后再交付应用层。

流量控制是接收方控制流量;拥塞控制是发送方感知网络拥塞后控制流量,二者不同。

6.2 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 指标志位,二者不同。

6.3 建立连接:三次握手

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 再答「我能听到」。三次互相确认后,双方才确信「对方能听到自己」,才能开始可靠通信。

6.4 关闭连接:四次挥手

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

7. UDP 协议

UDP(User Datagram Protocol,用户数据报协议)是无连接、不可靠的传输层协议。发送端把上层数据加 UDP 首部、简单校验后交给 IP 层;接收端简单校验,出错即丢弃且不通知源主机。到达顺序无法预知,需应用层自行排序。

特点 说明
无连接、不可靠 不需握手,报文可能丢失、乱序、重复
无反馈 出现差错直接丢弃,不产生应答
面向报文 直接添加 UDP 首部,不拆分合并
速度快 无握手、确认、窗口、重传、拥塞控制

UDP 适用于实时性要求高、可容忍少量差错的场景,如直播、网络电话、实时视频。

维度 TCP UDP
连接 面向连接(三次握手) 无连接
可靠性 可靠(确认、重传、排序) 不可靠
流量/拥塞控制
传输单位 字节流 数据报
速度 较慢
典型应用 文件传输、Web、SSH 直播、VoIP、DNS、广播/多播

8. socket 简介

套接字(socket) 是 Linux 下的进程间通信机制,可用于不同主机的应用程序间通信,也可用于同一主机的不同应用,通常采用客户端 ↔ 服务器模式。socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,在设计模式中是一个门面模式:把复杂的 TCP/IP 协议隐藏在接口之后,用户按 socket 规定编程即可。socket 也是标准(BSD socket),可移植到任何实现该标准的平台,如 LwIPWindows,甚至 RT-Thread 等 RTOS。


9. socket 编程 API

需包含:

#include <sys/types.h>
#include <sys/socket.h>

9.1 socket():创建套接字

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);

9.2 bind():绑定地址与端口

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));

9.3 listen():进入监听状态

int listen(int sockfd, int backlog);

只能用于服务器,一般在 bind() 之后、accept() 之前调用,不能在已连接的套接字上执行。backlog 为等待连接队列的最大长度;请求过多时内核把连接放入队列按先来后到处理,队列满时新请求可能被拒绝。

9.4 accept():接受连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

服务器进入监听后用 accept() 获取客户端连接并建立连接。

  • 若无等待请求,accept() 阻塞直到有客户端连接;
  • 返回一个全新的连接套接字,它连接调用 connect() 的客户端,服务器用它与该客户端通信;
  • sockfd 继续监听;
  • addr 为传出参数,返回客户端 IP 与端口,不关心可传 NULL
  • 出错返回 -1 并设置 errno

服务器流程:socket()bind()listen()accept()recv/sendclose()

9.5 connect():客户端发起连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

用于客户端连接远程服务器。对 TCP 会发生三次握手并最终建立连接;对 UDP 只是在 sockfd 中记录服务器 IP 与端口,不发送数据。成功返回 0,失败返回 -1 并设置 errno

9.6 发送与接收函数

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() 成功不代表对方进程已收到,只保证数据无错误地发到网络驱动。

9.7 close():关闭套接字

不再需要套接字描述符时调用 close() 关闭并释放资源。

9.8 IP 地址格式转换函数

需包含 <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_ptonafAF_INETdst 指向 struct in_addr,为 AF_INET6 时指向 struct in6_addr;成功返回 1,字符串无效返回 0,地址族无效返回 -1 且 errno=EAFNOSUPPORT
  • inet_ntop:把二进制地址转为点分十进制字符串存入 dstsize 为缓冲区大小;成功返回 dst,缓冲区太小返回 NULLerrno=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));  // 二进制 → 字符串
    

10. socket 通信流程总览

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

11. TCP 编程实战:服务器与客户端

11.1 服务器程序(socket_server.c)

#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()

11.2 客户端程序(socket_client.c)

#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。

11.3 交叉编译与实验步骤

把服务器跑在开发板、客户端跑在 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 双方退出。


12. UDP 编程与广播、组播

⚠️ 来源说明:本节不属于《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);
}

12.1 广播

目的地址为主机号全 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));

12.2 组播(多播)

组播地址属 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() 发送即可;组播地址只能作目的地址。


13. 多客户端并发服务器

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

13.1 fork 版并发服务器

每接受一个连接就 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);
}

13.2 select 版并发服务器

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. 高并发模型]]。


14. socket 选项:SO_REUSEADDR

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

服务器重启时若前一进程仍处于 TIME_WAITbind() 常报 Address already in use。设置 SO_REUSEADDR 可立即复用该地址(应在 bind() 之前调用):

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

15. 网络调试工具

工具 用途 示例
ifconfig 查看/配置网络接口 IP、状态 ifconfig eth0
ip addr 查看/配置地址(ifconfig 的现代替代) ip addr show eth0
ip link 查看/配置链路层(启用、关闭接口) ip link set eth0 up
ping 测试与目标主机连通性 ping 192.168.1.150ping 127.0.0.1
netstat 查看 TCP/UDP 连接、监听状态、路由 netstat -anpnetstat -tlnp
ss 更现代的 socket 统计工具 ss -tlnp
tcpdump 抓包分析网络流量 tcpdump -i eth0 port 8888

ping 127.0.0.1 可测试本机 TCP/IP 协议栈是否正常;用 netstat -an 可在握手/挥手过程中观察 SYN_SENTESTABLISHEDFIN_WAIT_2CLOSE_WAITTIME_WAIT 等状态。


16. 跨平台对比:Linux socket vs 其它平台

⚠️ 来源说明:本节不属于《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 是事实标准,跨平台移植时主要差异集中在初始化、关闭函数与错误码获取方式上。


17. 面试精选

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

Q1:TCP 为什么需要三次握手?两次握手会有什么问题?

:三次握手让通信双方都确认「对方能收到并理解自己的报文」,同时交换初始序列号与窗口大小。若只握手两次,服务器无法确认客户端已收到自己的 SYN+ACK;此外网络中滞留的旧连接请求可能在延迟后到达服务器,导致服务器单方面建立无效连接、浪费资源。三次握手使双方都进入 ESTABLISHED 后才开始传输数据,保证可靠。

Q2:TCP 与 UDP 的区别是什么?分别适用于哪些场景?

:TCP 面向连接、可靠、基于字节流,有确认重传、流量控制、拥塞控制,速度较慢;UDP 无连接、不可靠、面向报文,没有这些控制机制,速度快、实时性好。TCP 适用于文件传输、Web、远程登录等要求数据完整可靠的场景;UDP 适用于直播、网络电话、实时视频、DNS 等对实时性要求高、可容忍少量丢包的场景。

Q3:服务器端 accept() 返回的套接字和 socket() 返回的套接字有什么区别?

socket() 返回的是监听套接字,用于绑定地址、监听连接,在服务器整个生命周期内存在;accept() 返回的是新的连接套接字,代表服务器与某个具体客户端之间的一条 TCP 连接,专门用于与该客户端收发数据。服务器保留监听套接字继续 accept(),而用每个连接套接字分别服务对应客户端。

Q4:为什么 bind() 时端口号要用 htons(),而 IP 地址用 inet_pton() 后不用再 htonl()

:不同主机字节序可能不同,网络传输统一使用大端序。端口号是主机字节序的整数,必须用 htons() 转成网络字节序;而 inet_pton()/inet_addr() 把点分十进制字符串转成二进制时,输出的 struct in_addr 已经是网络字节序,无需再 htonl()。若手工给 s_addr 赋主机字节序整数,则必须 htonl()

Q5:TCP 四次挥手为什么是四次?TIME_WAIT 状态的作用是什么?

:TCP 全双工,每个方向必须单独关闭。主动方发 FIN 关闭自己的发送方向(第一次),被动方回 ACK(第二次);被动方可能还有数据要发,待发完后再发 FIN(第三次),主动方回 ACK(第四次),因此是四次。TIME_WAIT 出现在主动关闭方发出最后一个 ACK 之后:一方面确保该 ACK 能被对方收到,若丢失可重发对方的 FIN;另一方面让本次连接的残留报文在网络中消失,避免影响使用相同四元组的新连接。等待时间为 2MSL。


延伸阅读

  • socket 基础:[[Linux+C+C++技术体系梳理/2. Linux系统编程/14. socket网络编程基础]]
  • TCP 编程:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
  • UDP 编程:[[Linux+C+C++技术体系梳理/2. Linux系统编程/16. UDP编程]]
  • 深入 → IO 多路复用:[[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]
  • 深入 → 高并发模型:[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]
  • 本库相关:[[04-网络编程与项目实战/02-CAN应用编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]

内容来源:《I.MX6U嵌入式Linux C应用编程指南》第二十九章 网络基础知识、第三十章 socket编程基础;例程源码 30_socket/socket_server.c30_socket/socket_client.c