2、Socket API入门:socket()、bind()、listen()、accept()、connect()函数详解

说实话,很多初学者一上来就被这几个函数吓住了。五个函数,名字还都挺像,看着就头大。但我想告诉你的是——这五个函数,其实就是网络编程的「五步拳」。你只要搞懂了每一步在干什么,写起代码来就跟搭积木一样简单。

我个人习惯把这五个函数分成两组:服务端组客户端组。服务端走的是 socket → bind → listen → accept 这条线,客户端走的是 socket → connect。两条线最终交汇,数据就开始流通了。

Socket API 五步拳:服务端 vs 客户端 服务端 socket() 创建套接字 bind() 绑定地址和端口 listen() 开始监听 accept() 接受连接 客户端 socket() 创建套接字 connect() 发起连接 三次握手 服务端先跑起来,客户端后连上去 —— 这是 TCP 网络编程的基本节奏

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 就行,让系统根据前两个参数自动选。
我的习惯:每次创建 socket 后,我都会检查返回值。如果返回 -1,说明创建失败了。别偷懒,一定要加错误处理。
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);
}
我曾经踩过的坑:忘记调用 htons() 转换端口字节序。在 x86 机器上,端口 8080 如果不转换,实际绑定的端口会变成 0x901F(十进制 36911),客户端根本连不上。排查了半天才发现是字节序的问题。

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));
我的经验:accept() 返回的 client_fd 一定要保存好。我见过有人忘了保存,结果所有数据都发到了监听 socket 上,通信完全乱套。

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);
}
注意:connect() 可能会失败。比如服务端没启动、网络不通、防火墙拦截等。一定要检查返回值,并做好重试或报错逻辑。

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) 统一使用网络字节序

总结一下:这五个函数,你只要记住服务端是「创建→绑定→监听→接受」,客户端是「创建→连接」,剩下的就是多写多练。我刚开始学的时候,每天手敲一遍这个流程,敲了三天就烂熟于心了。


公众号:蓝海资料掘金营,微信deep3321