我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 socket
函数,它是网络编程的基础,用于创建一个套接字 (socket),这是进程间通过网络进行通信的端点。
1. 函数介绍
socket
是一个 Linux 系统调用,用于创建一个新的**套接字 **(socket)。套接字是一种抽象的概念,它是网络通信的基础端点。你可以把套接字想象成电话的听筒:
- 你需要先有一个听筒(调用
socket
创建)。 - 然后你可以用它来拨打电话(
connect
,作为客户端)或接听电话(bind
+listen
+accept
,作为服务器)。 - 通过这个听筒,你可以说话(
send
/write
)和听话(recv
/read
)。
socket
函数本身并不执行网络连接,它只是创建一个通信的“插头”或“接口”,后续需要使用 bind
, listen
, accept
, connect
, send
, recv
等函数来完成具体的网络操作。
2. 函数原型
#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
:- 如果
type
是SOCK_STREAM
,则protocol
通常为IPPROTO_TCP
(或 0,内核会选择默认协议 TCP)。 - 如果
type
是SOCK_DGRAM
,则protocol
通常为IPPROTO_UDP
(或 0,内核会选择默认协议 UDP)。
- 如果
- 通常设置为 0,表示使用给定
domain
和type
的默认协议。 - 对于原始套接字 (
SOCK_RAW
),需要显式指定协议,如IPPROTO_ICMP
,IPPROTO_RAW
等。
- 在大多数情况下,对于
5. 返回值
- 成功时: 返回一个非负整数,即新创建套接字的**文件描述符 **(file descriptor)。
- 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EACCES
权限不足,EAFNOSUPPORT
不支持的地址族,EINVAL
无效参数,EMFILE
进程打开的文件描述符已达上限,ENFILE
系统打开的文件总数已达上限,ENOMEM
内存不足等)。
6. 相似函数,或关联函数
bind
: 将套接字与一个本地地址(IP 地址和端口号)关联起来。listen
: 使套接字进入监听状态,准备接收来自客户端的连接请求(用于服务器)。accept
: 从监听套接字的连接队列中提取第一个未决连接,创建一个新的套接字用于与该客户端通信(用于服务器)。connect
: 主动向服务器发起连接请求(用于客户端)。close
: 关闭套接字文件描述符,释放相关资源。read
/write
/send
/recv
: 通过套接字发送和接收数据。getaddrinfo
: 现代的、线程安全的地址解析函数,用于将主机名和服务名转换为套接字地址结构。
7. 示例代码
示例 1:创建不同类型的套接字
这个例子演示了如何创建几种常见的套接字。
#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 操作阻塞。
- IPv4 TCP 流式套接字 (
- 每次调用
socket
后都检查返回值。 - 如果成功,打印返回的文件描述符,并立即调用
close
关闭它(因为这只是演示创建)。 - 最后尝试创建一个需要 root 权限的原始套接字 (
SOCK_RAW
),在普通用户权限下会失败。
示例 2:简单的 TCP 服务器套接字设置
这个例子演示了服务器端如何创建、绑定、监听一个 TCP 套接字。
#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 退出。 - 程序退出前关闭服务器套接字。
编译和测试:
gcc -o tcp_server tcp_server.c
./tcp_server
# 在另一个终端:
# telnet localhost 8080
# 或者
# nc localhost 8080
示例 3:简单的 TCP 客户端套接字设置
这个例子演示了客户端如何创建套接字并连接到服务器。
#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
来指定服务器的地址和端口。sin_family = AF_INET
。sin_port = htons(PORT)
:服务器端口。sin_addr
:服务器 IP 地址。使用inet_pton()
将点分十进制字符串"127.0.0.1"
转换为网络二进制格式。inet_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
函数是网络编程的起点,它创建了通信的端点。理解其参数(域、类型、协议)对于选择正确的通信方式至关重要。它是构建客户端和服务器应用程序的基础。