TCP 套接字:API 流程、地址结构与网络字节序
课程:尚硅谷《嵌入式 Linux 应用层开发》第 6 章 Socket 编程
依据:2026-09-29 21:22 录音转写;课程 PDF 第 264---276 页。
本节边界:从 Socket 的含义讲到常用 TCP API、IPv4 地址结构和字节序转换;第 277 页的测试程序尚未进入本次录音。
页码校正:用户输入的"2744页"与教材总页数及录音内容不符,按录音实际对应范围整理为第 264---276 页。
1. 本节知识路线
#mermaid-svg-eTBQbG793x3AzI5L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eTBQbG793x3AzI5L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eTBQbG793x3AzI5L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eTBQbG793x3AzI5L .error-icon{fill:#552222;}#mermaid-svg-eTBQbG793x3AzI5L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eTBQbG793x3AzI5L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eTBQbG793x3AzI5L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eTBQbG793x3AzI5L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eTBQbG793x3AzI5L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eTBQbG793x3AzI5L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eTBQbG793x3AzI5L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eTBQbG793x3AzI5L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eTBQbG793x3AzI5L .marker.cross{stroke:#333333;}#mermaid-svg-eTBQbG793x3AzI5L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eTBQbG793x3AzI5L p{margin:0;}#mermaid-svg-eTBQbG793x3AzI5L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eTBQbG793x3AzI5L .cluster-label text{fill:#333;}#mermaid-svg-eTBQbG793x3AzI5L .cluster-label span{color:#333;}#mermaid-svg-eTBQbG793x3AzI5L .cluster-label span p{background-color:transparent;}#mermaid-svg-eTBQbG793x3AzI5L .label text,#mermaid-svg-eTBQbG793x3AzI5L span{fill:#333;color:#333;}#mermaid-svg-eTBQbG793x3AzI5L .node rect,#mermaid-svg-eTBQbG793x3AzI5L .node circle,#mermaid-svg-eTBQbG793x3AzI5L .node ellipse,#mermaid-svg-eTBQbG793x3AzI5L .node polygon,#mermaid-svg-eTBQbG793x3AzI5L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eTBQbG793x3AzI5L .rough-node .label text,#mermaid-svg-eTBQbG793x3AzI5L .node .label text,#mermaid-svg-eTBQbG793x3AzI5L .image-shape .label,#mermaid-svg-eTBQbG793x3AzI5L .icon-shape .label{text-anchor:middle;}#mermaid-svg-eTBQbG793x3AzI5L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eTBQbG793x3AzI5L .rough-node .label,#mermaid-svg-eTBQbG793x3AzI5L .node .label,#mermaid-svg-eTBQbG793x3AzI5L .image-shape .label,#mermaid-svg-eTBQbG793x3AzI5L .icon-shape .label{text-align:center;}#mermaid-svg-eTBQbG793x3AzI5L .node.clickable{cursor:pointer;}#mermaid-svg-eTBQbG793x3AzI5L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eTBQbG793x3AzI5L .arrowheadPath{fill:#333333;}#mermaid-svg-eTBQbG793x3AzI5L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eTBQbG793x3AzI5L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eTBQbG793x3AzI5L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eTBQbG793x3AzI5L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eTBQbG793x3AzI5L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eTBQbG793x3AzI5L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eTBQbG793x3AzI5L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eTBQbG793x3AzI5L .cluster text{fill:#333;}#mermaid-svg-eTBQbG793x3AzI5L .cluster span{color:#333;}#mermaid-svg-eTBQbG793x3AzI5L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eTBQbG793x3AzI5L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eTBQbG793x3AzI5L rect.text{fill:none;stroke-width:0;}#mermaid-svg-eTBQbG793x3AzI5L .icon-shape,#mermaid-svg-eTBQbG793x3AzI5L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eTBQbG793x3AzI5L .icon-shape p,#mermaid-svg-eTBQbG793x3AzI5L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eTBQbG793x3AzI5L .icon-shape .label rect,#mermaid-svg-eTBQbG793x3AzI5L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eTBQbG793x3AzI5L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eTBQbG793x3AzI5L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eTBQbG793x3AzI5L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Socket 是什么
socket 创建通信端点
sockaddr_in 填写 IP 与端口
服务端 bind listen accept
客户端 connect
send recv 收发字节流
shutdown 或 close
网络字节序转换
htons htonl
inet_pton inet_ntop
老师这一节想解决的是:TCP 原理已经学过,真正写程序时应该调用哪些接口,以及 IP、端口为什么不能直接随意填入结构体。
2. Socket 到底是什么
Socket 是应用程序使用操作系统网络能力的编程接口。应用不需要自己实现网卡驱动、IP 路由、TCP 确认和重传,只需通过 Socket API 创建通信端点、建立连接和收发数据。
在 Linux 中,socket() 成功后返回一个文件描述符 。因此它可以像文件描述符一样传给系统调用,也可以用 close() 关闭。
text
应用程序
│ socket / connect / send / recv
▼
Socket 文件描述符
│
▼
内核协议栈:TCP → IP → 网卡驱动
2.1 "Socket 由 IP、端口、协议组成"怎样理解
这句话适合帮助入门,但要分清三个层次:
| 概念 | 常见标识 | 说明 |
|---|---|---|
| 本地 IPv4 Socket 地址 | 本地 IP + 本地端口 | sockaddr_in 描述的内容 |
| TCP 连接 | 源 IP、源端口、目的 IP、目的端口 | 一条已建立连接通常用四元组区分 |
| Linux Socket 对象 | 文件描述符指向的内核通信端点 | 还包含状态、缓冲区、选项和协议控制信息 |
因此,Socket 不只是一个字符串形式的 IP:端口,它是内核维护的通信对象;地址只是其中的重要属性。
3. Socket 的常见类型
c
int socket(int domain, int type, int protocol);
| 参数 | 本课程常用值 | 含义 |
|---|---|---|
domain |
AF_INET |
使用 IPv4 地址族 |
type |
SOCK_STREAM |
有连接、可靠、有序的字节流,通常对应 TCP |
protocol |
0 |
由系统为地址族和类型选择默认协议 |
本课程创建 TCP Socket 的常用写法:
c
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
常见组合:
| 组合 | 一般用途 |
|---|---|
AF_INET + SOCK_STREAM + 0 |
IPv4 TCP |
AF_INET + SOCK_DGRAM + 0 |
IPv4 UDP |
AF_INET6 + SOCK_STREAM + 0 |
IPv6 TCP |
AF_UNIX + SOCK_STREAM + 0 |
同一台 Linux 主机上的本地进程通信 |
protocol = 0 的意思是选择该 domain + type 组合的默认协议,不是自动打开某种"网络优化功能"。
4. TCP 服务端和客户端的整体流程
客户端 Linux 内核 TCP 服务端 客户端 Linux 内核 TCP 服务端 #mermaid-svg-X0izaFmOEWmtN9TL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X0izaFmOEWmtN9TL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X0izaFmOEWmtN9TL .error-icon{fill:#552222;}#mermaid-svg-X0izaFmOEWmtN9TL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X0izaFmOEWmtN9TL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X0izaFmOEWmtN9TL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X0izaFmOEWmtN9TL .marker.cross{stroke:#333333;}#mermaid-svg-X0izaFmOEWmtN9TL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X0izaFmOEWmtN9TL p{margin:0;}#mermaid-svg-X0izaFmOEWmtN9TL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-X0izaFmOEWmtN9TL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-X0izaFmOEWmtN9TL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-X0izaFmOEWmtN9TL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-X0izaFmOEWmtN9TL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-X0izaFmOEWmtN9TL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-X0izaFmOEWmtN9TL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-X0izaFmOEWmtN9TL .sequenceNumber{fill:white;}#mermaid-svg-X0izaFmOEWmtN9TL #sequencenumber{fill:#333;}#mermaid-svg-X0izaFmOEWmtN9TL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-X0izaFmOEWmtN9TL .messageText{fill:#333;stroke:none;}#mermaid-svg-X0izaFmOEWmtN9TL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-X0izaFmOEWmtN9TL .labelText,#mermaid-svg-X0izaFmOEWmtN9TL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-X0izaFmOEWmtN9TL .loopText,#mermaid-svg-X0izaFmOEWmtN9TL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-X0izaFmOEWmtN9TL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-X0izaFmOEWmtN9TL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-X0izaFmOEWmtN9TL .noteText,#mermaid-svg-X0izaFmOEWmtN9TL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-X0izaFmOEWmtN9TL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-X0izaFmOEWmtN9TL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-X0izaFmOEWmtN9TL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-X0izaFmOEWmtN9TL .actorPopupMenu{position:absolute;}#mermaid-svg-X0izaFmOEWmtN9TL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-X0izaFmOEWmtN9TL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-X0izaFmOEWmtN9TL .actor-man circle,#mermaid-svg-X0izaFmOEWmtN9TL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-X0izaFmOEWmtN9TL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TCP 连接已经建立 socket() bind(IP, 端口) listen(backlog) socket() connect(服务端 IP, 端口) accept() 返回连接 fd send() / recv() send() / recv() shutdown() 或 close() shutdown() 或 close()
4.1 服务端
text
socket → bind → listen → accept → recv/send → close
4.2 客户端
text
socket → connect → send/recv → close
最关键的区别是:
- 服务端把一个 Socket 绑定到本地地址和端口,并进入监听状态;
- 客户端主动连接服务端地址;
accept()为每个已接受连接返回新的连接文件描述符;- 原监听文件描述符继续负责接受后续连接。
5. bind():为 Socket 指定本地地址
c
int bind(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
| 参数 | 作用 |
|---|---|
sockfd |
socket() 返回的文件描述符 |
addr |
指向具体地址结构,调用时转为 struct sockaddr * |
addrlen |
具体地址结构所占字节数 |
socket() 刚创建的通信端点还没有指定本地 IPv4 地址。服务端通常用 bind() 绑定本地端口,客户端多数情况下可以让内核自动选择本地 IP 和临时端口。
6. 为什么同时有 sockaddr 和 sockaddr_in
6.1 通用接口类型 sockaddr
c
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
Socket API 需要兼容 IPv4、IPv6、UNIX 域等不同地址格式,所以函数参数统一写成 struct sockaddr *。
6.2 IPv4 专用类型 sockaddr_in
概念结构如下,具体字段类型以系统头文件为准:
c
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16 位端口,网络字节序 */
struct in_addr sin_addr; /* 32 位 IPv4 地址,网络字节序 */
};
程序实际填写 sockaddr_in,调用 bind() 或 connect() 时再转换指针类型:
c
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "192.168.1.10", &addr.sin_addr) != 1) {
fprintf(stderr, "invalid IPv4 address\n");
return -1;
}
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
教材把 sockaddr 称为其他地址结构的"基类"、把 sockaddr_in 称为"继承"它,便于类比理解,但 C 语言没有类继承。更准确的说法是:Socket API 使用统一的通用指针和长度参数,调用者根据地址族传入对应的具体结构。
7. sockaddr_in 三个核心字段怎么填
| 字段 | 服务端常见填写 | 客户端常见填写 |
|---|---|---|
sin_family |
AF_INET |
AF_INET |
sin_port |
htons(服务端端口) |
htons(服务端端口) |
sin_addr |
INADDR_ANY 或指定本机 IP |
服务端 IP |
服务端监听本机所有 IPv4 网卡地址:
c
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY 对应 0.0.0.0,只适合 bind() 表示监听本机所有接口,不能作为客户端要连接的远端目标地址。
7.1 端口范围
端口字段为 16 位,数值范围是 0---65535。Linux 中绑定 1024 以下端口通常需要 CAP_NET_BIND_SERVICE 权限。练习可选择 8080、8888 等较大端口,但仍需确认该端口没有被其他进程占用。
8. listen():把 Socket 变成监听 Socket
c
int listen(int sockfd, int backlog);
listen() 把一个流式 Socket 标记为被动监听 Socket,让它等待客户端连接。
backlog 是等待应用 accept() 的待处理连接队列长度上限。Linux 还会受到系统配置上限影响,因此不能简单理解为"服务器最多只能服务多少个客户端"。
9. accept():得到专门通信的新描述符
c
int accept(int sockfd,
struct sockaddr *addr,
socklen_t *addrlen);
成功时,accept():
- 从监听 Socket 的待处理连接队列中取出一个连接;
- 创建一个新的已连接 Socket;
- 返回新的文件描述符;
- 可把客户端地址写入
addr; - 保留原监听 Socket,继续接受其他客户端。
text
listen_fd ──只负责等待新连接
│
├─ accept → conn_fd_1 ──与客户端 1 收发
├─ accept → conn_fd_2 ──与客户端 2 收发
└─ accept → conn_fd_3 ──与客户端 3 收发
这是后续"一个服务端连接多个客户端"和多线程服务器的基础。每个工作线程通常处理一个 conn_fd,监听线程继续调用 accept()。
调用前要初始化长度:
c
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int connfd = accept(sockfd,
(struct sockaddr *)&client_addr,
&client_len);
默认阻塞模式下,如果当前没有连接可取,accept() 会等待。
10. connect():客户端主动建立连接
c
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
客户端把服务端地址传给 connect()。对于默认阻塞 TCP Socket,调用成功返回时连接已经建立,可以开始收发数据;失败返回 -1 并设置 errno。
代码里不需要自己发送 SYN、SYN+ACK、ACK。三次握手由内核 TCP 协议栈执行,但不能把 connect() 简化为"必定正好发送三个数据包":丢包重传、同时打开等情况会让实际抓包更复杂。
11. send() 与 recv():传输的是字节流
c
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
入门阶段 flags 通常写 0。
| 返回值 | send() |
recv() |
|---|---|---|
> 0 |
本次交给内核的字节数 | 本次读到的字节数 |
0 |
len 为 0 等特殊情况 |
对端已经有序关闭发送方向 |
-1 |
失败,查看 errno |
失败,查看 errno |
必须记住:
send(fd, buf, 1000, 0)成功时也可能只返回 400,需要继续发送剩余 600 字节;- 一次
send()不对应对端的一次recv(); - TCP 不保留消息边界,应用层需要使用固定长度、分隔符或"长度字段 + 正文"等方式拆包;
recv()返回的内容不是自动补\0的 C 字符串,按字符串打印前要自己控制长度。
12. shutdown() 与 close()
12.1 shutdown() 控制通信方向
c
int shutdown(int sockfd, int how);
how |
含义 |
|---|---|
SHUT_RD |
关闭接收方向 |
SHUT_WR |
关闭发送方向,可用于发出"我不再发送"的半关闭 |
SHUT_RDWR |
关闭收发两个方向 |
12.2 close() 释放当前文件描述符引用
c
int close(int fd);
close() 关闭当前进程持有的文件描述符。若它是该 Socket 的最后一个引用,内核才会释放相应资源,并按 TCP 状态进行连接终止处理。
因此,教材和录音中"close() 一次完成四次挥手"的说法只能当入门简化。close() 返回也不表示网络上的四个报文一定已经全部完成;若描述符还被 dup()、fork() 或其他线程共享,关闭一个引用也不会立即销毁 Socket。
13. 为什么要学习大小端
字节序只讨论多字节整数在内存中的排列顺序。假设 32 位数值为:
text
0x12345678
从低地址到高地址观察四个字节:
| 字节序 | 内存排列 |
|---|---|
| 大端 Big Endian | 12 34 56 78 |
| 小端 Little Endian | 78 56 34 12 |
高位字节是 0x12,低位字节是 0x78。大端把高位字节放在低地址,小端把低位字节放在低地址。
13.1 主机字节序和网络字节序
- 主机字节序:当前 CPU 存放多字节整数的方式;可能是小端,也可能是大端;
- 网络字节序:互联网协议规定使用大端;
- x86、x86-64 和常见 ARM Linux 环境通常采用小端,但"主机字节序 = 小端"不是普遍定义。
不同主机通过网络交换整数时统一使用网络字节序,接收端再转回自己的主机字节序。
14. 四个字节序转换函数
头文件:
c
#include <arpa/inet.h>
| 函数 | 名称拆解 | 方向 | 宽度 | 常见用途 |
|---|---|---|---|---|
htons() |
host to network short | 主机 → 网络 | 16 位 | 端口号 |
htonl() |
host to network long | 主机 → 网络 | 32 位 | IPv4 整数、32 位协议字段 |
ntohs() |
network to host short | 网络 → 主机 | 16 位 | 读取对端端口 |
ntohl() |
network to host long | 网络 → 主机 | 32 位 | 读取 32 位网络字段 |
记忆方法:
text
h = host n = network
s = short,16 位
l = long,接口定义为 32 位
例如:
c
uint16_t host_port = 8080;
uint16_t net_port = htons(host_port);
uint16_t restored_port = ntohs(net_port);
在小端主机上函数会交换字节;在大端主机上可能保持原值。程序仍应调用这些函数,而不是依赖当前 CPU 架构。
15. IP 字符串与二进制地址转换
字节序函数只处理定宽整数,不负责解析 "192.168.1.10" 这种文本。文本 IP 应使用:
c
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
15.1 文本转网络地址:inet_pton()
c
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
int ret = inet_pton(AF_INET,
"192.168.1.10",
&server_addr.sin_addr);
if (ret == 0) {
fprintf(stderr, "IP 字符串格式错误\n");
} else if (ret == -1) {
perror("inet_pton");
}
inet_pton() 已经把地址写成网络字节序,不能再对 sin_addr.s_addr 调一次 htonl(),否则会发生重复转换。
返回值必须分别判断:
| 返回值 | 含义 |
|---|---|
1 |
转换成功 |
0 |
输入不是该地址族的合法文本地址 |
-1 |
地址族不受支持,并设置 errno |
15.2 网络地址转文本:inet_ntop()
c
char ip_text[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET,
&client_addr.sin_addr,
ip_text,
sizeof(ip_text)) == NULL) {
perror("inet_ntop");
} else {
printf("client = %s:%u\n",
ip_text,
(unsigned)ntohs(client_addr.sin_port));
}
教材还介绍了 inet_addr()、inet_aton()、inet_ntoa() 等旧接口。新代码优先使用 inet_pton() 和 inet_ntop(),它们同时支持 IPv4 和 IPv6,错误表达也更清楚。
16. 一段最小地址准备代码
下面只练习本节最核心的"创建 Socket + 填地址",还没有进入完整服务器:
c
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) {
fprintf(stderr, "invalid address\n");
close(fd);
return 1;
}
printf("socket fd = %d, port = %u\n",
fd, (unsigned)ntohs(addr.sin_port));
close(fd);
return 0;
}
编译:
bash
gcc -Wall -Wextra -O2 socket_addr_demo.c -o socket_addr_demo
这段程序应该能够解释到每一行:
socket()创建 IPv4 TCP 通信端点;sockaddr_in保存 IPv4 地址;htons(8080)把 16 位端口改为网络字节序;inet_pton()把点分十进制地址转成二进制网络地址;ntohs()把端口转回来,供本机打印;close()释放文件描述符。
17. 常见错误排查
| 现象或错误 | 优先检查 |
|---|---|
socket() 返回 -1 |
地址族、类型、协议是否受支持;文件描述符是否耗尽 |
bind(): Address already in use |
端口是否被占用;旧连接状态;地址复用设置 |
bind(): Permission denied |
是否绑定了 1024 以下端口;进程权限 |
connect(): Connection refused |
服务端是否启动并监听;IP/端口是否正确;防火墙 |
accept() 一直不返回 |
当前无待接受连接;客户端是否真正连接到该端口 |
| 打印的端口数字异常 | 是否遗漏 ntohs() |
| 连接地址错误 | inet_pton() 的返回值是否为 1;是否重复调用 htonl() |
一次 recv() 少于预期 |
TCP 是字节流,应按协议循环接收,而不是假设一次读全 |
18. 录音与教材表述校正
- 转写中的
BCP、PCP、GDP等结合上下文多数应为 TCP。 - 转写中的
ITV4、IDV4等应为 IPv4 ;AFINET应为 AF_INET。 - Socket 是应用使用网络协议栈的通信端点,不能简单等同为"硬件层调用网络驱动"。
- 端口的数值范围是 0---65535;Linux 上 1024 以下端口通常需要相应能力,普通用户并非在所有系统上都绝对不能使用。
SOCK_STREAM是 Socket 语义类型,在 IPv4 常用组合中通常选择 TCP;它本身不是 TCP 的另一个名称。protocol = 0表示让内核选择该地址族和 Socket 类型的默认协议,不是启用优化。sockaddr与sockaddr_in不是 C++ 式基类和继承关系。- 监听 Socket 与
accept()返回的连接 Socket 用途不同;不能用一个描述符概括两者。 listen()的backlog不是服务器生命周期内能够服务的总客户端数量。send()和recv()的正返回值是本次实际处理的字节数,可能少于请求值。shutdown()控制一个或两个通信方向;close()释放文件描述符引用,两者不能简单互换解释。- 主机字节序取决于 CPU 和系统,不能普遍定义为小端;网络字节序固定为大端。
- 大小端只影响多字节数值的内存表示,不是把任意字符串或整个缓冲区反转。
inet_pton()转换结果已经是网络字节序,不再额外调用htonl()。inet_addr()对错误输入返回INADDR_NONE,会与合法广播地址255.255.255.255的结果冲突;新程序优先用inet_pton()。
19. 本节最低掌握标准
学完后应能回答:
- Linux 为什么用文件描述符表示 Socket?
socket(AF_INET, SOCK_STREAM, 0)三个参数分别是什么?- TCP 服务端与客户端的 API 顺序分别是什么?
bind()为什么接收struct sockaddr *,实际却填写sockaddr_in?listen_fd与accept()返回的conn_fd有什么区别?send()成功是否保证把指定长度全部发送?recv()返回 0 表示什么?- 大端和小端怎样排列
0x12345678? htons()和htonl()分别处理多少位?- 为什么端口赋值用
htons()? inet_pton()与htonl()的职责有什么不同?- 为什么打印客户端端口前要用
ntohs()?
最低实践要求:
- 能独立写出
socket()及错误判断; - 能正确初始化
sockaddr_in; - 能用
htons()设置端口; - 能用
inet_pton()设置 IPv4 地址; - 能画出服务端和客户端的函数调用顺序;
- 能解释监听描述符与连接描述符为何必须分开。
20. PDF 页码与录音时间索引
| 主题 | PDF 页码 | 录音时间 |
|---|---|---|
| Socket 概念、组成、类型和作用 | 264---265 | 00:05---04:24 |
socket() 参数与返回值 |
265---267 | 04:33---06:45 |
bind()、地址族与地址结构 |
267---270 | 06:55---09:54 |
listen()、accept()、connect() |
270---272 | 10:10---11:29 |
send()、recv()、shutdown()、close() |
272---274 | 11:29---12:47 |
| 大端、小端和网络字节序 | 274---275 | 12:53---15:59 |
hton*、ntoh* 与 IP 地址转换 |
275---276 | 15:59---18:10 |
| 字节序测试例程 | 277 页起 | 本次录音未讲 |
进一步核对资料:
- Linux man-pages:socket(2)
- Linux man-pages:bind(2)
- Linux man-pages:listen(2)
- Linux man-pages:accept(2)
- Linux man-pages:ip(7)
- Linux man-pages:byteorder(3)
- Linux man-pages:inet_pton(3)
21. 下一步学习
教材第 277---280 页先通过小程序观察字节序和地址转换结果,第 280 页起进入"只接受一个连接"的 TCP 服务端/客户端范例。
下一阶段建议按以下顺序练习:
text
字节序转换小实验
↓
单连接 TCP 服务端和客户端
↓
循环处理 send/recv 的实际字节数
↓
把监听 fd 与连接 fd 分开管理
↓
多客户端与线程模型
这部分与后续测试 A 直接对应:测试需要 TCP Server/Client 和多个客户端,必须先掌握本节的 Socket 创建、地址填写、监听描述符、连接描述符以及网络字节序。