当前位置: 首页 > news >正文

《UNIX网络编程卷1:套接字联网API》第3章 套接字编程简介

《UNIX网络编程卷1:套接字联网API》第3章 套接字编程简介


3.1 套接字地址结构:网络通信的基石

套接字地址结构是网络编程的核心数据结构,定义了通信实体的网络标识与端口信息。不同协议族(如IPv4、IPv6、Unix域)拥有独立的地址结构,但均通过通用结构 struct sockaddr 实现类型统一。

3.1.1 IPv4地址结构(sockaddr_in)
#include <netinet/in.h>

struct sockaddr_in {
    uint8_t         sin_len;     // 结构体长度(BSD系统特有)
    sa_family_t     sin_family;  // 地址族(AF_INET)
    in_port_t       sin_port;    // 16位端口号(网络字节序)
    struct in_addr  sin_addr;    // 32位IPv4地址(网络字节序)
    char            sin_zero[8]; // 填充字段(全零)
};

struct in_addr {
    in_addr_t s_addr; // 32位IPv4地址(网络字节序)
};

关键点

  • sin_portsin_addr 必须使用网络字节序(大端模式);
  • sin_zero 字段用于填充,确保结构体与 sockaddr 对齐;
  • 实际编程中需使用 bzeromemset 初始化结构体,避免内存残留。
3.1.2 IPv6地址结构(sockaddr_in6)
struct sockaddr_in6 {
    uint8_t         sin6_len;      // 结构体长度
    sa_family_t     sin6_family;   // 地址族(AF_INET6)
    in_port_t       sin6_port;     // 16位端口号(网络字节序)
    uint32_t        sin6_flowinfo; // 流标签(QoS支持)
    struct in6_addr sin6_addr;     // 128位IPv6地址
    uint32_t        sin6_scope_id; // 作用域标识符(链路本地地址使用)
};

struct in6_addr {
    uint8_t s6_addr[16]; // 128位IPv6地址
};

特点

  • 支持更大的地址空间(128位)和流标签(用于区分数据流优先级);
  • 作用域标识符用于处理链路本地地址(如 fe80::1%eth0)。
3.1.3 通用地址结构(sockaddr与sockaddr_storage)
struct sockaddr {
    uint8_t     sa_len;     // 结构体长度
    sa_family_t sa_family;  // 地址族(AF_xxx)
    char        sa_data[14];// 协议地址(IP+端口)
};

struct sockaddr_storage {
    sa_family_t ss_family;  // 地址族
    char        __ss_pad1[_SS_PAD1SIZE]; // 填充字段
    // ... 其他对齐字段(总长度通常为128字节)
};

设计意义

  • sockaddr 用于兼容旧版API,但无法容纳IPv6地址;
  • sockaddr_storage 是通用解决方案,可存储任意协议族的地址结构。

内存对齐图

偏移量 | 成员名称        | 大小(字节)  | 对齐方式         | 说明
-------------------------------------------------------------
0x00   | ss_family     | 2            | 自然对齐(2字节)| 地址族标识(如 AF_INET)
0x02   | __ss_pad1     | 6            | 填充至 8 字节对齐| 确保后续字段对齐
0x08   | __ss_align    | 8            | 8 字节对齐       | 强制 64 位对齐的占位符
0x10   | __ss_pad2     | 112          | 填充至 128 字节  | 剩余空间填充
-------------------------------------------------------------
总大小:128 字节

说明:sockaddr_storage通过填充字段确保内存对齐,避免平台兼容性问题。
以下是 sockaddr_storage 结构体的内存对齐图及详细分析,结合 Linux 和 Windows 系统的实现差异进行说明:

