2. C语言Socket编程:socket()、bind()、listen()、accept()、connect()函数详解

说实话,很多初学者一看到Socket编程就头大。我当年也一样,觉得这玩意儿太抽象了。但说白了,Socket就是网络通信的「门牌号」——你要跟别人通信,总得知道对方住哪儿吧?

这一章,咱们就把C语言里最核心的五个Socket函数掰开揉碎了讲。我保证,看完你就能明白整个流程是怎么回事。

2.1 整体流程:先看一张图

在动手写代码之前,我建议你先在脑子里建立整个通信模型。下面这张图,是我自己画的一个简化版TCP通信流程:

服务器端 客户端 socket() 创建套接字 bind() 绑定地址和端口 listen() 进入监听状态 accept() 等待连接 socket() 创建套接字 connect() 发起连接 三次握手 数据通信

嗯,这张图你看懂了吗?服务器端是「被动方」,先创建套接字,绑定地址,然后监听,最后阻塞在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就行,让系统自动选择。
我的小经验: 返回值一定要检查!如果返回 -1,说明创建失败。我见过太多新手直接拿返回值就用,结果后面段错误找半天。
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));
重点: accept()返回的client_fd才是通信用的。原来的sockfd只负责「接客」,不负责「聊天」。我在项目中见过有人搞混这两个fd,结果数据发不出去,排查了半天。

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);
}
避坑指南: 我曾经在connect时忘了检查返回值,结果后面send数据一直失败。connect失败的原因很多——服务器没启动、防火墙拦截、IP地址写错了。一定要打日志!

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」。顺序搞反了,程序就跑不起来。

最后提醒一句: 每个函数调用后都要检查返回值!我见过太多人在这上面栽跟头。网络编程不像单机程序,网络环境复杂得很——服务器挂了、网络断了、端口被占了,什么情况都可能发生。不检查返回值,就等于闭着眼睛开车。

嗯,这一章就到这儿。代码示例都是可以直接跑的,建议你动手敲一遍。光看是学不会网络编程的——我当年也是踩了无数坑才真正搞明白的。