《UNIX 网络编程-卷1》阅读笔记04: 套接字编程简介
第三章真正开始进入套接字API的世界。前一章讲的是TCP、UDP、SCTP这些传输层协议的理论知识,而从本章开始,要学习如何使用套接字来实际编写网络程序。本章不涉及具体的客户/服务器完整实现(那是第四章的内容),而是专注于介绍套接字编程的基础构件——地址结构、字节序转换、地址格式转换等基础函数。如果本章的概念没有吃透,后续的编程将寸步难行。所有套接字函数都依赖于对地址结构和字节序的正确理解。当向套
作者: 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_aton、inet_addr和inet_ntoa函数 |
IPv4专用的地址转换函数(过时) |
| 3.7 | inet_pton和inet_ntop函数 |
协议无关的地址转换函数(推荐) |
| 3.8 | sock_ntop和相关函数 |
作者封装的协议无关地址转换函数 |
| 3.9 | readn、writen和readline函数 |
处理不足字节计数的包裹函数 |
💡 本章核心价值:读完第三章,你应该能够——
- 理解套接字地址结构的每个字段的含义
- 正确使用字节序转换函数
- 使用协议无关的
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_family、sin_port和sin_addr三个字段。 |
sin_port |
in_port_t |
16位端口号,必须以网络字节序存储 |
sin_addr |
struct in_addr |
32位IPv4地址,必须以网络字节序存储 |
sin_zero |
char[8] |
填充字段,使结构体大小与struct sockaddr兼容,通常用bzero或memset置0 |
💡 关键理解:套接字地址结构仅在给定主机上使用——虽然结构中某些字段(如IP地址和端口号)用在不同主机之间通信中,但是结构本身并不在主机之间传递。
2.2 通用套接字地址结构
为什么需要通用地址结构?因为套接字函数需要支持多种协议族(IPv4、IPv6、Unix域等)。bind、connect、accept等函数的参数是指向特定协议地址结构的指针,为了能够处理所有协议族,这些函数被设计为接受一个指向通用套接字地址结构的指针。
#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_namelen和msg_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_addr和sin_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
参数说明:
family:AF_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 为什么需要这些函数?
字节流套接字上调用read或write时,读取或写入的字节数可能比请求的数量少。这不是错误,而是因为内核中用于套接字的缓冲区可能已达到了极限(例如接收缓冲区没有足够数据或发送缓冲区没有足够空间)。
常见场景:
| 场景 | 说明 |
|---|---|
read返回比请求少 |
接收缓冲区数据不足,只返回当前可用数据 |
write写入比请求少 |
发送缓冲区空间不足,只写入部分数据 |
read返回0 |
对端关闭连接 |
💡 关键理解:普通的
read和write是“尽力而为”的。它们不保证一次调用就能传输完请求的所有字节。readn和writen函数通过循环调用,确保传输完指定数量的字节。
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;
}
⚠️ 关于EINTR:
read或write系统调用在执行过程中被捕获的信号中断时会产生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_pton和inet_ntop时使用void*参数,而其他地址结构使用struct sockaddr*?
答案: void*只能用来传递参数,不能对void类型的指针进行加减和解引用操作。在inet_pton和inet_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_port和sin_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套接字编程
第四章将详细讲解:
-
核心套接字函数详解:
socket函数:创建套接字描述符connect函数:客户端连接服务器(含三次握手的阻塞行为)bind函数:服务器绑定地址,INADDR_ANY的含义listen函数:转换为被动监听套接字,backlog参数详解accept函数:返回已连接套接字
-
TCP客户/服务器完整流程:从socket到close的完整生命周期
-
并发服务器模型初探:使用
fork处理多个客户端 -
完整回射服务器代码:逐行解读
str_echo和str_cli函数 -
常见错误与调试技巧:
ECONNREFUSED、ETIMEDOUT等
学习目标:学完第四章后,你将能够——
- 完整实现一个TCP回射客户/服务器程序
- 理解
fork并发模型的基本原理 - 处理常见的TCP网络编程错误
敬请期待!
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)