作者: andylin02
学习章节: 第三章 套接字编程简介
关键词: 套接字地址结构, sockaddr_in, 字节序, 大端, 小端, 值-结果参数, inet_pton, inet_ntop, 字节操纵函数, readn, writen, readline, 包裹函数


一、章节概述

1.1 本章焦点

第三章真正开始进入套接字API的世界。前一章讲的是TCP、UDP、SCTP这些传输层协议的理论知识,而从本章开始,要学习如何使用套接字来实际编写网络程序。本章不涉及具体的客户/服务器完整实现(那是第四章的内容),而是专注于介绍套接字编程的基础构件——地址结构、字节序转换、地址格式转换等基础函数。

如果本章的概念没有吃透,后续的编程将寸步难行。所有套接字函数都依赖于对地址结构和字节序的正确理解。

1.2 本章内容结构

节号 标题 核心内容
3.2 套接字地址结构 IPv4地址结构、通用地址结构、IPv6地址结构
3.3 值-结果参数 从进程到内核 vs 从内核到进程的参数传递方式
3.4 字节排序函数 大端与小端、htons/htonl/ntohs/ntohl
3.5 字节操纵函数 bzero/bcopy/bcmp vs memset/memcpy/memcmp
3.6 inet_atoninet_addrinet_ntoa函数 IPv4专用的地址转换函数(过时)
3.7 inet_ptoninet_ntop函数 协议无关的地址转换函数(推荐)
3.8 sock_ntop和相关函数 作者封装的协议无关地址转换函数
3.9 readnwritenreadline函数 处理不足字节计数的包裹函数

💡 本章核心价值:读完第三章,你应该能够——

  • 理解套接字地址结构的每个字段的含义
  • 正确使用字节序转换函数
  • 使用协议无关的inet_pton/inet_ntop编写IPv4/IPv6兼容的程序
  • 理解值-结果参数的设计思想
  • 知道为什么需要readn/writen/readline以及如何实现它们

二、套接字地址结构

2.1 IPv4套接字地址结构

IPv4套接字地址结构通常称为“网际套接字地址结构”,以sockaddr_in命名,定义在<netinet/in.h>头文件中。POSIX规范定义的结构如下:

#include <netinet/in.h>

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

struct sockaddr_in {
    uint8_t         sin_len;     // 结构体长度(16字节)
    sa_family_t     sin_family;  // 地址族:AF_INET
    in_port_t       sin_port;    // 16位TCP/UDP端口号,网络字节序
    struct in_addr  sin_addr;    // 32位IPv4地址,网络字节序
    char            sin_zero[8]; // 未使用,用于填充
};

各字段详解:

字段 类型 说明
sin_len uint8_t 结构长度,使实现可以处理变长结构,并非所有实现都有此字段。POSIX不要求必须存在。
sin_family sa_family_t 地址族,必须是AF_INET。POSIX规范要求至少包含sin_familysin_portsin_addr三个字段。
sin_port in_port_t 16位端口号,必须以网络字节序存储
sin_addr struct in_addr 32位IPv4地址,必须以网络字节序存储
sin_zero char[8] 填充字段,使结构体大小与struct sockaddr兼容,通常用bzeromemset置0

💡 关键理解:套接字地址结构仅在给定主机上使用——虽然结构中某些字段(如IP地址和端口号)用在不同主机之间通信中,但是结构本身并不在主机之间传递。

2.2 通用套接字地址结构

为什么需要通用地址结构?因为套接字函数需要支持多种协议族(IPv4、IPv6、Unix域等)。bindconnectaccept等函数的参数是指向特定协议地址结构的指针,为了能够处理所有协议族,这些函数被设计为接受一个指向通用套接字地址结构的指针。

#include <sys/socket.h>

struct sockaddr {
    uint8_t         sa_len;      // 结构体长度
    sa_family_t     sa_family;   // 地址族
    char            sa_data[14]; // 协议特定数据
};

如何使用:

