1. 引言
Linux 网络编程是后端开发、嵌入式开发和系统编程的核心技能之一。本文面向有一定 C 语言基础、希望系统掌握 Linux 网络编程的读者,从最基本的 socket 接口函数讲起,逐步深入到 TCP/UDP 服务端与客户端实战、UNIX 域协议、套接字选项以及广播与组播编程。全文以函数原型、参数说明、返回值分析和可运行示例为主线,帮助读者建立完整的网络编程知识体系。
2. 基础函数
Socket(套接字)是 Linux 网络编程的基石,它是对 TCP/IP 协议栈的封装,为应用程序提供了一套统一的网络通信接口。下面从最核心的 socket 函数开始讲解。
2.1 socket 函数
函数原型:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);参数说明:
- domain:协议族,常用取值包括
AF_INET(IPv4 网络协议)、AF_INET6(IPv6)、AF_UNIX(UNIX 域套接字)。 - type:套接字类型,常用取值包括
SOCK_STREAM(流式套接字,面向连接,对应 TCP)、SOCK_DGRAM(数据报套接字,无连接,对应 UDP)、SOCK_RAW(原始套接字)。 - protocol:协议号,通常传 0 表示使用默认协议。也可以指定
IPPROTO_TCP或IPPROTO_UDP。
返回值:成功返回一个非负的文件描述符(套接字描述符);失败返回 -1,并设置 errno。
示例:
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket"); return 1; } printf("socket created, fd = %d\n", sockfd); return 0; }2.2 网络地址结构体
在 Linux 网络编程中,地址结构体用于描述通信双方的地址信息。最常用的是 IPv4 地址结构体sockaddr_in,其定义如下:
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; /* 地址族,AF_INET */ in_port_t sin_port; /* 端口号,网络字节序 */ struct in_addr sin_addr; /* IPv4 地址 */ char sin_zero[8]; /* 填充字段,保持与 sockaddr 大小一致 */ };其中struct in_addr的定义为:
struct in_addr { in_addr_t s_addr; /* 32 位 IPv4 地址,网络字节序 */ };通用地址结构体sockaddr用于函数参数的统一类型:
struct sockaddr { sa_family_t sa_family; /* 地址族 */ char sa_data[14]; /* 协议地址 */ };在实际编程中,通常先填充sockaddr_in,再强制转换为struct sockaddr *传入 bind、connect 等函数。
2.3 IPv4 地址转换函数
IPv4 地址在程序中通常以点分十进制字符串(如 "192.168.1.1")或 32 位二进制整数两种形式存在。地址转换函数用于在这两种形式之间转换。
inet_pton 函数:将点分十进制字符串转换为网络字节序的二进制地址。
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst);- af:地址族,通常为
AF_INET。 - src:点分十进制字符串,如 "192.168.1.1"。
- dst:指向存放转换结果的
struct in_addr的指针。
返回值:成功返回 1;src 不是有效地址返回 0;af 不支持返回 -1。
inet_ntop 函数:将网络字节序的二进制地址转换为点分十进制字符串。
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);- af:地址族,通常为
AF_INET。 - src:指向
struct in_addr的指针。 - dst:存放转换结果的字符串缓冲区。
- size:缓冲区大小,IPv4 地址至少需要
INET_ADDRSTRLEN(16 字节)。
返回值:成功返回指向 dst 的指针;失败返回 NULL。
示例:
#include <stdio.h> #include <arpa/inet.h> int main() { struct in_addr addr; char buf[INET_ADDRSTRLEN]; // 字符串转二进制 if (inet_pton(AF_INET, "192.168.1.100", &addr) != 1) { perror("inet_pton"); return 1; } // 二进制转字符串 if (inet_ntop(AF_INET, &addr, buf, sizeof(buf)) == NULL) { perror("inet_ntop"); return 1; } printf("address: %s\n", buf); return 0; }inet_aton 函数:将点分十进制字符串转换为网络字节序的二进制地址,是早期常用的转换函数。
#include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp);- cp:点分十进制字符串,如 "192.168.1.1"。
- inp:指向存放转换结果的
struct in_addr的指针。
返回值:成功返回非 0;失败返回 0。
inet_ntoa 函数:将网络字节序的二进制地址转换为点分十进制字符串,返回的字符串存放在静态缓冲区中。
#include <arpa/inet.h> char *inet_ntoa(struct in_addr in);- in:
struct in_addr结构体,包含 32 位网络字节序地址。
返回值:成功返回指向点分十进制字符串的指针;失败返回 NULL。注意该函数返回的字符串存放在静态缓冲区,多次调用会覆盖之前的结果。
inet_addr 函数:将点分十进制字符串转换为网络字节序的 32 位二进制地址,已废弃,建议使用 inet_pton 替代。
#include <arpa/inet.h> in_addr_t inet_addr(const char *cp);- cp:点分十进制字符串,如 "192.168.1.1"。
返回值:成功返回网络字节序的 32 位地址;失败返回INADDR_NONE(即 0xFFFFFFFF)。
inet_network 函数:将点分十进制字符串转换为主机字节序的 32 位地址,不包含网络字节序转换。
#include <arpa/inet.h> in_addr_t inet_network(const char *cp);- cp:点分十进制字符串,如 "192.168.1.1"。
返回值:成功返回主机字节序的 32 位地址;失败返回INADDR_NONE。
示例:
#include <stdio.h> #include <arpa/inet.h> int main() { struct in_addr addr; char buf[INET_ADDRSTRLEN]; // inet_aton:字符串转二进制 if (inet_aton("192.168.1.100", &addr) == 0) { perror("inet_aton"); return 1; } printf("inet_aton: %s\n", inet_ntoa(addr)); // inet_ntoa:二进制转字符串 printf("inet_ntoa: %s\n", inet_ntoa(addr)); // inet_addr:字符串转网络字节序整数 in_addr_t net_addr = inet_addr("192.168.1.100"); if (net_addr == INADDR_NONE) { perror("inet_addr"); return 1; } printf("inet_addr: 0x%x\n", net_addr); // inet_network:字符串转主机字节序整数 in_addr_t host_addr = inet_network("192.168.1.100"); if (host_addr == INADDR_NONE) { perror("inet_network"); return 1; } printf("inet_network: 0x%x\n", host_addr); return 0; }2.4 网络字节序函数
不同主机对多字节整数的存储方式不同(大端或小端),为保证网络通信的一致性,TCP/IP 协议规定使用大端字节序(网络字节序)。Linux 提供了一组字节序转换函数:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); /* 主机字节序转网络字节序(32 位) */ uint16_t htons(uint16_t hostshort); /* 主机字节序转网络字节序(16 位) */ uint32_t ntohl(uint32_t netlong); /* 网络字节序转主机字节序(32 位) */ uint16_t ntohs(uint16_t netshort); /* 网络字节序转主机字节序(16 位) */参数说明:传入待转换的数值,返回转换后的数值。
示例:
#include <stdio.h> #include <arpa/inet.h> int main() { uint16_t port = 8080; uint16_t net_port = htons(port); printf("host port: %u, network port: %u\n", port, net_port); printf("back to host: %u\n", ntohs(net_port)); return 0; }3. 核心函数
在 socket 创建完成后,服务端和客户端需要调用一系列函数建立连接并传输数据。本节详细讲解 bind、listen、accept、connect 四个核心函数。
3.1 bind
函数原型:
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明:
- sockfd:socket 函数返回的套接字描述符。
- addr:指向地址结构体的指针,通常为
struct sockaddr_in *强制转换而来。 - addrlen:地址结构体的大小,可用
sizeof(struct sockaddr_in)。
返回值:成功返回 0;失败返回 -1,并设置 errno。
示例:
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket"); return 1; } struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 绑定所有本地地址 */ if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_a