2、Socket API入门:socket()、bind()、listen()、accept()、connect()函数详解
说实话,很多初学者一上来就被这几个函数吓住了。五个函数,名字还都挺像,看着就头大。但我想告诉你的是——这五个函数,其实就是网络编程的「五步拳」。你只要搞懂了每一步在干什么,写起代码来就跟搭积木一样简单。
我个人习惯把这五个函数分成两组:服务端组和客户端组。服务端走的是 socket → bind → listen → accept 这条线,客户端走的是 socket → connect。两条线最终交汇,数据就开始流通了。
2.1 socket() —— 创建通信的「门」
socket() 说白了就是跟操作系统说:「嘿,给我开一个网络通信的接口。」这个接口就像一扇门,数据从这扇门进进出出。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
三个参数,我一个个说:
- domain:地址族。IPv4 用
AF_INET,IPv6 用AF_INET6。99% 的情况你只用 AF_INET。 - type:套接字类型。TCP 用
SOCK_STREAM,UDP 用SOCK_DGRAM。 - protocol:协议。传 0 就行,让系统根据前两个参数自动选。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
2.2 bind() —— 给门贴上「门牌号」
socket 创建好了,但它还没有地址。bind() 就是给这个 socket 绑定一个 IP 地址和端口号。你可以理解为给门贴上门牌号,这样别人才能找到你。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里有个坑,我当年踩过——struct sockaddr_in 和 struct sockaddr 的区别。bind() 的参数类型是 struct sockaddr *,但我们实际用的是 struct sockaddr_in,需要强制类型转换。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
server_addr.sin_port = htons(8080); // 端口号,注意字节序
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
2.3 listen() —— 开门营业,准备接客
bind() 之后,socket 还处于「待机」状态。listen() 就是告诉内核:「我准备好了,可以接受客户端的连接了。」同时还要指定一个连接队列的最大长度。
int listen(int sockfd, int backlog);
backlog 这个参数,很多人理解错了。它不是最大连接数,而是已完成三次握手但还没被 accept() 取走的连接数的上限。说白了就是「待处理队列」的长度。
关键点:backlog 的值不是越大越好。Linux 内核会有一个上限(通常是 /proc/sys/net/core/somaxconn),你设得再大也没用。我个人习惯设 128 或 256,够用。
if (listen(sockfd, 128) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
2.4 accept() —— 从队列里「接」一个客户端
listen() 之后,客户端可以发起连接了。但服务端怎么知道有人连上来了?答案是 accept()。它会阻塞等待,直到有客户端连接进来,然后返回一个新的 socket 文件描述符。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
注意:accept() 返回的是一个新的 socket,专门用来和这个客户端通信。原来的 sockfd 还在继续监听,等着下一个客户端。
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
printf("新客户端连接,IP: %s, 端口: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
2.5 connect() —— 客户端主动敲门
服务端那边已经开门营业了,客户端怎么连上去?用 connect()。它向服务端发起连接请求,完成 TCP 的三次握手。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端的 connect() 不需要 bind(),因为内核会自动给客户端的 socket 分配一个临时端口。你想想看,客户端有成千上万个,如果每个都要手动 bind 一个端口,那不乱套了?
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
exit(EXIT_FAILURE);
}
2.6 完整流程串起来
好了,五个函数都讲完了。我把它们串成一个完整的例子,你看看就明白了。
// 服务端代码片段
int main() {
int listen_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
// 1. 创建 socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 3. 开始监听
listen(listen_fd, 128);
// 4. 接受连接
client_len = sizeof(client_addr);
client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
// 5. 收发数据(略)
// ...
close(client_fd);
close(listen_fd);
return 0;
}
// 客户端代码片段
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 1. 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务端
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 3. 收发数据(略)
// ...
close(sockfd);
return 0;
}
2.7 常见问题速查表
| 问题 | 原因 | 解决方法 |
|---|---|---|
| bind() 返回 EADDRINUSE | 端口已被占用 | 换端口,或设置 SO_REUSEADDR 选项 |
| connect() 超时 | 服务端没启动或防火墙拦截 | 检查服务端状态,ping 测试网络连通性 |
| accept() 返回 -1 | 监听 socket 出错或被信号中断 | 检查 errno,必要时重新创建 socket |
| 数据收发乱码 | 字节序未转换(htons/htonl) | 统一使用网络字节序 |
总结一下:这五个函数,你只要记住服务端是「创建→绑定→监听→接受」,客户端是「创建→连接」,剩下的就是多写多练。我刚开始学的时候,每天手敲一遍这个流程,敲了三天就烂熟于心了。