关键设计解析

  1. 多平台兼容性

    • Linuxss_family(2字节)后通过 __ss_pad1(6字节)填充至 8 字节边界,使 __ss_align(8字节)满足 64 位对齐。
    • Windowsss_family(2字节)后直接填充 48 字节(__ss_pad1),再放置 __ss_align(8字节),最后通过 __ss_pad2(72字节)填充至 128 字节。
  2. 对齐规则

    • 自然对齐:首个成员 ss_family 从偏移量 0 开始,占用 2 字节。
    • 强制对齐__ss_align 需对齐到 8 字节边界(编译器默认对齐数),确保后续字段(如 IPv6 地址)无错位访问。
    • 总大小约束:结构体总大小需为最大对齐数(8字节)的整数倍,故填充至 128 字节。
  3. 功能目的

    • 通用存储:128 字节空间可容纳 IPv4(16字节)、IPv6(28字节)等地址,避免动态类型判断。
    • 类型转换安全:强制对齐后,可直接将协议特定地址(如 sockaddr_in6)强制转换为 sockaddr_storage,避免内存越界。

实际应用场景

// 示例:接收任意协议地址
struct sockaddr_storage addr;
socklen_t addr_len = sizeof(addr);
recvfrom(sockfd, buffer, sizeof(buffer), 0, 
         (struct sockaddr*)&addr, &addr_len);

// 根据 ss_family 处理不同协议
if (addr.ss_family == AF_INET) {
    struct sockaddr_in *ipv4_addr = (struct sockaddr_in*)&addr;
    // 处理 IPv4 地址
} else if (addr.ss_family == AF_INET6) {
    struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6*)&addr;
    // 处理 IPv6 地址
}

总结

sockaddr_storage 通过精心设计的填充字段和对齐机制,在以下方面达到平衡:

  • 空间效率:固定 128 字节,覆盖所有已知协议地址。
  • 访问安全性:强制对齐避免 CPU 非对齐访问异常。
  • 代码简洁性:统一接口处理多协议地址,降低网络编程复杂度。

3.2 字节序转换:跨越异构系统的桥梁

网络通信要求数据以**网络字节序(大端模式)**传输,主机字节序可能为小端(如x86)或大端(如PowerPC),需通过转换函数统一。

3.2.1 转换函数详解
#include <netinet/in.h>

// 主机字节序 → 网络字节序
uint16_t htons(uint16_t hostshort); // 短整型(端口号)
uint32_t htonl(uint32_t hostlong);  // 长整型(IP地址)

// 网络字节序 → 主机字节序
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

嵌入式应用场景

  • ARM设备(小端)与DSP处理器(大端)通信时需显式转换;
  • 示例:设置端口号 servaddr.sin_port = htons(8080);
3.2.2 结构体字段转换实践
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999); // 转换端口号
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr); // IP地址转换

3.3 地址转换函数:字符串与二进制的互操作

IPv4/IPv6地址常需在可读字符串(如"192.168.1.1")和二进制格式间转换,推荐使用现代函数 inet_ptoninet_ntop

3.3.1 函数原型与参数解析
#include <arpa/inet.h>

// 字符串 → 二进制(支持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);

参数说明

  • af:地址族(AF_INETAF_INET6);
  • src:输入数据指针;
  • dst:输出缓冲区;
  • size:缓冲区长度(推荐使用 INET_ADDRSTRLENINET6_ADDRSTRLEN)。

代码示例

char ipv4_str[INET_ADDRSTRLEN];
struct in_addr ipv4_addr;
inet_pton(AF_INET, "203.0.113.1", &ipv4_addr); // 字符串转二进制
inet_ntop(AF_INET, &ipv4_addr, ipv4_str, INET_ADDRSTRLEN); // 二进制转字符串
3.3.2 弃用函数警告
  • inet_addrinet_aton 仅支持IPv4,且无错误返回;
  • inet_ntoa 使用静态缓冲区,非线程安全。

3.4 套接字API核心函数解析
3.4.1 socket():创建通信端点
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数详解

  • domain:协议族(如 AF_INETAF_INET6AF_UNIX);
  • type:套接字类型(SOCK_STREAMSOCK_DGRAM);
  • protocol:通常为0(自动选择默认协议)。

返回值

  • 成功:返回非负套接字描述符;
  • 失败:返回-1,设置 errno(如 EPROTONOSUPPORT 协议不支持)。

代码示例

int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}
3.4.2 bind():绑定本地地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

关键点

  • 服务器必须调用 bind 以指定监听地址和端口;
  • 客户端通常无需显式绑定,由内核自动分配临时端口。

