TCP编程基础(上):socket()、bind()、listen()函数详解与实战
好,咱们正式开始接触TCP编程的核心了。说实话,很多初学者一上来就被这三个函数吓住了——socket、bind、listen,感觉像是什么高深莫测的魔法。其实你拆开来看,每个函数干的活都很具体,很实在。
我当年第一次写TCP服务器的时候,照着书敲了一遍代码,结果bind返回-1,查了半天才发现是端口被占用了。嗯,这种坑踩过一次就记住了。今天咱们就把这三个函数彻底讲透,让你写代码的时候心里有底。
1. socket() —— 创建通信端点
socket这个词直译是“插座”,说白了就是网络通信的端点。你要打电话,得先有一部电话机对吧?socket就是那个电话机。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数其实就三个:
- domain:协议族,一般用
AF_INET(IPv4)或AF_INET6(IPv6)。我99%的项目都用AF_INET,IPv6的坑更多一些。 - type:套接字类型,TCP用
SOCK_STREAM,UDP用SOCK_DGRAM。 - protocol:协议号,传0就行,系统会根据前两个参数自动选择。
返回值:成功返回一个非负整数(文件描述符),失败返回-1并设置errno。
举个例子:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
你看,就这么简单。不过有个细节我得提醒你——socket()返回的文件描述符默认是主动套接字,也就是用于主动发起连接的。后面我们要把它变成被动监听套接字,那就要靠bind和listen了。
2. bind() —— 绑定地址和端口
socket创建好了,但它还没有“身份”。就像你有了电话机,但还没插电话线。bind就是干这个的——把socket和本地的IP地址、端口号绑定在一起。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里有个容易让人困惑的地方:struct sockaddr是个通用结构体,我们实际用的是struct sockaddr_in(IPv4)。每次都要做类型转换,确实有点烦人,但这是历史遗留问题,咱们忍了。
看一个完整的绑定示例:
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口
server_addr.sin_port = htons(8888); // 端口号
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
注意:端口号小于1024的需要root权限。我建议开发阶段用8000-9999之间的端口,省得每次都要sudo。
这里有两个函数你一定会频繁用到:
htonl():把32位整数从主机字节序转换成网络字节序htons():把16位整数从主机字节序转换成网络字节序
为什么要转换?因为不同CPU的字节序不一样。Intel的CPU是小端模式,而网络协议规定用大端模式。不转换的话,你绑定的端口是8888,实际可能变成0x22B8,那可就乱套了。
我曾经在项目里见过一个bug:同事在x86机器上测试没问题,代码部署到ARM开发板上就bind失败了。查了半天,原来是忘了用htons()。ARM虽然是小端,但某些嵌入式板子的网络栈实现有差异,这种问题特别隐蔽。
3. listen() —— 进入监听状态
bind之后,socket已经绑定了地址,但它还是个“主动套接字”。listen的作用就是把它变成被动监听套接字,告诉内核:我要开始接受客户端的连接了。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
第二个参数backlog很多人理解错了。它不是最大连接数,而是已完成连接队列的长度。说白了,就是内核帮你排队的连接请求数量。
核心要点:backlog表示内核为socket维护的已完成连接队列的最大长度。当队列满了,新的连接请求会被拒绝。
那backlog设多大合适?
| 场景 | 推荐backlog值 | 说明 |
|---|---|---|
| 小型服务(并发<100) | 5-10 | 够用,省内存 |
| 中型服务(并发100-1000) | 128 | Linux默认值 |
| 高并发服务(并发>1000) | 1024或更大 | 需要配合调整内核参数 |
我个人习惯设128,除非有特殊需求。设得太大其实没意义,因为真正的瓶颈在后面的accept()处理速度。
if (listen(sockfd, 128) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
这里有个细节:listen成功后,socket就变成了被动套接字,不能再用于主动连接了。如果你试图用这个socket去connect,会报错。
4. 完整流程与核心逻辑
把三个函数串起来,就是TCP服务器启动的标准流程。我画了一张图,帮你理清思路:
你看,整个流程其实很清晰。但有个关键点很多人会忽略:每一步都必须检查返回值。socket失败、bind失败、listen失败,任何一个出问题,服务器都跑不起来。
5. 完整代码示例
把上面讲的整合起来,就是一个完整的TCP服务器初始化代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 2. 设置地址结构体
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
// 3. 绑定地址
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
// 4. 开始监听
if (listen(sockfd, 128) < 0) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port 8888...\n");
// 这里后面会接 accept() 循环处理连接
// 先保持程序运行,方便测试
while (1) {
sleep(1);
}
close(sockfd);
return 0;
}
小技巧:调试时可以用netstat -anp | grep 8888查看端口是否在监听。如果看到LISTEN状态,说明bind和listen都成功了。
6. 常见问题与避坑指南
我总结几个新手最容易踩的坑:
- bind失败,提示Address already in use:端口被占用了。要么换端口,要么等一会儿再试。也可以用
setsockopt()设置SO_REUSEADDR选项,允许重用地址。 - bind失败,提示Permission denied:端口号小于1024,需要root权限。我建议开发时用大于1024的端口。
- listen成功但客户端连不上:检查防火墙。很多Linux发行版默认开启防火墙,会拦截外部连接。
- 程序退出后端口还占着:这是正常现象,TCP有TIME_WAIT状态。等一两分钟就好了,或者用
SO_REUSEADDR。
我曾经在做一个物联网网关项目时,设备重启后bind总是失败,查了半天才发现是上一个进程没有正常关闭socket。后来我在代码里加了SO_REUSEADDR,问题就解决了。嗯,这种经验说多了都是泪。
好了,socket、bind、listen这三个函数就讲到这里。你想想看,其实每个函数干的活都很明确:socket造电话机,bind插电话线,listen把电话机调到自动接听模式。下一节我们会讲accept和收发数据,那才是真正开始通信的部分。