我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 socket 函数,它是网络编程的基础,用于创建一个套接字 (socket),这是进程间通过网络进行通信的端点。
1. 函数介绍
socket 是一个 Linux 系统调用,用于创建一个新的**套接字 **(socket)。套接字是一种抽象的概念,它是网络通信的基础端点。你可以把套接字想象成电话的听筒:
socket 函数本身并不执行网络连接,它只是创建一个通信的“插头”或“接口”,后续需要使用 bind, listen, accept, connect, send, recv 等函数来完成具体的网络操作。
2. 函数原型
1 2 3 4 5
| #include <sys/socket.h> // 必需 #include <sys/types.h> // 有时需要
int socket(int domain, int type, int protocol);
|
3. 功能
创建套接字: 请求内核创建一个新的套接字对象。
指定通信特性: 通过参数定义套接字的**通信域 **(domain)、**类型 (type) 和协议 **(protocol),从而确定套接字的行为和能力。
返回文件描述符: 如果成功,返回一个与新创建的套接字关联的**文件描述符 **(file descriptor)。后续所有对该套接字的操作(如 bind, connect, read, write)都将使用这个文件描述符。
4. 参数
int domain: 指定套接字的通信域,即套接字可以通信的范围。
AF_INET: IPv4 Internet 协议域。这是最常用的域,用于通过 IPv4 网络进行通信。
AF_INET6: IPv6 Internet 协议域。用于通过 IPv6 网络进行通信。
AF_UNIX 或 AF_LOCAL: Unix 域套接字。用于同一台机器上进程间的本地通信,不涉及网络协议栈,非常高效。
AF_PACKET: 用于直接访问网络接口(数据链路层)。
AF_NETLINK: 用于与内核进行通信。
int type: 指定套接字的通信语义或类型。
SOCK_STREAM: 流式套接字。提供面向连接、可靠、有序的双向数据传输。TCP 协议就是基于流式套接字的。数据像水流一样,没有边界。
SOCK_DGRAM: 数据报套接字。提供无连接、不可靠(可能丢包、重复、乱序)、有边界的数据传输。UDP 协议就是基于数据报套接字的。数据以一个独立的“包裹”(数据报)形式发送。
SOCK_RAW: 原始套接字。允许直接访问底层协议(如 IP 或 ICMP)。通常需要 root 权限。
SOCK_SEQPACKET: 有序数据包套接字。提供面向连接、可靠、有边界且有序的数据传输(像 TCP 一样可靠有序,但像 UDP 一样有消息边界)。
修饰符 (可以按位或 | 到 type 上):
SOCK_NONBLOCK: 将套接字设置为非阻塞模式。等同于创建套接字后调用 fcntl(sock, F_SETFL, O_NONBLOCK)。
SOCK_CLOEXEC: 在调用 exec() 时自动关闭该套接字。等同于创建后调用 fcntl(sock, F_SETFD, FD_CLOEXEC)。这可以防止将套接字意外传递给新执行的程序。
int protocol: 指定在给定域和类型下使用的具体协议。
在大多数情况下,对于 AF_INET 和 AF_INET6:
通常设置为 0,表示使用给定 domain 和 type 的默认协议。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
对于原始套接字 (SOCK_RAW),需要显式指定协议,如 IPPROTO_ICMP, IPPROTO_RAW 等。
5. 返回值
6. 相似函数,或关联函数
bind: 将套接字与一个本地地址(IP 地址和端口号)关联起来。
listen: 使套接字进入监听状态,准备接收来自客户端的连接请求(用于服务器)。
accept: 从监听套接字的连接队列中提取第一个未决连接,创建一个新的套接字用于与该客户端通信(用于服务器)。
connect: 主动向服务器发起连接请求(用于客户端)。
close: 关闭套接字文件描述符,释放相关资源。
read / write / send / recv: 通过套接字发送和接收数据。
getaddrinfo: 现代的、线程安全的地址解析函数,用于将主机名和服务名转换为套接字地址结构。
7. 示例代码
示例 1:创建不同类型的套接字
这个例子演示了如何创建几种常见的套接字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #include <sys/socket.h> // socket #include <stdio.h> // perror, printf #include <stdlib.h> // exit
int main() { int sockfd;
// 1. 创建一个 IPv4 的 TCP 流式套接字 (最常用) printf("Creating AF_INET, SOCK_STREAM socket...\n"); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket AF_INET SOCK_STREAM"); // 不 exit,继续演示其他类型 } else { printf("Success! Socket file descriptor: %d\n", sockfd); close(sockfd); // 创建后立即关闭,仅作演示 }
// 2. 创建一个 IPv4 的 UDP 数据报套接字 printf("\nCreating AF_INET, SOCK_DGRAM socket...\n"); sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket AF_INET SOCK_DGRAM"); } else { printf("Success! Socket file descriptor: %d\n", sockfd); close(sockfd); }
// 3. 创建一个 Unix 域流式套接字 printf("\nCreating AF_UNIX, SOCK_STREAM socket...\n"); sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket AF_UNIX SOCK_STREAM"); } else { printf("Success! Socket file descriptor: %d\n", sockfd); close(sockfd); }
// 4. 创建一个非阻塞的 TCP 套接字 (使用 SOCK_NONBLOCK 修饰符) printf("\nCreating non-blocking AF_INET, SOCK_STREAM socket...\n"); sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (sockfd == -1) { perror("socket AF_INET SOCK_STREAM | SOCK_NONBLOCK"); } else { printf("Success! Non-blocking socket file descriptor: %d\n", sockfd); // 检查是否真的非阻塞 (可选) // int flags = fcntl(sockfd, F_GETFL, 0); // if (flags & O_NONBLOCK) printf("Confirmed: socket is non-blocking.\n"); close(sockfd); }
// 5. 尝试创建一个无效的套接字组合 (例如,原始套接字需要权限) printf("\nTrying to create a raw socket (may fail without root)...\n"); sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd == -1) { perror("socket AF_INET SOCK_RAW IPPROTO_ICMP (expected to fail without root)"); } else { printf("Success! Raw socket file descriptor: %d\n", sockfd); close(sockfd); }
printf("\nSocket creation examples completed.\n"); return 0; }
|
代码解释:
演示了创建四种不同类型的套接字:
IPv4 TCP 流式套接字 (AF_INET, SOCK_STREAM):这是网络编程中最常见的类型,用于可靠的、面向连接的通信(如 HTTP)。
IPv4 UDP 数据报套接字 (AF_INET, SOCK_DGRAM):用于无连接的、不可靠但快速的通信(如 DNS 查询)。
Unix 域流式套接字 (AF_UNIX, SOCK_STREAM):用于同一主机上进程间的高效通信。
非阻塞 TCP 套接字 (AF_INET, SOCK_STREAM | SOCK_NONBLOCK):使用 SOCK_NONBLOCK 修饰符创建,避免后续 I/O 操作阻塞。
每次调用 socket 后都检查返回值。
如果成功,打印返回的文件描述符,并立即调用 close 关闭它(因为这只是演示创建)。
最后尝试创建一个需要 root 权限的原始套接字 (SOCK_RAW),在普通用户权限下会失败。
示例 2:简单的 TCP 服务器套接字设置
这个例子演示了服务器端如何创建、绑定、监听一个 TCP 套接字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| #include <sys/socket.h> // socket, bind, listen #include <netinet/in.h> // sockaddr_in #include <arpa/inet.h> // inet_addr (虽然此例未用,但常与网络编程相关) #include <unistd.h> // close #include <stdio.h> // perror, printf #include <stdlib.h> // exit #include <string.h> // memset
#define PORT 8080 #define BACKLOG 10 // 等待连接队列的最大长度
int main() { int server_fd; struct sockaddr_in address; int opt = 1; // 用于 setsockopt
// 1. 创建套接字 printf("Creating server socket...\n"); server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket creation failed"); exit(EXIT_FAILURE); } printf("Socket created successfully. File descriptor: %d\n", server_fd);
// 2. (可选但推荐) 设置套接字选项 // SO_REUSEADDR: 允许套接字绑定到处于 TIME_WAIT 状态的地址 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt SO_REUSEADDR failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Socket option SO_REUSEADDR set.\n");
// 3. 配置服务器地址结构 memset(&address, 0, sizeof(address)); // 清零结构体 address.sin_family = AF_INET; // IPv4 address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地接口 (0.0.0.0) address.sin_port = htons(PORT); // 端口号,从主机字节序转换为网络字节序
// 4. 将套接字绑定到地址和端口 printf("Binding socket to port %d...\n", PORT); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Socket bound successfully.\n");
// 5. 让套接字进入监听状态 printf("Putting socket into listening mode (backlog: %d)...\n", BACKLOG); if (listen(server_fd, BACKLOG) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server is now listening for incoming connections.\n");
printf("\nServer setup complete. Waiting for connections...\n"); printf("(Run a client to connect, e.g., 'telnet localhost %d' or 'nc localhost %d')\n", PORT, PORT);
// --- 服务器已准备好,可以调用 accept() 来接受连接 --- // 这里为了演示 socket, bind, listen,暂时不实现 accept 循环
// (在实际服务器中,这里会有一个循环调用 accept, fork/handle, close client_sock)
// 按 Ctrl+C 退出程序 pause(); // 永久挂起,直到收到信号
// 6. 关闭套接字 (在实际程序中,这会在适当的地方调用) close(server_fd); printf("Server socket closed.\n");
return 0; }
|
代码解释:
调用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。
(重要) 调用 setsockopt 设置 SO_REUSEADDR 选项。这允许服务器在重启时立即绑定到同一个地址,即使之前的连接可能处于 TIME_WAIT 状态。这是一个很好的实践。
初始化 sockaddr_in 结构体 address 来指定服务器的地址和端口:
sin_family = AF_INET:指定 IPv4。
sin_addr.s_addr = INADDR_ANY:绑定到所有可用的网络接口(服务器可能有多个网卡)。如果只想绑定到特定 IP,可以使用 inet_addr(“192.168.1.100”) 之类的函数。
sin_port = htons(PORT):设置端口号。重要:使用 htons() (host to network short) 将主机字节序的端口号转换为网络字节序。网络协议要求使用大端字节序。
调用 bind(server_fd, …) 将套接字与指定的地址和端口绑定。
调用 listen(server_fd, BACKLOG) 使套接字进入监听模式。BACKLOG 参数指定了内核为此套接字维护的未完成连接队列的最大长度。
此时,服务器已准备好接收客户端连接。后续需要在一个循环中调用 accept() 来处理连接。
为了演示,程序调用 pause() 挂起,等待用户按 Ctrl+C 退出。
程序退出前关闭服务器套接字。
编译和测试:
1 2 3 4 5 6 7
| gcc -o tcp_server tcp_server.c ./tcp_server # 在另一个终端: # telnet localhost 8080 # 或者 # nc localhost 8080
|
示例 3:简单的 TCP 客户端套接字设置
这个例子演示了客户端如何创建套接字并连接到服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // inet_addr, inet_ntoa #include <unistd.h> // close, read, write #include <stdio.h> // perror, printf #include <stdlib.h> // exit #include <string.h> // strlen, memset
#define PORT 8080 #define SERVER_IP "127.0.0.1" // 本地回环地址
int main() { int sock = 0; struct sockaddr_in serv_addr; char *hello = "Hello from client"; char buffer[1024] = {0};
// 1. 创建套接字 printf("Creating client socket...\n"); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket creation error"); exit(EXIT_FAILURE); } printf("Client socket created successfully. File descriptor: %d\n", sock);
// 2. 配置服务器地址结构 memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT);
// 将服务器 IP 地址从文本转换为二进制 // inet_addr 已过时,推荐使用 inet_pton // if (inet_addr(SERVER_IP) == INADDR_NONE) { ... handle error ... } if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) { perror("inet_pton error or invalid address"); close(sock); exit(EXIT_FAILURE); }
// 3. 连接到服务器 printf("Connecting to server %s:%d...\n", SERVER_IP, PORT); if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("connection failed"); close(sock); exit(EXIT_FAILURE); } printf("Connected to server successfully.\n");
// 4. 发送数据到服务器 printf("Sending message to server: %s\n", hello); if (send(sock, hello, strlen(hello), 0) != (int)strlen(hello)) { perror("send failed"); close(sock); exit(EXIT_FAILURE); } printf("Message sent.\n");
// 5. 读取服务器的响应 printf("Reading response from server...\n"); int valread = read(sock, buffer, 1024); if (valread > 0) { printf("Received from server: %s\n", buffer); } else if (valread == 0) { printf("Server closed the connection.\n"); } else { perror("read failed"); }
// 6. 关闭套接字 close(sock); printf("Client socket closed.\n");
return 0; }
|
代码解释:
调用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。
初始化 sockaddr_in 结构体 serv_addr 来指定服务器的地址和端口。
调用 connect(sock, …) 主动向服务器发起连接请求。这个调用会阻塞,直到连接建立或失败。
连接成功后,使用 send() (或 write()) 向服务器发送数据。
使用 read() (或 recv()) 从服务器读取响应数据。
通信结束后,调用 close() 关闭套接字。
重要提示与注意事项:
字节序: 网络协议规定使用大端字节序(Big-Endian)。主机字节序可能是大端或小端。使用 htons (host to network short), htonl (host to network long), ntohs, ntohl 进行转换。
错误处理: 始终检查 socket 及后续网络函数的返回值。
资源管理: 使用完套接字后,务必调用 close() 关闭它,以释放文件描述符和内核资源。
阻塞与非阻塞: 默认情况下,套接字是阻塞的。connect, read, write 等操作可能会无限期挂起。可以使用 SOCK_NONBLOCK 创建非阻塞套接字,或用 fcntl 修改现有套接字的标志。
bind 对于客户端?: 客户端通常不需要显式调用 bind。操作系统会自动为客户端套接字分配一个临时的端口号(ephemeral port)。
getaddrinfo: 对于需要处理 IPv4/IPv6 透明性或域名解析的现代程序,推荐使用 getaddrinfo() 来获取地址信息,而不是手动填充 sockaddr_in。
总结:
socket 函数是网络编程的起点,它创建了通信的端点。理解其参数(域、类型、协议)对于选择正确的通信方式至关重要。它是构建客户端和服务器应用程序的基础。