Linux 网络编程
Linux 网络编程是通过套接字(Socket)API 实现进程间网络通信的编程方式。在 Linux 中一切皆文件,套接字也是一种使用标准文件描述符与其他程序通信的方式。掌握网络编程是理解客户端/服务器架构、协议栈工作原理的基础
- 套接字:网络通信的端点,本质上是一个文件描述符。
- 数据封装:数据从应用层向下传递时,会被逐层包裹头部(如 TFTP [\to] UDP [\to] IP [\to] Ethernet),接收端则逐层拆包
- 流式套接字:基于 TCP 协议,提供可靠、双向、按序且无差错的数据传输
- 数据报套接字:基于 UDP 协议,无连接。数据可能失序或丢失,但如果到达则是无差错的
当把一个两字节的十六进制数(如 0xb34f)存入内存时:
- 大端序:按顺序存储为
b3然后是4f,即高位存入低地址。这被规定为网络字节序 - 小端序:反向存储为
4f然后是b3,即低位存入低地址。这是大多数 Intel 处理器的存储方式,称为主机字节序
在数据发出前需转成网络序,在数据收到后需转成主机序。Linux 提供了转换函数:
| 函数 | 全称 | 作用 |
|---|---|---|
htons() |
Host to Network Short | 将端口号(2 字节)从主机序转为网络序 |
htonl() |
Host to Network Long | 将 IPv4 地址(4 字节)从主机序转为网络序 |
ntohs() |
Network to Host Short | 将收到的端口号转回主机序 |
ntohl() |
Network to Host Long | 将收到的 IPv4 地址转回主机序 |
人的视角习惯看“点分十进制”字符串(如 192.0.2.1)或 IPv6 的“十六进制冒号”字符串,但内核需要的是二进制格式,因此需要使用支持 IPv4 和 IPv6 的现代函数进行地址转换。
inet_pton() – 将 IP 字符串转换为二进制结构体:
struct sockaddr_in sa;inet_pton(AF_INET, "192.0.2.1", &(sa.sin_addr));inet_ntop() – 将二进制结构体转换为可读字符串:
char ip4[INET_ADDRSTRLEN];struct sockaddr_in sa;inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);关键数据结构
Section titled “关键数据结构”| 结构体 | 适用版本 | 主要用途 |
|---|---|---|
addrinfo |
通用 | 调用 getaddrinfo 时使用,用于管理地址链表 |
sockaddr |
通用 | 系统调用函数的通用参数模板 |
sockaddr_in |
IPv4 | 存储 IPv4 端口和地址 |
sockaddr_in6 |
IPv6 | 存储 IPv6 端口和地址 |
sockaddr_storage |
通用 | 作为足够大的缓冲区,接收未知的地址信息 |
struct addrinfo
- 设计目的:用于主机名/服务查找(DNS)及准备后续使用的地址结构
- 核心特点:它是一个链表结构,
getaddrinfo()可能返回多个结果供选择 - 主要字段:
ai_family:地址族(AF_INET/AF_INET6/AF_UNSPEC)ai_socktype:套接字类型(SOCK_STREAM/SOCK_DGRAM)ai_addr:指向struct sockaddr的指针ai_next:指向链表中下一个节点的指针
struct sockaddr
- 设计目的:作为通用模板,处理多种不同类型的套接字地址
- 核心特点:手动填充这个结构体非常麻烦(
sa_data包含 14 字节混合数据),通常填充具体的 IPv4/IPv6 结构体再进行强制类型转换 - 主要字段:
sa_family:地址族sa_data:包含目的地址和端口号的原始数据
struct sockaddr_in
- 设计目的:专门为 IPv4 设计的地址结构体,可安全地强制转换为
struct sockaddr - 主要字段:
sin_family:必须设为AF_INETsin_port:端口号,必须使用htons()转换为网络字节序sin_addr:struct in_addr,内部包含一个 32 位的 IP 地址(s_addr)sin_zero:填充字段,必须用memset()置零
struct sockaddr_in6
- 设计目的:专门为 IPv6 设计的地址结构体,可安全地强制转换为
struct sockaddr - 主要字段:
sin6_family:设为AF_INET6sin6_port:端口号,必须使用htons()转换为网络字节序sin6_addr:struct in6_addr,内部包含一个 16 字节(128 位)的地址数组
struct sockaddr_storage
- 设计目的:当不知道即将到来的连接是 IPv4 还是 IPv6 时使用,空间足够大,可容纳任何一种结构体
- 典型用法:在调用
accept()或recvfrom()时传入,通过检查ss_family字段判断实际地址族,然后强制转换为sockaddr_in或sockaddr_in6提取数据
核心系统调用
Section titled “核心系统调用”| 角色 | 典型调用顺序 |
|---|---|
| 流式服务器(TCP) | getaddrinfo -> socket -> bind -> listen -> accept -> recv/send -> close |
| 流式客户端(TCP) | getaddrinfo -> socket -> connect -> send/recv -> close |
| 数据报服务器(UDP) | getaddrinfo -> socket -> bind -> recvfrom/sendto -> close |
| 数据报客户端(UDP) | getaddrinfo -> socket -> sendto/recvfrom -> close |
准备阶段:配置与初始化
-
getaddrinfo():进行域名(或 IP)和服务的查找,返回一个包含地址信息的链表node:主机名或 IP 地址(使用主机名时内核会启动 DNS 查找)service:端口号或服务名(如 “http”,内核会查阅/etc/services映射为端口号)hints:addrinfo结构体,用于筛选返回结果- 注意:必须调用
freeaddrinfo()释放分配的链表空间
-
socket():创建一个套接字端点,返回一个文件描述符domain:协议族(如PF_INET或PF_INET6)type:套接字类型(SOCK_STREAM或SOCK_DGRAM)protocol:通常设为0以自动匹配类型- 推荐直接使用
getaddrinfo()返回的结果来填充参数
建立阶段:绑定与连接
-
bind():将套接字与本地机器的某个 IP 和端口关联。这通常是服务器程序必须做的第一步,以便内核将入站包匹配到特定进程- 如果重启服务器时报错 “Address already in use”,可使用
setsockopt()设置SO_REUSEADDR
- 如果重启服务器时报错 “Address already in use”,可使用
-
connect():客户端通过此调用连接到远程服务器。如果客户端没有先调用bind(),内核会自动选择未使用的本地端口 -
listen():告诉内核该套接字将用于接收连接backlog:指定入站连接队列的长度
-
accept():从监听队列中提取第一个挂起的连接,返回一个全新的文件描述符用于通信,原始的监听套接字继续保持监听状态
通信阶段:数据交换
-
send()/recv()(TCP)send()返回实际发送的字节数,可能小于请求发送的长度,需自行处理剩余数据recv()返回读取的字节数,返回 0 表示远程端已关闭连接
-
sendto()/recvfrom()(UDP)- 由于 UDP 是无连接的,每次发送数据都必须携带目的地址信息
- 如果对 UDP 套接字调用了
connect(),也可以转用send()和recv()
退出阶段:清理与识别
-
close()/shutdown()close():防止进一步的读写并释放文件描述符shutdown():允许只切断“发送”或“接收”方向的通信,而不立刻释放描述符
-
getpeername():获取连接另一端的 IP 地址和端口号
#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netdb.h>#include <arpa/inet.h>#include <netinet/in.h>
// 使用 getaddrinfo 解析地址int main(void) { struct addrinfo hints, *res, *p; int status; char ipstr[INET6_ADDRSTRLEN];
memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // IPv4 或 IPv6 均可 hints.ai_socktype = SOCK_STREAM; // TCP
if ((status = getaddrinfo("www.example.com", "80", &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); return 1; }
for (p = res; p != NULL; p = p->ai_next) { void *addr; if (p->ai_family == AF_INET) { struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); } else { struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); } inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); printf(" %s\n", ipstr); }
freeaddrinfo(res); return 0;}