搭建TCP Socket服务(C++版)

一、参考资料

9.2 I/O 多路复用:select/poll/epoll | 小林coding

二、相关介绍

1. TCP Socket

TCP Socket性能优化秘籍:掌握read、recv、readv、write、send、sendv的最佳实践-CSDN博客

2. socket buffer

Linux(程序设计):48—缓冲区大小(SO_RCVBUF、SO_SNDBUF套接字选项)-CSDN博客

何时调整buffer大小

在以下情况下,调整发送缓冲区大小可能是必要的:

  • 高丢包率的网络:在不可靠的网络中,发送缓冲区需要保留未确认的数据包,较大的缓冲区可以减少阻塞1
  • 高吞吐量应用:对于需要高吞吐量的应用,增加缓冲区大小可以提高性能。

调整send buffer大小:

sysctl -w net.ipv4.tcp_wmem="4096 16384 8388608"

为了使更改永久生效,可以将设置添加到 /etc/sysctl.conf 文件中:

net.ipv4.tcp_wmem = 4096 16384 8388608

使生效:

sysctl -p

通过合理调整 net.ipv4.tcp_wmem 参数,可以优化 TCP 连接的性能,特别是在高负载或不可靠的网络环境中。

send buffer

查看send的默认buffer大小:/proc/sys/net/ipv4/tcp_wmemnet.ipv4.tcp_wmem 是一个内核参数,用于配置 Linux 系统中 TCP 发送缓冲区的大小。它包含三个值,分别表示最小默认最大缓冲区大小。调整这些值可以优化网络性能,特别是在高吞吐量或不可靠网络环境中。

yoyo@yoyo:/proc/sys/net/ipv4$ cat tcp_wmem 
4096	16384	4194304

或者:

yoyo@yoyo:/proc/sys/net/ipv4$ sysctl net.ipv4.tcp_wmem 
net.ipv4.tcp_wmem = 4096	16384	4194304

输出显示最小值为 4096 字节(4KB),默认值为 16384 字节(16KB),最大值为 4194304 字节(4MB)。

recv buffer

查看recv的默认buffer大小:/proc/sys/net/ipv4/tcp_rmem

yoyo@yoyo:/proc/sys/net/ipv4$ cat tcp_rmem 
4096	131072	6291456

或者:

yoyo@yoyo:/proc/sys/net/ipv4$ sysctl net.ipv4.tcp_rmem 
net.ipv4.tcp_rmem = 4096	131072	6291456

一次发送完整buffer

C++ Socket编程中send()函数不能保证一次性发送所有缓冲区的数据-CSDN博客

第五篇:socket通讯中采取循环方式发送数据的优点-CSDN博客

3. 网络字节序

主机字节序(大端/小端) 和 网络字节序 - 52php - 博客园

4. 同步阻塞

同步socket通讯时,程序会阻塞在诸如(connectacceptrecvrecvfrom)等操作上,直到有事件发生时才会继续。

5. I/O多路复用

小胖:远哥,什么是 I/O 多路复用?

Linux下的异步TCP socket及实例_linux tcp服务器的异步模式-CSDN博客

Linux 五种 IO 模型和三种多路复用技术大详解-51CTO.COM

深入了解select、poll、epoll之间的区别 — [野火]嵌入式Linux基础与应用开发实战指南——基于i.MX6ULL开发板 文档

select

Linux网络设计之网络IO与select_linux 读取网口开发 select(devicemax,&deviceread, &device-CSDN博客

epoll

Linux网络设计之网络IO与epoll_linux 文件描述符监听到的事件类型是什么-CSDN博客

reactor网络模型

Linux网络设计之reactor网络模型及其应用-CSDN博客

Linux网络设计之Reactor与http静态服务器-CSDN博客

Linux网络设计之Reactor模型和百万级并发-CSDN博客

三、常用函数

1. 常用头文件

在 TCP 服务器程序中:

  1. 创建套接字socket()<sys/socket.h>)。
  2. 绑定地址bind()<sys/socket.h> + struct sockaddr_in 来自 <netinet/in.h>)。
  3. 监听连接listen()<sys/socket.h>)。
  4. 接受连接accept()<sys/socket.h>)。
  5. 处理数据read()/write()<unistd.h>)或 send()/recv()<sys/socket.h>)。
  6. 关闭套接字close()<unistd.h>)。

