TCP 套接字:API 流程、地址结构与网络字节序

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

  1. 从监听 Socket 的待处理连接队列中取出一个连接;
  2. 创建一个新的已连接 Socket;
  3. 返回新的文件描述符;
  4. 可把客户端地址写入 addr;
  5. 保留原监听 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

这段程序应该能够解释到每一行:

  1. socket() 创建 IPv4 TCP 通信端点;
  2. sockaddr_in 保存 IPv4 地址;
  3. htons(8080) 把 16 位端口改为网络字节序;
  4. inet_pton() 把点分十进制地址转成二进制网络地址;
  5. ntohs() 把端口转回来,供本机打印;
  6. 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. 录音与教材表述校正

  1. 转写中的 BCP、PCP、GDP 等结合上下文多数应为 TCP。
  2. 转写中的 ITV4、IDV4 等应为 IPv4 ;AFINET 应为 AF_INET。
  3. Socket 是应用使用网络协议栈的通信端点,不能简单等同为"硬件层调用网络驱动"。
  4. 端口的数值范围是 0---65535;Linux 上 1024 以下端口通常需要相应能力,普通用户并非在所有系统上都绝对不能使用。
  5. SOCK_STREAM 是 Socket 语义类型,在 IPv4 常用组合中通常选择 TCP;它本身不是 TCP 的另一个名称。
  6. protocol = 0 表示让内核选择该地址族和 Socket 类型的默认协议,不是启用优化。
  7. sockaddr 与 sockaddr_in 不是 C++ 式基类和继承关系。
  8. 监听 Socket 与 accept() 返回的连接 Socket 用途不同;不能用一个描述符概括两者。
  9. listen() 的 backlog 不是服务器生命周期内能够服务的总客户端数量。
  10. send() 和 recv() 的正返回值是本次实际处理的字节数,可能少于请求值。
  11. shutdown() 控制一个或两个通信方向;close() 释放文件描述符引用,两者不能简单互换解释。
  12. 主机字节序取决于 CPU 和系统,不能普遍定义为小端;网络字节序固定为大端。
  13. 大小端只影响多字节数值的内存表示,不是把任意字符串或整个缓冲区反转。
  14. inet_pton() 转换结果已经是网络字节序,不再额外调用 htonl()。
  15. inet_addr() 对错误输入返回 INADDR_NONE,会与合法广播地址 255.255.255.255 的结果冲突;新程序优先用 inet_pton()。

19. 本节最低掌握标准

学完后应能回答:

  1. Linux 为什么用文件描述符表示 Socket?
  2. socket(AF_INET, SOCK_STREAM, 0) 三个参数分别是什么?
  3. TCP 服务端与客户端的 API 顺序分别是什么?
  4. bind() 为什么接收 struct sockaddr *,实际却填写 sockaddr_in?
  5. listen_fd 与 accept() 返回的 conn_fd 有什么区别?
  6. send() 成功是否保证把指定长度全部发送?
  7. recv() 返回 0 表示什么?
  8. 大端和小端怎样排列 0x12345678?
  9. htons() 和 htonl() 分别处理多少位?
  10. 为什么端口赋值用 htons()?
  11. inet_pton() 与 htonl() 的职责有什么不同?
  12. 为什么打印客户端端口前要用 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 页起 本次录音未讲

进一步核对资料:

21. 下一步学习

教材第 277---280 页先通过小程序观察字节序和地址转换结果,第 280 页起进入"只接受一个连接"的 TCP 服务端/客户端范例。

下一阶段建议按以下顺序练习:

text 复制代码
字节序转换小实验
    ↓
单连接 TCP 服务端和客户端
    ↓
循环处理 send/recv 的实际字节数
    ↓
把监听 fd 与连接 fd 分开管理
    ↓
多客户端与线程模型

这部分与后续测试 A 直接对应:测试需要 TCP Server/Client 和多个客户端,必须先掌握本节的 Socket 创建、地址填写、监听描述符、连接描述符以及网络字节序。

相关推荐
smaller_maple1 小时前
中兴 E2623 移动定制版路由器 破解 & 关闭远程服务 详细教程
网络·智能路由器
fhq_fn_jgd1 小时前
ARM I2C
网络
HZZD_HZZD1 小时前
弱网下CoAP还省吗?合众致达Cat.1电表90天实测:MQTT重连流量占52%、续航差距从38%拉大到73%
开发语言·物联网·php·腾讯云
一条破秋裤2 小时前
TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲
arm开发·网络协议·tcp/ip
phltxy2 小时前
C 语言文件操作:让数据从内存走向持久化
c语言·网络·数据库
一条破秋裤2 小时前
TCP 并发服务器:连接分析、多线程与多进程
服务器·网络·tcp/ip
北京恒星科通刘军2 小时前
灾害监测预警系统中,应急疏散广播的“应急叫应”实际送达率与有效性研究
网络·数据库·人工智能
TDengine (老段)3 小时前
集群从“能用“到“高可用“:副本、选主与故障切换到底怎么配
大数据·网络·数据库·物联网·oracle·时序数据库·tdengine
自己的九又四分之三站台3 小时前
TCP 与 UDP:从可靠字节流到无连接数据报,怎么选、怎么测
网络协议·tcp/ip·udp