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 就行,让系统自动选。
我的习惯: 每次创建 socket 后,立刻检查返回值。如果返回 -1,别急着往下走,先看看 errno。我遇到过有人 socket 返回 -1 还继续往下调,结果整个程序崩了。

2. bind() —— 绑定地址和端口

bind() 就是把你的套接字和一个本地地址绑在一起。相当于你给手机插上 SIM 卡,有了一个固定的号码。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

这里有个容易搞混的地方:struct sockaddr_in 和 struct sockaddr。我刚开始学的时候也懵,后来才明白,sockaddr_in 是 IPv4 专用的,用的时候要强转成 sockaddr *。

我曾经踩过的坑: bind 的时候忘记设置 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. 完整流程示意图

下面这张图,把整个流程串起来了。你一看就明白。

服务端 socket() bind() listen() accept() recv() / send() close() 客户端 socket() connect() send() / recv() close() 三次握手 数据流

8. 常见问题与避坑指南

问题 原因 解决办法
bind() 返回 EADDRINUSE 端口被占用 换端口,或用 SO_REUSEADDR 选项
connect() 超时 服务端没启动或防火墙拦截 检查服务端状态,设置超时时间
recv() 返回 0 对方关闭了连接 关闭本端套接字,退出通信循环
send() 返回小于 len 内核缓冲区满 循环发送剩余数据
我曾经犯过的错: 有一次写服务端,accept 返回后直接用了原来的 sockfd 来 recv,结果死活收不到数据。后来才反应过来,accept 返回的是新套接字,得用那个新的。这个错误我犯了两次,说出来都是泪。

好了,Socket API 的核心就这些。你把这些函数串起来,一个完整的 TCP 通信程序就出来了。多写几遍,自然就熟了。

可可精品资料库