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

说实话,很多初学者学网络编程,一上来就被这五个函数吓住了。我当年也一样,对着 man 手册看了半天,感觉每个字都认识,但连起来就是不知道它们在干嘛。其实说白了,这五个函数就是网络通信的「五步走」,每一步都有它的道理。

今天我就把这五个函数掰开揉碎了讲清楚。我会结合我这些年踩过的坑,帮你把每个函数的「脾气秉性」摸透。

1. socket() —— 创建通信的「门」

你要跟别人通信,总得先有个门吧?socket() 就是干这个的。它创建一个套接字,相当于在操作系统内核里申请了一个通信端点。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

三个参数,我一个个说:

  • domain:协议族。IPv4 用 AF_INET,IPv6 用 AF_INET6。我建议你直接用 AF_INET,兼容性最好。
  • type:套接字类型。流式套接字 SOCK_STREAM 对应 TCP,数据报套接字 SOCK_DGRAM 对应 UDP。
  • protocol:协议。传 0 就行,让系统根据前两个参数自动选择。

返回值:成功返回一个非负整数(文件描述符),失败返回 -1。

我记得刚入行时,有个同事死活连不上服务器,查了半天发现是 socket() 返回了 -1 但没检查。嗯,这种低级错误我也犯过,后来养成了每次调用都检查返回值的习惯。

2. bind() —— 给门挂上门牌号

门有了,但别人怎么找到你?你得有个地址。bind() 就是把 IP 地址和端口号绑定到套接字上。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

这里有个坑:addr 参数是 struct sockaddr * 类型,但我们实际用的是 struct sockaddr_in(IPv4)。每次都要强制类型转换,挺烦的。我习惯写个辅助函数来封装这个操作。

小技巧:如果你想让服务器监听所有网卡,IP 地址填 INADDR_ANY(值为 0.0.0.0)。端口号建议用大于 1024 的,避免权限问题。

我曾经遇到过一个线上问题:服务器重启后 bind 失败,报「Address already in use」。这是因为之前的连接还没完全关闭。解决方案是在 bind() 之前设置 SO_REUSEADDR 选项。

3. listen() —— 开门营业,准备接客

bind() 只是挂了个门牌,还没开门呢。listen() 就是告诉内核:「我准备好了,可以接受连接请求了」。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

backlog 这个参数很有意思。它指定了连接请求队列的最大长度。什么意思呢?当多个客户端同时连接时,来不及处理的请求会先排队。这个队列满了,新的连接就会被拒绝。

注意:backlog 不是最大连接数!它只是「等待 accept 处理的连接」的最大数量。已经 accept 的连接不在此列。

我见过有人把 backlog 设成 65535,以为能处理更多并发。其实 Linux 内核会把它截断成 somaxconn(默认 128)。你想想看,设那么大有什么用?

4. accept() —— 真正建立连接

这是服务端最核心的函数。accept() 从等待队列里取出一个连接请求,创建一个新的套接字用于通信。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注意,accept() 返回的是一个新的文件描述符。原来的监听套接字还在继续监听。这就像前台接待员:你(监听套接字)负责招呼客人,然后让另一个同事(新套接字)带客人去会议室聊。

关键点:accept() 是阻塞的。如果没有新连接,它会一直等。如果你想做非阻塞,可以用 fcntl() 设置 O_NONBLOCK。

我记得有一次写高并发服务器,发现 accept() 返回了 -1,错误码是 EINTR。这是因为进程收到了信号。嗯,这种情况要重新调用 accept(),不能直接退出。

5. connect() —— 客户端主动敲门

服务端那边又是 bind 又是 listen 又是 accept 的,客户端这边就简单多了:一个 connect() 搞定。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端不需要 bind,系统会自动分配一个临时端口。你只需要指定服务器的 IP 和端口就行。

connect() 也是阻塞的。它会发送 SYN 包,然后等待服务器的 SYN+ACK。如果网络不通或者服务器没启动,它会等超时(通常 75 秒左右)才返回错误。

避坑指南:我曾经写过一个客户端,服务器没启动就调 connect,结果程序卡了 75 秒。后来我设置了 SO_SNDTIMEO 超时选项,几秒没响应就直接报错。

完整流程:一张图看懂

说了这么多,我画张图帮你理清思路。这张图展示了 TCP 服务端和客户端完整的交互流程:

TCP Socket 编程流程 服务端 客户端 socket() 创建套接字 bind() 绑定地址和端口 listen() 开始监听 accept() 等待连接 recv()/send() 通信 close() 关闭 socket() 创建套接字 connect() 发起连接 send()/recv() 通信 close() 关闭 三次握手 数据交换

常见问题与避坑指南

问题 原因 解决方案
bind 返回 EADDRINUSE 端口被占用 设置 SO_REUSEADDR 选项,或换端口
accept 返回 EINTR 被信号中断 重新调用 accept
connect 超时 网络不通或服务器未启动 设置 SO_SNDTIMEO 超时
listen 的 backlog 无效 内核限制 修改 /proc/sys/net/core/somaxconn

重要提醒:永远不要假设函数调用一定会成功。每次调用 socket()、bind()、listen()、accept()、connect() 后,都要检查返回值。这是我在生产环境里用血泪换来的教训。

好了,这五个函数就讲到这里。你想想看,其实它们就是一套标准流程:创建、绑定、监听、接受、连接。理解了每个函数「为什么存在」,用起来就顺手多了。

可可精品资料库