错误处理

  • EADDRINUSE:端口被占用;
  • EACCES:尝试绑定特权端口(<1024)无权限。

代码示例

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
servaddr.sin_port = htons(8080);

if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
3.4.3 listen():开启监听模式
int listen(int sockfd, int backlog);

参数解析

  • backlog:已完成连接队列(ESTABLISHED)的最大长度;
  • Linux内核4.3+后,backlog 仅限制已完成队列,由 net.core.somaxconn 定义上限。

最佳实践

if (listen(sockfd, SOMAXCONN) < 0) {
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
3.4.4 accept():接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

值-结果参数

  • addr:客户端地址结构指针;
  • addrlen:输入时为缓冲区大小,返回时为实际地址长度。

代码示例

struct sockaddr_in cliaddr;
socklen_t cliaddr_len = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
if (connfd < 0) {
    perror("accept failed");
    continue; // 非致命错误可继续运行
}
3.4.5 connect():发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

错误处理

  • ETIMEDOUT:服务器无响应(网络不通或防火墙拦截);
  • ECONNREFUSED:目标端口无服务监听;
  • EHOSTUNREACH:路由不可达。

代码示例

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "203.0.113.1", &servaddr.sin_addr);

if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
    perror("connect failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

3.5 值-结果参数:内核与用户空间的协作

套接字API中,地址长度参数(如 acceptaddrlen)采用值-结果传递机制:

  • 输入:用户告知内核缓冲区大小,避免内存溢出;
  • 输出:内核返回实际地址长度,用户可验证协议类型。

示例分析

socklen_t len = sizeof(struct sockaddr_storage);
struct sockaddr_storage client_addr;
int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);

// 根据实际协议族处理连接
if (client_addr.ss_family == AF_INET) {
    struct sockaddr_in *ipv4_addr = (struct sockaddr_in*)&client_addr;
    // 处理IPv4地址
} else if (client_addr.ss_family == AF_INET6) {
    struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6*)&client_addr;
    // 处理IPv6地址
}

3.6 错误处理与包裹函数设计
3.6.1 系统调用错误处理模式

UNIX系统调用通过返回-1并设置 errno 指示错误类型:

if ((n = read(fd, buf, size)) < 0) {
    if (errno == EINTR) // 被信号中断
        goto retry;
    else
        err_sys("read error"); // 终止程序
}
3.6.2 包裹函数封装

通过包裹函数简化错误处理,提升代码可读性:

int Socket(int family, int type, int protocol) {
    int n;
    if ((n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return n;
}

void Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
    if (connect(sockfd, addr, addrlen) < 0)
        err_sys("connect error");
}

优点

  • 统一错误处理逻辑;
  • 减少代码冗余;
  • 支持调试日志插入。

3.7 实战:完整TCP回射服务器/客户端
3.7.1 服务器端代码
#include "unp.h"

void str_echo(int sockfd) {
    ssize_t n;
    char buf[MAXLINE];
    while ((n = Read(sockfd, buf, MAXLINE)) > 0)
        Writen(sockfd, buf, n);
}

int main() {
    int listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);

    for (;;) {
        int connfd = Accept(listenfd, NULL, NULL);
        if (fork() == 0) { // 子进程
            Close(listenfd);
            str_echo(connfd);
            Close(connfd);
            exit(0);
        }
        Close(connfd); // 父进程关闭连接套接字
    }
}
3.7.2 客户端代码
#include "unp.h"

void str_cli(FILE *fp, int sockfd) {
    char sendline[MAXLINE], recvline[MAXLINE];
    while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Writen(sockfd, sendline, strlen(sendline));
        if (Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
        Fputs(recvline, stdout);
    }
}

int main(int argc, char **argv) {
    if (argc != 2)
        err_quit("usage: tcpcli <IPaddress>");

    int sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(9999);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
    str_cli(stdin, sockfd);
    exit(0);
}