<sys/socket.h>

  • 作用:套接字编程的核心头文件,提供套接字操作函数和数据结构。

  • 主要内容

    • 函数socket()(创建套接字)、bind()(绑定地址)、listen()(监听连接)、accept()(接受连接)、connect()(发起连接)、send()/recv()(数据传输)等。

      int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      int listen(int sockfd, int backlog);
      int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      
    • 数据结构struct sockaddr(通用套接字地址结构)。

    • 常量SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、AF_INET(IPv4地址族)等。

  • 用途:实现套接字通信的核心功能,适用于 TCP/UDP 客户端和服务器。

<unistd.h>

作用:提供 POSIX 操作系统 API 的系统调用。

主要内容

  • 函数close()(关闭文件描述符或套接字)、read()/write()(低级 I/O 操作)、fork()(创建子进程)、getpid()(获取进程ID)等。

用途:关闭套接字、进程控制及低级文件操作。

<netinet/in.h>

  • 作用:定义 IPv4/IPv6 地址结构和网络字节序转换函数。

  • 主要内容

    • 数据结构

      • struct sockaddr_in(IPv4 地址结构,包含端口和地址)。

        struct sockaddr_in {     // IPv4地址结构
            sa_family_t    sin_family; // AF_INET
            in_port_t      sin_port;   // 16位端口号
            struct in_addr sin_addr;   // 32位IP地址
            unsigned char  sin_zero[8];// 填充字段
        };
        
        struct in_addr {
            uint32_t s_addr;     // 网络字节序IP地址
        };
        
      • struct sockaddr_in6(IPv6 地址结构)。

    • 字节序函数htonl()htons()(主机到网络字节序)、ntohl()ntohs()(网络到主机字节序)。

      // 字符串IP与二进制转换
      int inet_pton(int af, const char *src, void *dst);
      const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
      
      // 字节序转换
      uint32_t htonl(uint32_t hostlong); // 主机到网络长整型
      uint16_t htons(uint16_t hostshort);
      uint32_t ntohl(uint32_t netlong);  // 网络到主机
      uint16_t ntohs(uint16_t netshort);
      
    • 常量INADDR_ANY(绑定所有接口)、IPPROTO_TCP(TCP 协议号)。

  • 用途:处理 IP 地址和端口号,确保数据在不同系统间的兼容性。

<arpa/inet.h>

  • 作用:提供 IP 地址转换函数。
  • 主要内容
    • 函数:
      • inet_pton()(将字符串 IP 转换为二进制,支持 IPv4/IPv6)。
      • inet_ntop()(将二进制 IP 转换为字符串)。
      • 旧函数:inet_addr()(IPv4 字符串转二进制)、inet_ntoa()(二进制转字符串)。
  • 用途:在可读格式(如 "192.168.1.1")和二进制格式(网络字节序)之间转换 IP 地址。

2. socket()-创建套接字

函数原型

// 创建套接字
/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
int socket(int domain, int type, int protocol);

参数解释

  • domain,表示地址域。
    • AF_INET,IPv4地址。
  • type,表示套接字类型。
    • SOCK_STREAM,流式套接字。
    • SOCK_DGRAM,数据报套接字。
  • protocol,表示协议类型,默认为0。流式套接字默认为TCP协议 IPPROTO_TCP,数据报套接字默认 UDP 协议 IPPROTO_UDP

3. bind()-为套接字绑定地址信息(ip、port)

bind() 函数既可以绑定IPV4版本协议,也可以绑定IPV6版本。两个版本的IP头结点的大小不同,定义 sockaddr 为了寻求接口的统一,都要使用 struct sockaddr

struct sockaddr_in;  // IPV4
struct sockaddr_in6; // IPV6

struct sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons("8081");
address.sin_addr.s_addr = INADDR_ANY;

参数解释

  • INADDR_ANY(值为 0.0.0.0)允许服务器监听所有网络接口,无论客户端从哪个网卡连接都能接受。

函数原型

/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);

