4. Socket API实战
说实话,很多初学者学网络编程,一上来就被这些 API 吓住了。什么 socket、bind、listen……名字看着就头大。但你别怕,这些东西说白了就是「打电话」的流程。你想想看,打电话要干嘛?先买个手机(socket),然后插上 SIM 卡(bind),接着开机等着(listen),有人打进来你接听(accept),对方拨号(connect),接通了就开始说话(send/recv)。
我当年第一次写 TCP 服务端的时候,对着 man 手册看了半天,结果 bind 老是返回 -1。后来才发现,端口被占用了。嗯,这里要注意,很多坑都是小细节。
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:套接字类型。TCP 用
SOCK_STREAM,UDP 用SOCK_DGRAM。 - protocol:协议。传 0 就行,让系统自动选。
2. bind() —— 绑定地址和端口
bind() 就是把你的套接字和一个本地地址绑在一起。相当于你给手机插上 SIM 卡,有了一个固定的号码。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里有个容易搞混的地方:struct sockaddr_in 和 struct sockaddr。我刚开始学的时候也懵,后来才明白,sockaddr_in 是 IPv4 专用的,用的时候要强转成 sockaddr *。
sin_family = AF_INET,结果 bind 一直失败。还有一次,端口号忘了转成网络字节序(用 htons()),bind 倒是成功了,但客户端连不上。你想想看,端口号不对,能连上才怪。
3. listen() —— 进入监听状态
listen() 让套接字进入被动监听状态。说白了就是:「我准备好了,谁来连我?」
int listen(int sockfd, int backlog);
backlog 这个参数,很多人不理解。它表示连接请求队列的最大长度。我一般设 5 或 10,够用。设太大反而浪费资源。
我记得有一次,线上服务突然拒绝连接了。查了半天,发现是 backlog 设得太小,客户端请求一多就满了。嗯,这个参数别看小,关键时刻能要命。
4. accept() —— 接受连接
accept() 从等待队列里取出一个连接。它是阻塞的,如果没有连接,它就卡在那里等着。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回一个新的套接字文件描述符。注意,是新的!原来的监听套接字还在继续监听。这个新套接字专门用来和这个客户端通信。
5. connect() —— 发起连接
connect() 是客户端用的。它向服务端发起连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
我写客户端程序时,习惯加个超时机制。因为 connect 默认是阻塞的,如果服务端挂了,客户端会卡很久。用 setsockopt() 设置超时,或者用非阻塞模式 + select,都是好办法。
6. send() 和 recv() —— 收发数据
连接建立后,就可以用 send() 和 recv() 收发数据了。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
这里有个大坑:send() 不一定能一次发完所有数据。它返回的是实际发送的字节数。如果返回值小于 len,你得继续发剩下的。recv() 也一样,一次不一定能收完。
7. 完整流程示意图
下面这张图,把整个流程串起来了。你一看就明白。
8. 常见问题与避坑指南
| 问题 | 原因 | 解决办法 |
|---|---|---|
| bind() 返回 EADDRINUSE | 端口被占用 | 换端口,或用 SO_REUSEADDR 选项 |
| connect() 超时 | 服务端没启动或防火墙拦截 | 检查服务端状态,设置超时时间 |
| recv() 返回 0 | 对方关闭了连接 | 关闭本端套接字,退出通信循环 |
| send() 返回小于 len | 内核缓冲区满 | 循环发送剩余数据 |
好了,Socket API 的核心就这些。你把这些函数串起来,一个完整的 TCP 通信程序就出来了。多写几遍,自然就熟了。
可可精品资料库