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 服务端和客户端完整的交互流程:
常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| bind 返回 EADDRINUSE | 端口被占用 | 设置 SO_REUSEADDR 选项,或换端口 |
| accept 返回 EINTR | 被信号中断 | 重新调用 accept |
| connect 超时 | 网络不通或服务器未启动 | 设置 SO_SNDTIMEO 超时 |
| listen 的 backlog 无效 | 内核限制 | 修改 /proc/sys/net/core/somaxconn |
重要提醒:永远不要假设函数调用一定会成功。每次调用 socket()、bind()、listen()、accept()、connect() 后,都要检查返回值。这是我在生产环境里用血泪换来的教训。
好了,这五个函数就讲到这里。你想想看,其实它们就是一套标准流程:创建、绑定、监听、接受、连接。理解了每个函数「为什么存在」,用起来就顺手多了。
可可精品资料库