跳转到内容

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);
结构体 适用版本 主要用途
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_INET
    • sin_port:端口号,必须使用 htons() 转换为网络字节序
    • sin_addr:struct in_addr,内部包含一个 32 位的 IP 地址(s_addr)
    • sin_zero:填充字段,必须用 memset() 置零

struct sockaddr_in6

  • 设计目的:专门为 IPv6 设计的地址结构体,可安全地强制转换为 struct sockaddr
  • 主要字段:
    • sin6_family:设为 AF_INET6
    • sin6_port:端口号,必须使用 htons() 转换为网络字节序
    • sin6_addr:struct in6_addr,内部包含一个 16 字节(128 位)的地址数组

struct sockaddr_storage

  • 设计目的:当不知道即将到来的连接是 IPv4 还是 IPv6 时使用,空间足够大,可容纳任何一种结构体
  • 典型用法:在调用 accept() 或 recvfrom() 时传入,通过检查 ss_family 字段判断实际地址族,然后强制转换为 sockaddr_in 或 sockaddr_in6 提取数据
角色 典型调用顺序
流式服务器(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

准备阶段:配置与初始化

  1. getaddrinfo():进行域名(或 IP)和服务的查找,返回一个包含地址信息的链表

    • node:主机名或 IP 地址(使用主机名时内核会启动 DNS 查找)
    • service:端口号或服务名(如 “http”,内核会查阅 /etc/services 映射为端口号)
    • hints:addrinfo 结构体,用于筛选返回结果
    • 注意:必须调用 freeaddrinfo() 释放分配的链表空间
  2. socket():创建一个套接字端点,返回一个文件描述符

    • domain:协议族(如 PF_INET 或 PF_INET6)
    • type:套接字类型(SOCK_STREAM 或 SOCK_DGRAM)
    • protocol:通常设为 0 以自动匹配类型
    • 推荐直接使用 getaddrinfo() 返回的结果来填充参数

建立阶段:绑定与连接

  1. bind():将套接字与本地机器的某个 IP 和端口关联。这通常是服务器程序必须做的第一步,以便内核将入站包匹配到特定进程

    • 如果重启服务器时报错 “Address already in use”,可使用 setsockopt() 设置 SO_REUSEADDR
  2. connect():客户端通过此调用连接到远程服务器。如果客户端没有先调用 bind(),内核会自动选择未使用的本地端口

  3. listen():告诉内核该套接字将用于接收连接

    • backlog:指定入站连接队列的长度
  4. accept():从监听队列中提取第一个挂起的连接,返回一个全新的文件描述符用于通信,原始的监听套接字继续保持监听状态

通信阶段:数据交换

  1. send() / recv()(TCP)

    • send() 返回实际发送的字节数,可能小于请求发送的长度,需自行处理剩余数据
    • recv() 返回读取的字节数,返回 0 表示远程端已关闭连接
  2. sendto() / recvfrom()(UDP)

    • 由于 UDP 是无连接的,每次发送数据都必须携带目的地址信息
    • 如果对 UDP 套接字调用了 connect(),也可以转用 send() 和 recv()

退出阶段:清理与识别

  1. close() / shutdown()

    • close():防止进一步的读写并释放文件描述符
    • shutdown():允许只切断“发送”或“接收”方向的通信,而不立刻释放描述符
  2. 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;
}