当向任何套接字函数传递地址结构时,必须将特定协议的地址结构指针强制转换为struct sockaddr*类型:

struct sockaddr_in serv;
// ... 填充 serv 结构 ...
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));

💡 为什么不用void* 套接字函数早于ANSI C标准,当时还没有void*类型,所以采用了struct sockaddr*作为通用指针类型。历史原因使然,现在依然沿用这套约定。

2.3 IPv6套接字地址结构

IPv6套接字地址结构在<netinet/in.h>中定义:

struct in6_addr {
    uint8_t  s6_addr[16];        // 128位IPv6地址,网络字节序
};

struct sockaddr_in6 {
    uint8_t         sin6_len;     // 结构体长度(28字节)
    sa_family_t     sin6_family;  // 地址族:AF_INET6
    in_port_t       sin6_port;    // 端口号,网络字节序
    uint32_t        sin6_flowinfo;// 流信息
    struct in6_addr sin6_addr;    // 128位IPv6地址,网络字节序
    uint32_t        sin6_scope_id;// 范围ID
};

2.4 新的通用地址结构:sockaddr_storage

sockaddr_storage是比struct sockaddr更通用的地址结构,足以容纳系统支持的任何套接字地址结构:

#include <sys/socket.h>

struct sockaddr_storage {
    uint8_t      ss_len;         // 结构长度
    sa_family_t  ss_family;      // 地址族
    // 其他填充字段,足以容纳最大地址结构
};

struct sockaddr的区别: sockaddr_storage能够容纳系统支持的任何地址结构,而struct sockaddr可能不够大。在编写协议无关的程序时,推荐使用sockaddr_storage作为通用地址结构。

2.5 地址结构对比总览

地址结构 用途 关键字段 大小(典型值)
struct sockaddr 通用地址(旧式) sa_family, sa_data 16字节
struct sockaddr_in IPv4 sin_family, sin_port, sin_addr 16字节
struct sockaddr_in6 IPv6 sin6_family, sin6_port, sin6_addr 28字节
struct sockaddr_un Unix域协议 sun_family, sun_path 可变
struct sockaddr_storage 新的通用结构 ss_family 足够大

三、值-结果参数

3.1 什么是值-结果参数?

当向套接字函数传递地址结构时,需要同时传递该结构的长度。但传递方式取决于结构的传递方向

从进程到内核: 直接传递结构大小的值(值参数)

connect(sockfd, (SA*) &serv, sizeof(serv));

从内核到进程: 传递指向结构大小变量的指针(值-结果参数)

struct sockaddr_un cli;
socklen_t len = sizeof(cli);
getpeername(unixfd, (SA*) &cli, &len);   // len可能被修改

3.2 为什么需要值-结果参数?

当函数被调用时,结构大小是一个(value),它告诉内核该结构的大小,内核在写这个结构时就不会越界;当函数返回时,结构大小又是一个结果(result),它告诉进程内核在该结构中究竟存储了多少信息。

不同类型的套接字地址结构长度不同。IPv4和IPv6的长度是固定的,但Unix域套接字和数据链路结构是可变长度的。内核需要告诉应用程序实际存储了多少字节,因此必须传递指针而不是值。

3.3 两类传递方向的函数汇总

传递方向 函数 参数形式
进程 → 内核 bind, connect, sendto 直接传递大小值
内核 → 进程 accept, recvfrom, getsockname, getpeername 传递指向大小变量的指针

3.4 值-结果参数在其他场景中的应用

除了套接字地址结构,本书后面还会遇到其他值-结果参数:

  • select函数的中间3个参数(fd_set集合)
  • getsockopt函数的长度参数
  • recvmsg函数中msghdr结构体的msg_namelenmsg_controllen字段
  • ifconf结构中的ifc_len字段

四、字节排序函数

4.1 大端与小端

不同的操作系统在内存中存储多字节整数的方式不同:

