欢迎光临
我们一直在努力

【Socket 进阶之路】第 2 章 Socket 原生 API 零基础上手|socket/bind/listen/accept/connect/read/write 源码

💖 点赞 + 收藏 + 关注,Socket 工业级网络编程全套实战持续更新!

✨ 本专栏 全程手写源码、无阉割、纯工程

0. 前言

上一章我们理解了 Socket 的核心概念、TCP/UDP 选型以及 TCP 流式特性。理论看懂只是第一步,本章我们正式上手 Linux 下原生 Socket API,从零实现一套标准阻塞式 TCP 客户端与 TCP 服务端完整代码。

很多初学者直接复制网上 Demo,但是根本不懂:

  • socket () 创建套接字时参数为什么是 AF_INET、SOCK_STREAM
  • bind 绑定 INADDR_ANY 到底代表什么含义
  • listen 的第二个参数 backlog 监听队列作用是什么
  • 网络字节序 htons、htonl 为什么必须转换,大小端是什么
  • accept、connect、read、write 什么时候会阻塞,如何判断连接断开
  • 本章逐行拆解每一个系统调用,代码可直接在 Ubuntu、Linux 开发板编译运行,同时讲解移植到嵌入式 MCU(LWIP)的兼容要点。

    开发环境:Ubuntu 20.04 / GCC 编译器,编译命令:gcc server.c -o server 、gcc client.c -o client

    1. TCP 阻塞通信整体流程

    服务端完整流程

  • socket():创建 TCP 套接字文件描述符
  • bind():绑定本地 IP 地址 + 端口号
  • listen():开启监听,设置连接等待队列
  • accept():阻塞等待客户端发起连接,返回新连接 fd
  • read() / write():和客户端双向收发数据
  • close():关闭套接字,释放资源
  • 客户端完整流程

  • socket():创建 TCP 套接字
  • connect():主动连接服务端指定 IP 与端口
  • write() / read():双向收发业务数据
  • close():断开连接,释放资源
  • 重点区分:listen、accept 只有服务端需要调用;connect 只有客户端调用。

    2. 核心 API 详细讲解

    2.1 socket () 创建套接字

    int socket(int domain, int type, int protocol);

    • domain:地址族,AF_INET = IPv4 协议
    • type:套接字类型,SOCK_STREAM = TCP 流式套接字;SOCK_DGRAM = UDP 数据报套接字
    • protocol:协议类型,TCP 填 0 即可
    • 返回值:成功返回文件描述符 fd(int),失败返回 -1

    2.2 sockaddr_in IPv4 地址结构体

    所有绑定、连接都需要填充这个结构体:

    struct sockaddr_in {
    sa_family_t sin_family; // 地址族 AF_INET
    in_port_t sin_port; // 端口号,必须网络字节序
    struct in_addr sin_addr; // IPv4地址
    unsigned char sin_zero[8]; // 保留填充,补齐字节
    };

    struct in_addr {
    uint32_t s_addr; // ip地址,网络字节序
    };

    通用结构体 struct sockaddr 是为了兼容 IPv4/IPv6,实际开发优先使用 sockaddr_in,强制类型转换传入 bind/connect。

    2.3 字节序转换函数(工程必用)

    CPU 存储分为主机字节序(小端),网络传输统一规定 大端(网络字节序),必须转换,否则通信异常!

    • htons(uint16_t hostshort):host to network short,端口转换(16 位)
    • htonl(uint32_t hostlong):host to network long,IP 转换(32 位)
    • ntohs / ntohl:网络字节序转主机字节序

    INADDR_ANY:宏定义 0.0.0.0,表示绑定本机所有网卡 IP,多网卡设备推荐使用。

    2.4 bind () 绑定地址端口

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

    sockfd:socket 创建返回的 fd;addr 地址结构体;addrlen 结构体大小 返回值:0 成功,-1 失败

    常见报错:Address already in use 端口被占用,后续章节讲解端口复用 setsockopt

    2.5 listen () 开启监听

    backlog:内核等待连接队列最大长度,Linux 新版代表未完成握手 + 已完成握手队列总和,一般填 5、10 即可,不要过大。

    2.6 accept () 阻塞等待连接

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

    阻塞函数:没有客户端连接时,程序卡在 accept 不往下运行 成功返回 新的连接文件描述符 connfd,后续收发数据全部使用 connfd!

    注意区分:sockfd 是监听套接字(一直保留),connfd 是每个客户端独立通信套接字

    2.7 connect () 客户端发起连接

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

    客户端调用,向服务端 IP 端口发起三次握手。阻塞 API,握手成功返回 0。

    2.8 read /write 数据收发

    套接字可以当作普通文件描述符,使用 read、write:

    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);

    返回值说明(重中之重):

  • 返回 >0:成功读到 / 写入对应字节数
  • 返回 =0:对端正常关闭连接(四次挥手完成)
  • 返回 <0:出错,errno 判断具体错误(ECONNRESET 连接重置等)
  • 工程避坑:read 返回 0 必须关闭 fd,回收资源,防止句柄泄漏!

    2.9 close () 关闭套接字

    int close(int fd);

    调用后触发 TCP 四次挥手,释放文件描述符资源。

    3. 完整代码实战:TCP 阻塞服务端 server.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT 8888
    #define BUF_LEN 1024

    int main(void)
    {
    int sockfd, connfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char recv_buf[BUF_LEN] = {0};

    // 1. 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
    perror("socket create fail");
    exit(EXIT_FAILURE);
    }

    // 填充服务端地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 2. 绑定端口IP
    if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
    perror("bind fail");
    close(sockfd);
    exit(EXIT_FAILURE);
    }

    // 3. 开启监听
    if (listen(sockfd, 5) < 0)
    {
    perror("listen fail");
    close(sockfd);
    exit(EXIT_FAILURE);
    }
    printf("Server start success, listen port: %d\\n", PORT);

    // 4. 阻塞等待客户端连接
    connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
    if (connfd < 0)
    {
    perror("accept fail");
    close(sockfd);
    exit(EXIT_FAILURE);
    }
    printf("client connect: %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 5. 循环收发数据
    while (1)
    {
    memset(recv_buf, 0, BUF_LEN);
    int ret = read(connfd, recv_buf, BUF_LEN – 1);
    if (ret > 0)
    {
    printf("recv data: %s\\n", recv_buf);
    // 回发数据给客户端
    write(connfd, recv_buf, ret);
    }
    else if (ret == 0)
    {
    printf("client disconnect!\\n");
    break;
    }
    else
    {
    perror("read error");
    break;
    }
    }

    close(connfd);
    close(sockfd);
    return 0;
    }

    4. 完整代码实战:TCP 阻塞客户端 client.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define SERVER_IP "127.0.0.1"
    #define SERVER_PORT 8888
    #define BUF_LEN 1024

    int main(void)
    {
    int sockfd;
    struct sockaddr_in serv_addr;
    char send_buf[BUF_LEN] = {0};
    char recv_buf[BUF_LEN] = {0};

    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
    perror("socket create fail");
    exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    // 将字符串IP转为网络字节序
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0)
    {
    perror("inet_pton fail");
    close(sockfd);
    exit(EXIT_FAILURE);
    }

    // 2. 连接服务端
    if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
    perror("connect fail");
    close(sockfd);
    exit(EXIT_FAILURE);
    }
    printf("connect server success!\\n");

    // 循环输入发送数据
    while (1)
    {
    printf("please input send data:");
    fgets(send_buf, BUF_LEN, stdin);
    // 去除fgets自带换行符
    send_buf[strcspn(send_buf, "\\n")] = '\\0';

    write(sockfd, send_buf, strlen(send_buf));
    memset(recv_buf, 0, BUF_LEN);
    int ret = read(sockfd, recv_buf, BUF_LEN – 1);
    if (ret > 0)
    {
    printf("server echo: %s\\n", recv_buf);
    }
    else if (ret == 0)
    {
    printf("server close connect!\\n");
    break;
    }
    else
    {
    perror("read error");
    break;
    }
    }

    close(sockfd);
    return 0;
    }

    5. 编译运行测试步骤

  • 保存两份代码 server.c 、client.c
  • 终端编译:
  • gcc server.c -o server
    gcc client.c -o client

  • 第一个终端运行服务端:./server
  • 新开第二个终端运行客户端:./client
  • 客户端输入文字回车,服务端接收并原路回显(回声服务)
  • 测试现象

    客户端输入 hello socket 服务端打印收到数据,并回传给客户端显示。 客户端 ctrl+c 退出,服务端检测 read 返回 0,打印客户端断开。

    6. 本章工程重点踩坑总结

  • 端口和 IP必须转换成网络大端字节序,忘记 htons 是新手最高发 bug;
  • accept 返回的 connfd 才是通信 fd,监听 sockfd 不能用来收发数据;
  • read 返回 0 = 对端正常关闭连接,必须关闭 fd 释放资源,长期运行会句柄泄漏;
  • 阻塞模式下:accept、read、connect 都会卡住线程,无法同时处理多客户端;
  • 拓展:本章是单客户端阻塞模型,无法支持多个客户端同时连接,后续 IO 多路复用章节解决并发问题

  • fgets 读取自带换行符,发送前建议截断,避免多余数据传输;
  • INADDR_ANY 绑定所有网卡,如果指定固定网卡,填写对应网卡 IP 即可。
  • 7. 本章小结

    本章完整梳理阻塞 TCP 通信全流程,逐个拆解 Socket 基础 API 含义,实现了回声服务端与客户端可运行源码。 我们掌握了套接字创建、地址绑定、监听、连接建立、双向收发、连接关闭整套标准流程,理解阻塞 IO 基础特性。

    遗留问题思考:

  • TCP 三次握手、四次挥手在内核到底发生了什么?连接状态如何流转?
  • 为什么有时候客户端断开,服务端很久感知不到(半连接)?
  • TIME_WAIT、CLOSE_WAIT 是什么,怎么处理?
  • 下一章:TCP 三次握手 & 四次挥手深度剖析|连接状态机、TIME_WAIT 核心工程问题

     

    赞(0)
    未经允许不得转载:171主机测评 » 【Socket 进阶之路】第 2 章 Socket 原生 API 零基础上手|socket/bind/listen/accept/connect/read/write 源码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址