2. C语言Socket编程:socket()、bind()、listen()、accept()、connect()函数详解
说实话,很多初学者一看到Socket编程就头大。我当年也一样,觉得这玩意儿太抽象了。但说白了,Socket就是网络通信的「门牌号」——你要跟别人通信,总得知道对方住哪儿吧?
这一章,咱们就把C语言里最核心的五个Socket函数掰开揉碎了讲。我保证,看完你就能明白整个流程是怎么回事。
2.1 整体流程:先看一张图
在动手写代码之前,我建议你先在脑子里建立整个通信模型。下面这张图,是我自己画的一个简化版TCP通信流程:
嗯,这张图你看懂了吗?服务器端是「被动方」,先创建套接字,绑定地址,然后监听,最后阻塞在accept()等待客户端来连。客户端是「主动方」,创建套接字后直接connect()去敲门。
2.2 socket() —— 创建通信的「门」
所有网络通信的第一步,就是创建一个套接字。你可以把它想象成装了一扇门——有了门,才能有人进出。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
三个参数,我一个个说:
- domain:协议族。一般用
AF_INET(IPv4)或AF_INET6(IPv6)。我99%的项目都用AF_INET。 - type:套接字类型。
SOCK_STREAM是TCP,SOCK_DGRAM是UDP。 - protocol:协议。传0就行,让系统自动选择。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
2.3 bind() —— 贴上「门牌号」
套接字创建好了,但它还没地址。bind()就是给这个套接字绑定IP地址和端口号。你想想看,没有门牌号,别人怎么找到你?
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里有个坑——第二个参数是 struct sockaddr *,但我们实际用的是 struct sockaddr_in(IPv4地址结构体)。需要强制类型转换一下。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(8888); // 端口号
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
htons() 转换字节序!我刚开始学的时候忘了这个,结果bind总是失败。还有,端口号别用小于1024的——那是系统保留端口,需要root权限。
2.4 listen() —— 打开「门铃」
bind之后,套接字还不会主动接收连接。你需要调用listen(),告诉内核:「嘿,我准备好接客了,有人敲门就通知我。」
int listen(int sockfd, int backlog);
backlog 这个参数,很多人不理解。它表示连接请求队列的最大长度。说白了,就是最多允许多少个客户端在门外排队等着。
我个人习惯设成5或10。设太大没意义,反而浪费资源。
if (listen(sockfd, 5) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
2.5 accept() —— 开门「迎客」
这是服务器端最关键的一步。accept()会阻塞,直到有客户端连接进来。然后它会返回一个新的套接字——这个新套接字才是真正用来和客户端通信的。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
注意,原来的监听套接字还在!它继续监听新的连接。新返回的套接字专门服务当前这个客户端。
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 == -1) {
perror("accept");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("客户端连接成功,IP: %s, 端口: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
2.6 connect() —— 主动「敲门」
客户端这边就简单多了。创建套接字后,直接connect()去连服务器。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里要填服务器的IP和端口。注意,客户端的端口是系统自动分配的,不需要你bind。
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
2.7 完整流程总结
好了,五个函数都讲完了。咱们捋一下整个流程:
| 步骤 | 服务器端 | 客户端 |
|---|---|---|
| 1 | socket() 创建套接字 | socket() 创建套接字 |
| 2 | bind() 绑定地址端口 | (系统自动分配端口) |
| 3 | listen() 进入监听 | connect() 发起连接 |
| 4 | accept() 接受连接 | (连接成功) |
| 5 | recv()/send() 通信 | send()/recv() 通信 |
| 6 | close() 关闭连接 | close() 关闭连接 |
说白了,这五个函数就是TCP通信的「五步曲」。你只要记住:服务器是「先bind再listen再accept」,客户端是「直接connect」。顺序搞反了,程序就跑不起来。
嗯,这一章就到这儿。代码示例都是可以直接跑的,建议你动手敲一遍。光看是学不会网络编程的——我当年也是踩了无数坑才真正搞明白的。