字节序 存储方式 常见系统
大端字节序 高序字节存储在内存的低地址 网络字节序、某些嵌入式系统
小端字节序 高序字节存储在内存的高地址 Linux、Windows、x86架构

网络字节序:网络协议规定使用大端字节序。当在主机之间传递多字节整数时,必须将其转换为网络字节序。

💡 为什么需要转换? 如果主机是小端序,直接发送二进制整数,接收方按大端序解析就会得到错误的值。使用字节序转换函数可以确保无论主机是什么字节序,网络传输的数据格式是一致的。

4.2 字节序判断程序

利用union联合体,所有成员共享同一块内存,可以判断当前主机的字节序:

#include "unp.h"

int main(int argc, char **argv)
{
    union {
        short s;
        char c[sizeof(short)];
    } un;

    un.s = 0x0102;    // 十六进制:0x01是高字节,0x02是低字节

    printf("%s: ", CPU_VENDOR_OS);
    if (sizeof(short) == 2) {
        if (un.c[0] == 1 && un.c[1] == 2)
            printf("大端字节序\n");
        else if (un.c[0] == 2 && un.c[1] == 1)
            printf("小端字节序\n");
        else
            printf("未知\n");
    } else {
        printf("sizeof(short) = %d\n", sizeof(short));
    }

    return 0;
}

4.3 字节序转换函数

网络字节序转换函数在<netinet/in.h>中定义:

#include <netinet/in.h>

uint16_t htons(uint16_t host16bitvalue);   // 主机 → 网络(16位)
uint32_t htonl(uint32_t host32bitvalue);   // 主机 → 网络(32位)
uint16_t ntohs(uint16_t net16bitvalue);    // 网络 → 主机(16位)
uint32_t ntohl(uint32_t net32bitvalue);    // 网络 → 主机(32位)

函数命名解析:

缩写 含义 缩写 含义
h host(主机) n network(网络)
s short(16位) l long(32位)

使用示例:

// 设置端口号:主机字节序 → 网络字节序
servaddr.sin_port = htons(13);

// 设置IP地址:INADDR_ANY(0)不需要转换,但为了风格统一仍可调用htonl
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

// 读取客户端端口号:网络字节序 → 主机字节序
printf("port: %d\n", ntohs(client_addr.sin_port));

⚠️ 注意sin_addrsin_port字段必须始终以网络字节序存储。在设置这些字段时,务必使用字节序转换函数。

五、字节操纵函数

5.1 源于BSD的函数

#include <strings.h>

void bzero(void *dest, size_t nbytes);              // 将n字节置0
void bcopy(const void *src, void *dest, size_t nbytes); // 复制n字节
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); // 比较,相同返回0

5.2 ANSI C函数

#include <string.h>

void *memset(void *dest, int c, size_t len);        // 将len字节置为c
void *memcpy(void *dest, const void *src, size_t nbytes); // 复制n字节
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes); // 比较

5.3 两种函数族的区别

特性 bzero/bcopy/bcmp memset/memcpy/memcmp
来源 BSD ANSI C
参数顺序 bcopy(src, dest, n) memcpy(dest, src, n)
重叠处理 bcopy能正确处理 memcpy结果不可知(需用memmove)

💡 使用建议:在本书示例中,作者使用bzero来清零地址结构。实际编程中也可以使用memset,两者功能等效。

六、地址转换函数

6.1 两类函数对比

地址转换函数用于在ASCII字符串(如"192.168.1.1")和网络字节序的二进制值(存放在套接字地址结构中)之间进行转换。

函数 适用协议 线程安全 可重入 推荐程度
inet_aton/inet_ntoa 仅IPv4 不(使用静态数据) ❌ 已过时
inet_addr 仅IPv4 - - ❌ 已废弃
inet_pton/inet_ntop IPv4和IPv6 强烈推荐

6.2 inet_pton和inet_ntop(推荐)

这两个函数是协议无关的,可同时用于IPv4和IPv6。

#include <arpa/inet.h>