3.8 图文说明与调试技巧
  1. TCP三次握手与套接字函数关联图
    在这里插入图片描述

    说明:socket()、bind()、listen()在服务器端准备连接,accept()阻塞等待SYN。

  2. 值-结果参数传递示意图

    +----------------+          +----------------+
    |   用户空间      |          |   内核空间      |
    | addrlen=16     |  --->    | 检查缓冲区大小  |
    | (输入值)       |  <---    | 设置实际长度    |
    +----------------+          +----------------+
    
  3. 常见错误排查表

    错误码原因解决方案
    EADDRINUSE端口被占用更换端口或设置SO_REUSEADDR
    ECONNREFUSED目标端口无服务检查服务器是否运行
    ENETUNREACH网络不可达检查路由与防火墙配置

3.9 本章小结与进阶习题

小结:本章深入解析了套接字地址结构、核心API函数及错误处理机制,通过完整的TCP案例演示了网络编程的基础流程,为后续高级主题奠定基础。

习题

  1. 修改回射服务器支持IPv6,并测试客户端连接;
  2. 实现基于UDP的简单聊天程序,支持多客户端通信;
  3. 使用Wireshark抓取TCP握手过程,分析序列号与确认号变化。

付费用户专属资源

  • 完整代码工程(含IPv4/IPv6双栈支持);

  • 套接字API调用流程图(矢量图);

  • 扩展阅读:《UNIX网络编程中的原子操作与线程安全》。

    | 目标端口无服务 | 检查服务器是否运行 |
    | ENETUNREACH | 网络不可达 | 检查路由与防火墙配置 |


3.9 本章小结与进阶习题

小结:本章深入解析了套接字地址结构、核心API函数及错误处理机制,通过完整的TCP案例演示了网络编程的基础流程,为后续高级主题奠定基础。

习题

  1. 修改回射服务器支持IPv6,并测试客户端连接;
  2. 实现基于UDP的简单聊天程序,支持多客户端通信;
  3. 使用Wireshark抓取TCP握手过程,分析序列号与确认号变化。

付费用户专属资源

  • 完整代码工程(含IPv4/IPv6双栈支持);
  • 套接字API调用流程图(矢量图);
  • 扩展阅读:《UNIX网络编程中的原子操作与线程安全》。

通过本章学习,读者将掌握套接字编程的核心技术,并能够开发健壮的网络应用。

相关文章:

  • MBR的 扩展分区 和 逻辑分区 笔记250407
  • 循环神经网络 - 机器学习任务之同步的序列到序列模式
  • 计算机网络学习前言
  • 八、C++速通秘籍—动态多态(运行期)
  • 【蓝桥杯】搜索算法:剪枝技巧+记忆化搜索
  • SpringBoot类跨包扫描失效的几种解决方法
  • SpringBoot企业级开发之【用户模块-登录】
  • 群晖NAS的最好的下载软件/电影的方式(虚拟机安装win系统安装下载软件)
  • 【5分钟论文阅读】InstructRestore论文解读
  • linux-core分析 : sip变量赋值-指针悬挂
  • 【LeetCode】算法详解#3 ---最大子数组和
  • 人工智能新时代:从深度学习到自主智能
  • 人工智能:深度学习关键技术与原理详解
  • LeetCode 解题思路 30(Hot 100)
  • 硬盘分区格式之GPT(GUID Partition Table)笔记250407
  • 【k8s学习之CSI】理解 LVM 存储概念和相关操作
  • 喂饭教程-Dify如何集成RAGFlow知识库
  • [ISP] ISP 中的 GTM 与 LTM:原理、算法与与 Gamma 校正的对比详解
  • Token+JWT+Redis 实现鉴权机制
  • 2024年十五届蓝桥杯青少年Scratch省赛初级组——找不同
  • 吉利汽车一季度净利润大增264%,称整合极氪后实现整体效益超5%
  • 光明日报:家长孩子共同“息屏”,也要保证高质量陪伴
  • 彭丽媛同巴西总统夫人罗桑热拉参观中国国家大剧院
  • 上海首发经济“卷”到会展业,浦东签约三个年度“首展”
  • 多家中小银行存款利率迈入“1时代”
  • 美国“贸易战”前线的本土受害者:安静的洛杉矶港和准备关门的小公司