参数解释

  • sockfd,套接字描述符。
  • addr,要绑定的地址信息。
  • addrlen,地址信息的长度。

4. listen()-监听(服务端)

函数原型

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
int listen(int sockfd, int backlog);

参数解释

  • sockfd,sockfd描述符。
  • backlog,表示等待连接队列的最大长度。当连接请求过多时,超过这个值的连接会被拒绝。

5. connect()-连接请求(客户端)

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

参数解释

  • sockfd,客户端的socket描述符。
  • addr,要连接的服务端地址。
  • addrlen,要连接的服务端的socket结构体长度,单位为字节。

6. accept()-接受连接请求(服务端)

accept() 函数是一个阻塞型的函数,连接成功队列中如果没有新的连接到来,那么就会一直阻塞直到有新的客户端连接到来。

函数原型

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数解释

  • sockfd,服务端新创建的socket描述符。
  • addr,新创建连接的客户端地址信息。
  • addrlen,客户端的socket结构体的长度,单位为字节。

当服务端成功接受连接请求之后,会为新连接重新创建一个socket用于专门和这个连接的通信,而这个重新创建的socket是由 accept() 这个函数完成的。 而原来socket函数返回的socket只是用于接受连接,并不用来进行通信

7. recv()接收/send()发送数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数解释

  • sockfd,连接成功的socket描述符。
  • buf,指定接收数据的存放位置。
  • len,指定希望接收的数据长度。
  • flags,默认为0,表示阻塞式接收。

8. close()-关闭套接字

int close(int sockfd);

9. 简单示例

Linux系统C++编程实现TCP通信-CSDN博客

网络编程-一个简单的客户端与服务器程序(0),(看这篇就够了)-CSDN博客

tcpServer

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

int main() {
    // 创建socket套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        return -1;
    }

    // 定义sockaddr结构体
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(atoi("8081"));
    ser.sin_addr.s_addr = inet_addr("192.168.33.1");

    // 绑定地址
    socklen_t len = sizeof(struct sockaddr_in);
    if (bind(sockfd, (struct sockaddr*)&ser, len) < 0) {
        perror("bind error");
        close(sockfd);
        return -1;
    }

    // 监听
    if (listen(sockfd, 5) < 0) {
        perror("listen error");
        close(sockfd);
        return -1;
    }

    // 等待连接
    while(1) {
        int n_sockfd;
        struct sockaddr_in cli;

        // 接受连接
        n_sockfd = accept(sockfd, (struct sockaddr*)&cli, &len);
        if (n_sockfd < 0) {
            perror("accept error");
            continue;
        }

        // 循环接收消息
        while(1) {
            char buff[1024] = {0};

            // 接收消息
            int ret = recv(n_sockfd, buff, 1023, 0);
            if (ret < 0) {
                perror("recv error");
                continue;
            }
            printf("client %s[%d]say: %s \n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port), buff);

            // 发送消息
            memset(buff, 0x00, 1024);
            scanf("%s", buff);
            send(n_sockfd, buff, sizeof(buff), 0);
        }

        close(n_sockfd);
    }
    
    // 关闭连接
    close(sockfd);
}

tcpClient

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

int main(){
    // 创建socket套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        return -1;
    }

    // 定义sockaddr
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(atoi("8081"));
    ser.sin_addr.s_addr = inet_addr("192.168.33.1");

    // 连接请求
    socklen_t len = sizeof(struct sockaddr_in);
    if(connect(sockfd, (struct sockaddr*)&ser, len) < 0) {
        perror("connet error");
        return -1;
    }

    // 循环接收消息
    while (1) {
        // 发送消息
        char buff[1024] = {0};
        scanf("%s", buff);
        send(sockfd, buff, sizeof(buff), 0);

        // 接收消息
        int ret = recv(sockfd, buff, 1023, 0);
        if (ret < 0) {
            perror("recv error");
            return -1;
        }

        printf("server %s[%d]say: %s \n", inet_ntoa(ser.sin_addr), ntohs(ser.sin_port), buff);

    }

    // 关闭套接字
    close(sockfd);
}

四、相关经验

优化TCP服务

一个简单的TCP服务器(C++)的设计 - 田大叔 - 博客园