// 表达式格式 → 数值格式
int inet_pton(int family, const char *strptr, void *addrptr);
// 返回值:1=成功,0=输入无效,-1=出错

// 数值格式 → 表达式格式
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 返回值:指向结果的指针,若出错则返回NULL

参数说明:

  • familyAF_INET(IPv4)或AF_INET6(IPv6)
  • strptr:点分十进制的IP地址字符串
  • addrptr:存放二进制结果的缓冲区
  • len:目标字符串缓冲区的大小

inet_ntop的缓冲区大小常量:

#include <netinet/in.h>

#define INET_ADDRSTRLEN     16   // IPv4点分十进制字符串的最大长度
#define INET6_ADDRSTRLEN    46   // IPv6字符串的最大长度

6.3 使用示例

struct sockaddr_in servaddr;

// 将字符串IP地址转换为二进制,存入servaddr.sin_addr
if (inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr) <= 0) {
    err_quit("inet_pton error");
}

// 将二进制IP地址转换回字符串
char str[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &servaddr.sin_addr, str, sizeof(str)) == NULL) {
    err_sys("inet_ntop error");
}
printf("IP address: %s\n", str);

6.4 旧式函数(了解即可)

这些函数在旧代码中可能出现,但新程序不应使用:

// inet_aton:将字符串转换为网络字节序的IPv4地址
int inet_aton(const char *strptr, struct in_addr *addrptr);

// inet_addr:已废弃,对255.255.255.255处理有问题
in_addr_t inet_addr(const char *strptr);

// inet_ntoa:返回指向静态数据的指针,不可重入
char *inet_ntoa(struct in_addr inaddr);

七、sock_ntop和相关函数

作者提供了一个协议无关的地址转换函数sock_ntop,其输出字符串中包含端口号,相比inet_ntop更加方便。这类自定义函数用于简化代码,使其与协议无关:

// 函数原型(来自unp.h)
char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);

该函数通过检查sockaddr中的地址族,自动判断是IPv4还是IPv6,并返回格式如192.168.1.100:8888的字符串。

八、readn、writen和readline函数

8.1 为什么需要这些函数?

字节流套接字上调用readwrite时,读取或写入的字节数可能比请求的数量少。这不是错误,而是因为内核中用于套接字的缓冲区可能已达到了极限(例如接收缓冲区没有足够数据或发送缓冲区没有足够空间)。

常见场景:

场景 说明
read返回比请求少 接收缓冲区数据不足,只返回当前可用数据
write写入比请求少 发送缓冲区空间不足,只写入部分数据
read返回0 对端关闭连接

💡 关键理解:普通的readwrite是“尽力而为”的。它们不保证一次调用就能传输完请求的所有字节。readnwriten函数通过循环调用,确保传输完指定数量的字节。

8.2 readn函数实现

#include "unp.h"

ssize_t readn(int fd, void *vptr, size_t n)
{
    size_t  nleft;
    ssize_t nread;
    char   *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)     // 被信号中断,重试
                nread = 0;
            else                    // 真正的错误
                return -1;
        } else if (nread == 0) {    // EOF,对端关闭连接
            break;
        }
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;               // 返回实际读取的字节数
}

ssize_t Readn(int fd, void *ptr, size_t nbytes)
{
    ssize_t n;
    if ((n = readn(fd, ptr, nbytes)) < 0)
        err_sys("readn error");
    return n;
}

⚠️ 关于EINTRreadwrite系统调用在执行过程中被捕获的信号中断时会产生EINTR错误,此时应该忽略该错误并继续被中断的系统调用。

8.3 writen函数实现

#include "unp.h"

ssize_t writen(int fd, const void *vptr, size_t n)
{
    size_t      nleft;
    ssize_t     nwritten;
    const char *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)  // 被信号中断,重试
                nwritten = 0;
            else                                 // 真正的错误
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

void Writen(int fd, void *ptr, size_t nbytes)
{
    if (writen(fd, ptr, nbytes) != nbytes)
        err_sys("writen error");
}

