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服务器启动的标准流程。我画了一张图,帮你理清思路:

TCP服务器启动流程 socket() 创建主动套接字 bind() 绑定IP和端口 listen() 转为被动监听 就绪 失败处理 perror() 打印错误 close() 释放资源 检查端口是否被占用 检查权限是否足够 检查backlog是否合理 确认socket状态正确 核心要点 socket → bind → listen 顺序不可颠倒 每个步骤都要检查返回值,失败必须清理资源

你看,整个流程其实很清晰。但有个关键点很多人会忽略:每一步都必须检查返回值。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和收发数据,那才是真正开始通信的部分。


公众号:蓝海资料掘金营,微信deep3321