协议设计建议:

  • 心跳机制:每30秒发送0x06指令保持连接。
  • 数据校验:关键数据传输使用CRC32校验。
  • 完善的错误处理。
  • 多线程/异步处理。
  • 分块传输:大文件分块传输(建议1024-4096字节/块)。
  • 重传机制:增加ACK确认包(0x07指令),超时未确认则重传。
  • 版本协商:固件更新前先交换版本信息。
  • 加密传输:敏感数据使用AES加密载荷。
  • 协议版本管理。

协议扩展性设计:

  • 保留0x80-0xFF为自定义扩展指令
  • 载荷前16字节保留为扩展字段
  • 使用TLV(Type-Length-Value)格式承载复杂参数

完整实现需要考虑:

  • 字节序统一(建议网络字节序)
  • 粘包处理(建议头部定长+长度字段)
  • 超时重传机制
  • 数据完整性验证
  • 多线程处理(收发分离)
  • 流量控制(滑动窗口机制)

需要根据具体硬件性能和网络环境调整:

  • 视频流的分辨率/码率
  • 分块传输的大小
  • 心跳间隔时间
  • 超时重试次数

建议先用Wireshark抓包验证协议格式,再逐步实现完整功能

NetAssist网络调试工具

Windows上网络调试助手NetAssist的使用-CSDN博客

下载地址:https://free.cmsoft.cn/download/cmsoft/assistant/netassist5.0.13.zip

TCP+多线程

TCP服务器的演变过程:揭秘使用多线程实现一对多的TCP服务器-CSDN博客

04.基于C++实现多线程TCP服务器与客户端通信-CSDN博客

TCP+多进程

TCP服务器的演变过程:多进程实现一对多的TCP服务器-CSDN博客

GitHub - WuMingrui98/TCP-Server-Client: C++多进程实现高并发TCP服务器与客户端

TCP+select

TCP服务器的演变过程:IO多路复用机制select实现TCP服务器-CSDN博客

TCP+epoll

【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器-CSDN博客

TCP+epoll+reactor

TCP服务器的演变过程:使用epoll构建reactor网络模型实现百万级并发(详细代码)-CSDN博客

TCP+libevent

TCP服务器的演变过程:C++使用libevent库开发服务器程序-CSDN博客

C++中使用 Protocol Buffers(从零开始掌握跨语言数据传输的利器) - 知乎

TCP+协程(推荐)

GitHub - pxzwxx/Coroutine: C++多线程+协程构建异步并发TCP服务器

纯c协程框架NtyCo实现与原理-CSDN博客

NtyCo的实现 · wangbojing/NtyCo Wiki · GitHub

以同步的编程方式,实现异步的性能

TCP+json

Linux下通过jsoncpp和socket解析和发送json数据的使用例程 - 代码先锋网

基于C++、JsonCpp、Muduo库实现的分布式RPC通信框架-腾讯云开发者社区-腾讯云

自定义应用层通信协议 - 山上有风景 - 博客园

TCP+二进制协议

在C++中如何实现自定义的网络协议 - 我爱学习网

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| 指令类型(1B) | 状态码(1B) | 序列号(2B) | 数据长度(4B) | 载荷(N Bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段说明:

  • 指令类型:1字节(0x01~0xFF)
    • 0x01 拍照指令
    • 0x02 开始录像
    • 0x03 停止录像
    • 0x04 实时预览
    • 0x05 固件更新
    • 0x06 心跳包
    • 0x07 传输确认
  • 状态码:1字节(0x00=成功,其他为错误码)
    • 0x00 成功
    • 0x01 无效指令
    • 0x02 设备忙
    • 0x03 存储空间不足
    • 0x04 校验失败
    • 0x05 固件版本过低
  • 序列号:2字节(用于请求-响应匹配)
  • 数据长度:4字节(大端字节序)
  • 载荷:变长数据

TCP客户端

使用epoll实现异步的tcp请求客户端 - 步孤天 - 博客园

在C++中实现多线程异步TCP消息发送_51CTO博客

心跳检测

void heartbeat_check() {
    while (running) {
        std::this_thread::sleep_for(10s);
        if (sendto(sockfd, "PING", 4, 0, 
                  (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            g_socket_valid = false;
        }
    }
}