8.4 readline函数实现

readline函数从套接字中一次读取一行文本。为了提高效率,它使用内部缓冲区每次读取多个字符,然后逐行返回:

#include "unp.h"

static int  read_cnt;      // 内部缓冲区中剩余的字符数
static char *read_ptr;     // 当前读取位置指针
static char read_buf[MAXLINE];  // 内部缓冲区

static ssize_t my_read(int fd, char *ptr)
{
    if (read_cnt <= 0) {
    again:
        if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR)
                goto again;
            return -1;
        } else if (read_cnt == 0) {
            return 0;
        }
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char    c, *ptr;

    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ((rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n')
                break;           // 遇到换行符,停止读取
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;        // EOF,返回已读取的字节数
        } else {
            return -1;           // 错误
        }
    }
    *ptr = 0;
    return n;
}

九、关键图表

9.1 套接字地址结构传递示意图

┌─────────────────────────────────────────────────────────────────────────────┐
│                          套接字地址结构传递                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  进程 → 内核(值参数)                         内核 → 进程(值-结果参数)       │
│  ┌─────────────┐                             ┌─────────────┐                │
│  │  bind       │                             │  accept     │                │
│  │  connect    │                             │  recvfrom   │                │
│  │  sendto     │                             │  getsockname│                │
│  └──────┬──────┘                             │  getpeername│                │
│         │                                    └──────┬──────┘                │
│         │                                           │                        │
│         ▼                                           ▼                        │
│  ┌─────────────────┐                       ┌─────────────────┐              │
│  │ 传递地址结构指针 │                       │ 传递地址结构指针 │              │
│  │ 传递结构大小值   │                       │ 传递指向大小的指针│              │
│  └─────────────────┘                       └─────────────────┘              │
│                                                                              │
│  内核知道结构大小,不会越界                  内核写入后告诉应用程序写了多少      │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

9.2 字节序转换流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                            字节序转换流程                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────┐     htons/htonl    ┌─────────┐                                 │
│  │ 主机端口 │ ──────────────────→│ 网络端口 │ ────→ 发送到网络               │
│  │ (小端)  │                     │ (大端)  │                                 │
│  └─────────┘                     └─────────┘                                 │
│                                                                              │
│  ┌─────────┐     ntohs/ntohl    ┌─────────┐                                 │
│  │ 主机端口 │ ←──────────────────│ 网络端口 │ ←───── 从网络接收               │
│  │ (小端)  │                     │ (大端)  │                                 │
│  └─────────┘                     └─────────┘                                 │
│                                                                              │
│  h = host (主机)   n = network (网络)   s = short (16位)   l = long (32位)   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

9.3 地址转换函数调用流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                         地址转换函数调用流程                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  方式一(inet_pton / inet_ntop - 推荐):                                     │
│                                                                              │
│  ┌─────────────────┐   inet_pton   ┌─────────────────┐                      │
│  │ "192.168.1.100" │ ────────────→ │ 0xC0A80164 (大端)│                      │
│  │  (ASCII字符串)  │               │   (二进制)      │                      │
│  └─────────────────┘               └────────┬────────┘                      │
│                                              │                               │
│                                              │ inet_ntop                     │
│                                              ▼                               │
│                                      ┌─────────────────┐                    │
│                                      │ "192.168.1.100" │                    │
│                                      └─────────────────┘                    │
│                                                                              │
│  方式二(旧式函数 - 不推荐):                                                  │
│                                                                              │
│  ┌─────────────────┐   inet_aton   ┌─────────────────┐                      │
│  │ "192.168.1.100" │ ────────────→ │ 0xC0A80164      │                      │
│  └─────────────────┘               └────────┬────────┘                      │
│                                              │                               │
│                                              │ inet_ntoa                     │
│                                              │ (返回静态数据,不可重入)        │
│                                              ▼                               │
│                                      ┌─────────────────┐                    │
│                                      │ "192.168.1.100" │                    │
│                                      └─────────────────┘                    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

十、完整示例代码

10.1 套接字地址结构初始化完整示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in servaddr;
    char str[INET_ADDRSTRLEN];

    if (argc != 2) {
        fprintf(stderr, "usage: %s <IPaddress>\n", argv[0]);
        exit(1);
    }

    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        exit(1);
    }

    // 2. 清零地址结构
    bzero(&servaddr, sizeof(servaddr));
    // 或使用: memset(&servaddr, 0, sizeof(servaddr));

    // 3. 设置地址族
    servaddr.sin_family = AF_INET;

    // 4. 设置端口号(主机字节序 → 网络字节序)
    servaddr.sin_port = htons(8888);

    // 5. 设置IP地址(字符串 → 二进制,网络字节序)
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        fprintf(stderr, "inet_pton error for %s\n", argv[1]);
        exit(1);
    }

    // 6. 演示逆转换:将二进制IP转回字符串
    if (inet_ntop(AF_INET, &servaddr.sin_addr, str, sizeof(str)) == NULL) {
        perror("inet_ntop error");
        exit(1);
    }
    printf("IP address: %s, port: %d\n", str, ntohs(servaddr.sin_port));

    // 7. 连接服务器(假设connect实现)
    // connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    close(sockfd);
    return 0;
}

10.2 使用sock_ntop的示例

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;
    char *ptr;

    if (argc != 2)
        err_quit("usage: %s <IPaddress>", argv[0]);

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

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

    // 使用sock_ntop打印地址和端口
    ptr = sock_ntop((SA *)&servaddr, sizeof(servaddr));
    printf("Connecting to %s\n", ptr);

    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

    close(sockfd);
    return 0;
}

十一、本章习题精解

习题3.1

问题: 为什么套接字函数中从内核到进程传递地址结构时,需要传递指向长度变量的指针,而不是直接传递长度值?

答案: 因为不同类型的套接字地址结构长度不同。IPv4和IPv6的长度是固定的,但Unix域套接字和数据链路套接字结构是可变长度的。传递指针可以让内核告诉应用程序实际存储了多少字节。如果传递值,内核无法返回实际长度。

习题3.2

问题: 为什么在定义inet_ptoninet_ntop时使用void*参数,而其他地址结构使用struct sockaddr*

答案: void*只能用来传递参数,不能对void类型的指针进行加减和解引用操作。在inet_ptoninet_ntop中,只需要将二进制地址写入传入的缓冲区,不需要解析缓冲区内容,因此使用void*是合适的。而套接字函数需要解析地址结构中的具体字段,因此需要使用具体的结构指针类型。

习题3.3

问题: 编写一个inet_pton_loose函数,它能处理宽松的地址转换——如果inet_pton失败,则尝试inet_aton

参考实现:

#include "unp.h"

int inet_pton_loose(int family, const char *strptr, void *addrptr)
{
    if (family == AF_INET) {
        // 先尝试inet_pton
        if (inet_pton(AF_INET, strptr, addrptr) == 1)
            return 1;
        // 失败则尝试inet_aton
        struct in_addr v4addr;
        if (inet_aton(strptr, &v4addr) == 1) {
            memcpy(addrptr, &v4addr, sizeof(struct in_addr));
            return 1;
        }
        return 0;
    }
    else if (family == AF_INET6) {
        // 先尝试inet_pton
        if (inet_pton(AF_INET6, strptr, addrptr) == 1)
            return 1;
        // 失败则尝试将IPv4地址映射为IPv6地址
        struct in_addr v4addr;
        if (inet_aton(strptr, &v4addr) == 1) {
            struct in6_addr v6addr;
            bzero(&v6addr, sizeof(v6addr));
            // IPv4映射IPv6地址格式: ::FFFF:IPv4
            v6addr.s6_addr[10] = 0xff;
            v6addr.s6_addr[11] = 0xff;
            memcpy(&v6addr.s6_addr[12], &v4addr.s_addr, 4);
            memcpy(addrptr, &v6addr, sizeof(struct in6_addr));
            return 1;
        }
        return 0;
    }
    return -1;  // 不支持的地址族
}

十二、环境搭建快速参考

12.1 编译使用libunp.a

# 编译使用unp.h的程序
gcc -o myprogram myprogram.c -lunp

# 或指定库路径
gcc -o myprogram myprogram.c -L/usr/local/lib -lunp

12.2 常用头文件汇总

头文件 提供内容
<sys/socket.h> socket、bind、connect、listen、accept等核心函数
<netinet/in.h> sockaddr_in、inet_pton、inet_ntop、字节序转换函数
<arpa/inet.h> inet_pton、inet_ntop(部分系统)
<strings.h> bzero、bcopy、bcmp(BSD风格)
<string.h> memset、memcpy、memcmp(ANSI C风格)

十三、本章小结

13.1 核心知识点回顾

知识点 关键要点
套接字地址结构 sockaddr_in用于IPv4,字段必须以网络字节序存储,传递时强制转换为(struct sockaddr*)
值-结果参数 进程→内核用值传递,内核→进程用指针传递,以处理可变长度地址结构
字节序 网络字节序为大端,使用htons/htonl/ntohs/ntohl进行转换
字节操纵 bzero/memset清零地址结构,bcopy/memcpy复制
地址转换 推荐使用协议无关的inet_pton/inet_ntop,旧函数已废弃
读写函数 readn/writen确保读写指定字节数,readline处理文本行

13.2 常见错误与注意事项

常见错误 正确做法
忘记转换字节序 所有sin_portsin_addr必须使用字节序转换函数
忘记类型强制转换 传递地址结构时务必转换为(struct sockaddr*)
假设一次read读完数据 使用readn或在循环中调用read
忽略EINTR错误 被信号中断的系统调用应该重试
使用废弃的inet_addr 改用inet_pton

13.3 第三章思维导图

第三章 套接字编程简介
├── 套接字地址结构
│   ├── IPv4: sockaddr_in
│   │   ├── sin_family (AF_INET)
│   │   ├── sin_port (网络字节序)
│   │   └── sin_addr (网络字节序)
│   ├── IPv6: sockaddr_in6
│   ├── 通用: sockaddr
│   └── 新通用: sockaddr_storage
├── 值-结果参数
│   ├── 进程→内核: bind, connect, sendto
│   └── 内核→进程: accept, recvfrom, getsockname, getpeername
├── 字节序
│   ├── 大端 vs 小端
│   ├── htons/htonl (主机→网络)
│   └── ntohs/ntohl (网络→主机)
├── 地址转换
│   ├── 旧式: inet_aton, inet_ntoa (仅IPv4, 不可重入)
│   └── 新式: inet_pton, inet_ntop (协议无关, 可重入)
└── 包裹函数
    ├── readn / writen (确保读写n字节)
    └── readline (读取一行文本)

十四、下一章预告

📌 下一篇:《UNIX网络编程》读书笔记(四):第四章 基本TCP套接字编程

第四章将详细讲解:

  1. 核心套接字函数详解

    • socket函数:创建套接字描述符
    • connect函数:客户端连接服务器(含三次握手的阻塞行为)
    • bind函数:服务器绑定地址,INADDR_ANY的含义
    • listen函数:转换为被动监听套接字,backlog参数详解
    • accept函数:返回已连接套接字
  2. TCP客户/服务器完整流程:从socket到close的完整生命周期

  3. 并发服务器模型初探:使用fork处理多个客户端

  4. 完整回射服务器代码:逐行解读str_echostr_cli函数

  5. 常见错误与调试技巧ECONNREFUSEDETIMEDOUT

学习目标:学完第四章后,你将能够——

  • 完整实现一个TCP回射客户/服务器程序
  • 理解fork并发模型的基本原理
  • 处理常见的TCP网络编程错误

敬请期待!


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