Linux 基于 TCP 协议的简单服务器-客户端应用

news2024/11/20 10:28:12

目录

一、相关函数 

1、listen()

2、accept()

3、connect() 

4、两种IP地址转换方式 

5、TCP和UDP数据发送和接收函数对比

5、log.hpp自定义记录日志

二、udp_server.hpp单进程版本

三、tcp_server.cc

四、Telnet客户端(代替tcp_client.cc)

五、多进程实现udp_server.hpp

1、多进程版本一

2、tcp_client.cc

3、多进程版本二

六、多线程版本

七、线程池版本

tcp_server.hpp

ThreadPool代码

lockGuard.hpp

log.hpp

thread.hpp

threadPool.hpp

八、实现回显、字符转换、在线字典查询服务

tcp_server.hpp

 三个服务函数

TcpServer类

tcp_server.cc

tcp_client.cc

九、TCP协议通讯流程

1、服务器初始化

2、建立连接的过程(三次握手)

3、数据传输的过程

4、断开连接的过程(四次挥手)


一、相关函数 

1、listen()

int listen(int socket, int backlog);

只有对于TCP服务端套接字才需要调用此函数,它使套接字进入监听状态,等待客户端的连接请求。参数含义如下:

成功监听后返回0,出错则返回非零错误码。

  • socket:要监听的服务器端套接字描述符。
  • backlog:指定同时可以排队等待处理的最大连接数。超过这个数量的连接请求会被拒绝。

2、accept()

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

也是只在TCP服务器端使用,用于接受一个客户端的连接请求。参数含义如下:

成功接受一个连接请求后,accept()函数返回一个新的套接字描述符,这个描述符用于与该客户端进行通信。同时,address参数所指向的结构体会填充上客户端的地址信息。

  • socket:已经监听的服务器端套接字描述符。
  • address:用于存储新连接客户端的地址信息的sockaddr结构体指针。
  • address_len:指向一个socklen_t变量的指针,用于记录地址结构体的实际大小,传入时应初始化为地址结构体的大小,返回时会更新为实际填充的大小。

3、connect() 

TCP的connect函数是用于客户端编程中的一个重要系统调用,它是TCP/IP协议栈的一部分,允许客户端应用程序建立与远程服务器的连接。在C语言或C++编程环境下,connect函数的基本原型如下:

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
  • sockfd:这是一个之前通过socket()函数创建并返回的套接字描述符,标识着一个未连接的套接字。

  • serv_addr:这是一个指向sockaddr结构体的指针,包含了远程服务器的地址信息,对于IPv4而言,通常会使用sockaddr_in结构体,其中包括服务器的IP地址和端口号。

  • addrlen:这是serv_addr指向的地址结构体的长度。

当调用connect函数时,TCP客户端会执行以下动作:

  1. 发起连接请求connect函数会触发TCP三次握手的过程,即客户端发送一个SYN(同步)分节给服务器,请求建立连接。

  2. 等待响应:客户端会等待服务器回应SYN+ACK分节,然后发送ACK(确认)分节作为响应。

  3. 连接建立:一旦三次握手成功完成,连接就建立了,此时套接字的状态转变为ESTABLISHED,客户端可以在该套接字上进行读写操作。

  4. 错误处理:如果在一定时间内没有收到服务器的响应,或者由于其他原因无法建立连接(比如网络问题、服务器拒绝连接等),connect函数会返回错误,errno会被设置为相应的错误代码,如ETIMEDOUT(超时)、ECONNREFUSED(连接被拒绝)等。

例如,假设已经有了一个未连接的套接字sockfd,并且有了服务器的地址信息serv_addr,可以通过以下方式调用connect函数:

struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT_NUMBER);
inet_pton(AF_INET, SERVER_IP_ADDRESS, &serv_addr.sin_addr);

if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("connect error");
    // 错误处理...
} else {
    // 连接成功,可以开始进行数据交换
}

在这里,PORT_NUMBER是服务器监听的端口号,SERVER_IP_ADDRESS是服务器的IP地址,通过inet_pton函数将IP地址字符串转换为网络字节序的形式存放在sin_addr中。成功连接后,应用程序就可以通过writeread或其他IO函数与服务器进行双向数据传输。

4、两种IP地址转换方式 

在这段代码中,TcpServer 类用于创建一个 TCP 服务器,初始化时会绑定到特定的 IP 地址和端口上。

第一种方式:

// 默认构造函数,若不传入ip则默认绑定所有网络接口,“0.0.0.0”等于空字符“”
TcpServer(uint16_t port, std::string ip = "0.0.0.0") 
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
  • inet_pton 是一个从点分十进制格式的字符串转换为网络字节序二进制格式的函数,这里它将 _ip 字符串转换为 sockaddr_in 结构体中的 sin_addr 成员。
  • 当 _ip 为空字符串或默认值 "0.0.0.0" 时,服务器将会监听所有可用网络接口。

第二种方式:

TcpServer(uint16_t port, std::string ip = "") // 若不传入ip,则默认为空字符串
inet_aton(_ip.c_str(), &local.sin_addr);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
  • inet_aton 和 inet_pton 功能类似,也是将点分十进制的 IP 地址字符串转换为网络字节序的二进制表示形式。
  • 如果 _ip 为空字符串,那么下面的条件语句会执行:
    local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
    • 当 _ip 为空时,sin_addr.s_addr 被赋值为 INADDR_ANY,这同样表示服务器应监听所有可用网络接口。

结论:两种方式都可以实现当未传入 IP 地址参数时,服务器监听所有网络接口的目的。不过,在现代 C++ 编程实践中,推荐使用 inet_pton 函数,因为它支持 IPv6 地址,并且在一些平台上兼容性更好。

5、TCP和UDP数据发送和接收函数对比

TCP

  • 数据发送:
    • write() 或 send() 函数用于在已建立连接的TCP套接字上发送数据。send() 可以带有额外的标志参数,但对于大多数情况,write() 即可满足需求。
  • 数据接收:
    • read() 或 recv() 函数用于在TCP套接字上接收数据。recv() 同样可以携带标志参数,但通常情况下,read() 已足够用于接收TCP数据流。

UDP

  • 数据发送:
    • 因为UDP是无连接的协议,所以在发送数据时需要指定目的地址,因此使用 sendto() 函数,它需要包含目标IP地址和端口号的sockaddr结构体作为参数。
  • 数据接收:
    • 对应地,在接收UDP数据时,不仅要接收数据,还需要得到发送数据的源地址和端口号,因此使用 recvfrom() 函数,它不仅能返回接收到的数据,还能填充提供给它的sockaddr结构体。

5、log.hpp自定义记录日志

#pragma once

#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

#define LOGFILE "./threadpool.log"

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);

    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    FILE *fp = fopen("LOGFILE", "a+");
    fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    fclose(fp);
}

二、udp_server.hpp单进程版本

TcpServer类实现了创建TCP服务器、监听客户端连接、处理客户端连接服务的基本功能。通过调用initServer()方法初始化服务器,然后调用start()方法开始监听和处理客户端连接。当有新客户端连接时,创建子进程(或线程)处理与该客户端的通信。

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>

// 定义静态函数,用于处理客户端连接的服务逻辑
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << clientip << ":" << clientport << "#" << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

// 定义TCP服务器类
class TcpServer
{
private:
    const static int gbacklog = 20; // 服务器监听队列长度(最大挂起连接数)

public:
    // 构造函数,接收服务器监听端口和可选的绑定IP地址
    TcpServer(uint16_t port, std::string ip = "")
        : listensock(-1), _port(port), _ip(ip)
    {}

    // 初始化服务器:创建套接字、绑定端口、监听连接
    void initServer()
    {
        listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
        if (listensock < 0)
        {
            logMessage(FATAL, "%d%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success listensock: %d", listensock);

        struct sockaddr_in local; // 用于存储服务器地址信息的结构体
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET; // 设置协议族为IPv4
        local.sin_port = htons(_port); // 设置服务器监听端口,转换为主机字节序
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 设置服务器绑定IP地址

        if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定套接字到指定地址和端口
        {
            logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
        if (listen(listensock, gbacklog) < 0) // 开始监听客户端连接,设置监听队列长度
        {
            logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    // 启动服务器:设置信号处理、循环接受客户端连接并创建子进程处理
    void start()
    {
        // 设置信号处理,忽略SIGCHLD信号以自动回收子进程资源
        signal(SIGCHLD, SIG_IGN);

        while (true)
        {
            struct sockaddr_in src; // 用于存储客户端地址信息的结构体
            socklen_t len = sizeof src;
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客户端连接请求
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(src.sin_port); // 获取客户端端口
            std::string client_ip = inet_ntoa(src.sin_addr); // 获取客户端IP地址

            logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
                        servicesock, client_ip.c_str(), client_port);
            
            service(servicesock, client_ip, client_port); // 子进程处理客户端连接
            close(servicesock); // 主进程中关闭已接受的客户端连接套接字            
        }
    }

    // 析构函数
    ~TcpServer()
    {
    }

private:
    uint16_t _port; // 服务器监听端口
    std::string _ip; // 服务器绑定IP地址(可选)
    int listensock; // 服务器监听套接字
};

这段代码定义了一个名为TcpServer的类,用于实现一个基础的TCP服务器。该服务器具有以下功能:

  1. 构造函数

    • TcpServer(uint16_t port, std::string ip = ""):初始化服务器对象,接收一个监听端口号port和一个可选的服务器绑定IP地址ip。默认情况下,如果不提供IP地址,服务器将在所有可用网络接口上监听。
  2. initServer()

    • 创建TCP套接字。
    • 使用struct sockaddr_in结构体存储服务器地址信息。
    • 绑定服务器套接字到指定的IP地址和端口号。
    • 开始监听客户端的连接请求,并设置监听队列的最大长度为gbacklog(默认20)。
  3. service()静态函数

    • 用于处理与单个客户端的连接服务逻辑。
    • 通过read()函数读取客户端发送过来的数据,然后回显到控制台。
    • 若读取到的数据长度为0,表示客户端关闭连接,服务器也结束对该客户端的服务。
    • 若读取过程中发生错误,则记录错误并结束服务。
    • 使用write()函数将读取到的数据回传给客户端。
  4. start()

    • 设置信号处理,忽略SIGCHLD信号,这样操作系统会在子进程结束后自动回收资源。
    • 服务器进入无限循环,不断地通过accept()函数接受新的客户端连接请求。
    • 当接收到新的连接请求时,获取客户端的IP地址和端口号,并调用service()函数处理客户端连接。
    • 处理完客户端连接后,关闭已接受的客户端连接套接字。
  5. 析构函数

    • 类似于其他类的析构函数,~TcpServer()在此处没有特别的操作,但在实际开发中可能需要关闭监听套接字或执行其他清理工作。

三、tcp_server.cc

#include "tcp_server.hpp"
#include <memory>

// 定义展示程序用法的帮助函数
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    // 检查命令行参数数量是否为2(即程序名 + 监听端口号)
    if (argc != 2)
    {
        usage(argv[0]); // 输出程序使用说明
        exit(1); // 参数错误,退出程序
    }

    // 从命令行参数中获取监听端口号并转换为整型数值
    uint16_t port = atoi(argv[1]);

    // 使用智能指针创建并管理TCP服务器实例
    std::unique_ptr<TcpServer> svr(new TcpServer(port));

    // 初始化服务器,包括创建套接字、绑定端口和开始监听客户端连接
    svr->initServer();

    // 启动服务器,开始循环接受客户端连接并创建子进程处理
    svr->start();

    // 当`svr`的作用域结束时,智能指针会自动释放TCP服务器实例
    // 此时由于TCP服务器已经进入了无限循环的`start()`方法,程序不会立即结束
    // 而是在接收到终止信号(如Ctrl+C)或系统关闭时,TCP服务器才会停止运行

    // 返回0,表示程序正常退出
    return 0;
}

四、Telnet客户端(代替tcp_client.cc)

在Linux CentOS环境下,telnet 是一个命令行工具,用于通过Telnet协议与远程主机上的服务进行交互。Telnet最初设计用于远程登录和命令行交互,但在现代环境中,由于其不提供加密保护,通常被更安全的SSH(Secure Shell)协议所取代。尽管如此,Telnet在某些特定场景下(如测试、调试网络服务)因其简单易用仍被临时使用。

1. 安装Telnet客户端

在CentOS系统中,telnet客户端可能未预装。若要使用telnet,首先需要确保已经安装了该客户端。可以通过以下命令安装:

sudo yum install telnet

2. 使用telnet命令

基本语法如下:

telnet [options] host [port]
  • options:可选的命令行选项,如 -l username 用于指定登录用户名。
  • host:远程主机的IP地址或域名,如 192.168.0.100 或 example.com
  • port:可选的端口号,用于指定远程主机上服务监听的端口。如果不指定,默认为Telnet服务的标准端口 23

3. 示例:连接到远程主机

要连接到IP地址为192.168.0.100、端口为23的远程主机,执行:

telnet 192.168.0.100

或者连接到特定端口(如 8080)上的服务:

telnet 192.168.0.100 8080

4. 交互过程

  • 成功连接后,将看到类似于以下的响应:

    Trying 192.168.0.100...
    Connected to 192.168.0.100.
    Escape character is '^]'.
  • 如果远程主机要求身份验证,可能需要输入用户名和密码。这些凭据将以明文形式在网络中传输。

  • 输入用户名和密码后(如果有),将进入远程主机的命令行环境,可以像在本地终端一样输入命令并观察响应。

  • 若要断开连接,可以输入命令 logout 或 quit,然后按回车。或者直接使用快捷键 Ctrl+](即按住Ctrl键同时按下右方括号]),接着输入 q 并回车,快速退出Telnet客户端。

5. 安全注意事项

由于Telnet不提供任何加密保护,其明文传输特性使得用户名、密码以及整个会话内容都容易被嗅探。在实际环境中,强烈建议使用更安全的替代方案,如SSH(Secure Shell),它提供了加密的远程登录功能,能有效保护敏感信息的安全。如果必须使用Telnet,请确保仅在受信任的网络环境中进行,并且了解潜在的安全风险。

6. 其他实用操作

除了基本的远程登录,telnet还可以用于简单的网络诊断,如测试某个端口是否开放。例如,要检查远程主机example.com80端口是否开放,可以执行:

telnet example.com 80

如果端口开放且服务响应,将看到类似以下的输出(以HTTP服务为例):

Trying 93.184.216.34...
Connected to example.com.
Escape character is '^]'.

此时,可以尝试输入HTTP请求(如GET / HTTP/1.1,然后回车两次),观察服务是否返回响应。如果端口未开放或无服务响应,将看到类似“Connection refused”或“Timeout”的错误消息。这种简易的测试方法可以帮助初步判断远程主机的网络服务状态。然而,对于专业的网络诊断,建议使用更专业的工具,如nc(Netcat)或nmap

五、多进程实现udp_server.hpp

1、多进程版本一

这个TcpServer类利用了fork()函数实现了多进程方式处理并发客户端连接,相比单进程版本增加了并发能力和资源隔离性,但也引入了额外的系统调用开销。

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>

// 定义静态函数,用于处理客户端连接的服务逻辑
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << clientip << ":" << clientport << "#" << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

// 定义TCP服务器类
class TcpServer
{
private:
    const static int gbacklog = 20; // 服务器监听队列长度(最大挂起连接数)

public:
    // 构造函数,接收服务器监听端口和可选的绑定IP地址
    TcpServer(uint16_t port, std::string ip = "")
        : listensock(-1), _port(port), _ip(ip)
    {}

    // 初始化服务器:创建套接字、绑定端口、监听连接
    void initServer()
    {
        listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
        if (listensock < 0)
        {
            logMessage(FATAL, "%d%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success listensock: %d", listensock);

        struct sockaddr_in local; // 用于存储服务器地址信息的结构体
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET; // 设置协议族为IPv4
        local.sin_port = htons(_port); // 设置服务器监听端口,转换为主机字节序
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 设置服务器绑定IP地址

        if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定套接字到指定地址和端口
        {
            logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
        if (listen(listensock, gbacklog) < 0) // 开始监听客户端连接,设置监听队列长度
        {
            logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    // 启动服务器:设置信号处理、循环接受客户端连接并创建子进程处理
    void start()
    {
        // 设置信号处理,忽略SIGCHLD信号以自动回收子进程资源
        signal(SIGCHLD, SIG_IGN);

        while (true)
        {
            struct sockaddr_in src; // 用于存储客户端地址信息的结构体
            socklen_t len = sizeof src;
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客户端连接请求
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(src.sin_port); // 获取客户端端口
            std::string client_ip = inet_ntoa(src.sin_addr); // 获取客户端IP地址

            logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
                        servicesock, client_ip.c_str(), client_port);
            
            // 多进程处理客户端连接
            pid_t id = fork(); // 创建子进程
            assert(id != -1); // 断言子进程创建成功
            if (id == 0) // 子进程
            {
                close(listensock); // 子进程中关闭监听套接字
                service(servicesock, client_ip, client_port); // 子进程处理客户端连接
                exit(0); // 子进程处理完客户端连接后退出
            }

            close(servicesock); // 主进程中关闭已接受的客户端连接套接字
        }
    }

    // 析构函数
    ~TcpServer()
    {
    }

private:
    uint16_t _port; // 服务器监听端口
    std::string _ip; // 服务器绑定IP地址(可选)
    int listensock; // 服务器监听套接字
};

这个TcpServer类相较于单进程版本,主要区别在于如何处理每个客户端连接。在这个多进程版本中,服务器在接收到客户端连接请求后,通过fork()系统调用创建子进程来处理每个客户端连接。以下是不同之处的详细说明:

  1. 启动服务器(start()方法)的变化

    • start()方法内,当服务器通过accept()函数成功接受一个客户端连接后,调用fork()创建一个子进程。

    • 子进程中:

      • 关闭监听套接字(listensock),因为它仅用于监听新的连接请求,无需在处理现有客户端连接的子进程中保持打开。
      • 调用service()函数处理客户端连接。
      • service()函数结束后,子进程调用exit(0)退出,释放资源。
    • 主进程中:同样关闭已接受的客户端连接套接字,但在主进程中这样做是为了让主进程能够继续监听新的客户端连接,而不是去处理已连接的客户端通信。

  2. 多进程处理客户端连接的优势

    • 并发处理:父进程可以继续接受新的客户端连接请求,而子进程独立处理已连接的客户端,从而实现并发处理多个客户端连接。
    • 资源隔离:每个客户端连接都在各自的子进程中处理,使得各个连接间的资源相互独立,避免了共享资源的竞争问题。
  3. 注意点:在实际部署中,频繁创建和销毁子进程可能会带来一定的开销,尤其是当客户端连接数量很大时。在某些情况下,可能选择多线程而非多进程的方式来处理并发连接,这取决于具体应用场景和性能要求。

  4. 在上述提供的TCP服务器类中,TcpServerstart方法中,在主进程每次接受到客户端连接请求并创建子进程后,都会关闭已接受的客户端连接套接字servicesock。这是因为主进程并不需要处理与已连接客户端的实际通信,这部分任务交由子进程完成。

    主进程关闭servicesock的原因:

    • 资源释放:每个文件描述符都是系统资源的一部分。在主进程关闭已接受的客户端连接套接字后,可以释放系统资源,以便主进程可以继续接受新的客户端连接,而不会因为文件描述符耗尽而导致无法创建新的连接。

    • 避免资源竞争:当主进程不关闭已连接的客户端套接字时,子进程和主进程之间会产生资源竞争,因为同一套接字在父子进程中同时存在,会导致难以预料的行为。

使用telnet客户端运行示例:

2、tcp_client.cc

这段代码是一个简单的TCP客户端程序,首先从命令行参数获取服务器的IP地址和端口号,然后创建一个TCP套接字并与服务器建立连接。接着,程序进入一个无限循环,循环中接收用户输入并通过套接字发送给服务器,并从服务器接收回显的数据。如果在任何环节出现错误(如创建套接字失败、连接服务器失败等),程序将打印错误信息并退出。

#include <iostream>
#include <string>
#include <unistd.h> // 提供Unix标准函数,如close、read、write等
#include <sys/socket.h> // 提供创建、操作套接字的函数原型
#include <arpa/inet.h> // 提供IPv4地址转换函数,如inet_addr、htonl等
#include <netinet/in.h> // 提供Internet地址家族(AF_INET)相关的结构和常量
#include <sys/types.h> // 提供通用的数据类型定义

// 使用示例:./tcp_client 目标IP 目标端口
void usage(std::string proc) 
{
    std::cout << "Usage: " << proc << " serverIp serverPort" << std::endl;
}

int main(int argc, char *argv[]) 
{
    // 检查命令行参数个数是否正确(IP地址+端口号)
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1); // 参数错误,退出程序
    }

    // 从命令行参数中提取目标服务器的IP地址和端口号
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]); // 将端口号字符串转换为整数

    // 创建一个基于IPv4的TCP套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    
    // 检查套接字创建是否成功
    if (sock < 0)
    {
        std::cerr << "socket create error" << std::endl;
        exit(2); // 套接字创建失败,退出程序
    }

    // 初始化服务器地址结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 清零内存区域
    server.sin_family = AF_INET; // 设置为IPv4协议
    server.sin_port = htons(serverport); // 将端口号转换为网络字节序
    server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 将IP地址字符串转换为网络字节序
    
    // 尝试连接服务器
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3); // 连接服务器失败,退出程序
    }

    // 进入通信循环,等待用户输入并向服务器发送数据,接收并显示服务器响应
    while (true)
    {
        std::string line;
        std::cout << "请输入# ";
        std::getline(std::cin, line); // 从标准输入读取一行文本
        send(sock, line.c_str(), line.size(), 0); // 发送消息至服务器
        
        // 准备接收服务器响应的缓冲区
        char buffer[1024];
        
        // 接收服务器数据
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        
        // 如果接收到数据
        if (s > 0)
        {
            buffer[s]=0; // 在有效数据后面添加结束符,方便当作字符串处理
            std::cout<<"server 回显# "<<buffer<<std::endl; // 输出服务器响应
        }
    }

    // 主程序结束,返回0表示正常退出
    return 0;
}

3、多进程版本二

class TcpServer
{
private:
    const static int gbacklog = 20; // 服务器监听队列长度(最大挂起连接数)

public:
    // 启动服务器:设置信号处理、循环接受客户端连接并创建子进程处理
    void start()
    {
        while (true)
        {
            struct sockaddr_in src; // 用于存储客户端地址信息的结构体
            socklen_t len = sizeof src;
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客户端连接请求
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(src.sin_port); // 获取客户端端口
            std::string client_ip = inet_ntoa(src.sin_addr); // 获取客户端IP地址

            logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
                        servicesock, client_ip.c_str(), client_port);
            
            // 多进程v2
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                close(listensock);
                if (fork() > 0 )// 子进程本身
                    exit(0); // 子进程本身立即退出
                // 孙子进程,孤儿进程,OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
                service(servicesock, client_ip, client_port);
            }
            // 父进程
            waitpid(id, nullptr, 0); // 不会阻塞!
            close(servicesock);
        }
    }
private:
    uint16_t _port; // 服务器监听端口
    std::string _ip; // 服务器绑定IP地址(可选)
    int listensock; // 服务器监听套接字
};

六、多线程版本

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp" // 自定义的日志记录模块
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>

// 静态函数,用于处理客户端连接的服务逻辑
// 参数:sock - 与客户端建立连接的套接字
//       clientip - 客户端IP地址字符串
//       clientport - 客户端端口号
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    char buffer[1024]; // 缓冲区,用于读取和发送数据
    while (true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // 从客户端接收数据
        if (s > 0)
        {
            buffer[s] = 0; // 添加字符串结束符
            std::cout << clientip << ":" << clientport << "#" << buffer << std::endl; // 输出接收到的数据和客户端信息
            write(sock, buffer, strlen(buffer)); // 将接收到的数据原样发送回客户端
        }
        else if (s == 0)
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport); // 如果读取到EOF,认为客户端已关闭连接
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno)); // 若读取发生错误,记录错误信息并断开连接
            break;
        }
    }
}

// 定义ThreadData类,用于传递给线程处理函数的参数
class ThreadData
{
public:
    int _sock; // 客户端连接套接字
    std::string _ip; // 客户端IP地址
    uint16_t _port; // 客户端端口号
};

// TCP服务器类
class TcpServer
{
private:
    const static int gbacklog = 20; // 服务器监听队列大小,表示能同时待处理的连接请求个数

    // 线程处理函数,负责处理客户端连接
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 确保线程结束后能够被内核回收资源
        ThreadData *td = static_cast<ThreadData *>(args);
        service(td->_sock, td->_ip, td->_port); // 调用service函数处理客户端连接
        delete td; // 删除ThreadData对象

        return nullptr;
    }

public:
    // 构造函数,接收服务器监听端口和可选的绑定IP地址
    TcpServer(uint16_t port, std::string ip = "")
        : listensock(-1), _port(port), _ip(ip)
    {}

    // 初始化服务器:创建套接字、绑定端口、监听连接
    void initServer()
    {
        listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
        if (listensock < 0)
        {
            logMessage(FATAL, "%d%s", errno, strerror(errno)); // 记录并输出错误信息
            exit(2); // 出错则退出程序
        }
        logMessage(NORMAL, "create socket success listensock: %d", listensock);

        struct sockaddr_in local; // 本地服务器地址结构体
        memset(&local, 0, sizeof local); // 清零结构体内容
        local.sin_family = AF_INET; // 设置为IPv4协议
        local.sin_port = htons(_port); // 设置服务器监听端口(主机字节序)
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 设置服务器绑定IP地址(若为空则监听所有地址)

        if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定套接字到指定地址和端口
        {
            logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
        if (listen(listensock, gbacklog) < 0) // 开始监听客户端连接,设置监听队列长度
        {
            logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success"); // 初始化服务器成功
    }

    // 启动服务器:设置信号处理、循环接受客户端连接并创建子线程处理
    void start()
    {
        // 设置信号处理,忽略SIGCHLD信号以自动回收子进程(这里是子线程)资源
        signal(SIGCHLD, SIG_IGN);

        while (true)
        {
            struct sockaddr_in src; // 客户端地址结构体
            socklen_t len = sizeof src; // 结构体大小

            int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客户端连接请求
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(src.sin_port); // 获取客户端端口(网络字节序转为主机字节序)
            std::string client_ip = inet_ntoa(src.sin_addr); // 获取客户端IP地址

            logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
                        servicesock, client_ip.c_str(), client_port);

            // 多线程处理客户端连接
            ThreadData *td = new ThreadData();
            td->_sock = servicesock;
            td->_ip = client_ip;
            td->_port = client_port;
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, td); // 创建新线程处理客户端连接

            // 注意:不应该在这里关闭servicesock,否则新创建的线程将无法继续通过该套接字与客户端通信
        }
    }

    // 析构函数
    ~TcpServer()
    {
        // 可在此处关闭监听套接字(如果需要的话),但通常在程序退出前由操作系统自动关闭所有打开的文件描述符
    }

private:
    uint16_t _port; // 服务器监听端口
    std::string _ip; // 服务器绑定IP地址(可选)
    int listensock; // 服务器监听套接字
};

七、线程池版本

 tcp_server.hpp

代码实现了一个可以并发处理多个客户端连接的TCP服务器,通过线程池调度不同的客户端连接任务,每个任务都在独立的线程中执行service函数以处理客户端的数据传输。服务器启动后,将在指定的端口上监听客户端连接,并在接收到连接请求时创建新的线程处理连接,从而实现高效、并发的通信服务。 

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

// 服务处理函数,负责处理客户端连接并回显数据
static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    char buffer[1024];
    while (true)
    {
        // 读取客户端发送的数据
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            // 结束字符串
            buffer[s] = 0;
            // 打印客户端信息及接收到的数据
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, closing connection...", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "Read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        // 将接收到的数据回写给客户端
        write(sock, buffer, strlen(buffer));
    }
    // 关闭已断开的客户端连接
    close(sock);
}

// TCP服务器类
class TcpServer
{
private:
    static const int gbacklog = 20; // 用于listen的连接请求队列长度

public:
    // 构造函数,初始化服务器监听端口与IP地址
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1), _port(port), _ip(ip),
          _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {
    }

    // 初始化服务器:创建socket,绑定地址,并开始监听
    void initServer()
    {
        // 1. 创建socket
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "Create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. 绑定socket到指定IP地址和端口
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "Bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 开始监听连接请求
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "Listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "Init server success");
    }

    // 启动服务器主循环,接受新连接并将它们分配给线程池
    void start()
    {
        // 忽略子进程结束时产生的SIGCHLD信号(避免产生僵尸进程)
        // signal(SIGCHLD, SIG_IGN);

        // 启动线程池
        _threadpool_ptr->run();

        // 主循环等待并处理新连接
        while (true)
        {
            // 4. 接受新的客户端连接请求
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "Accept error, %d:%s", errno, strerror(errno));
                continue;
            }

            // 获取客户端信息
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Link success, servicesock: %d | %s : %d |\n", servicesock, client_ip.c_str(), client_port);

            // 创建任务对象并将客户端连接放入线程池执行服务
            Task t(servicesock, client_ip, client_port, service);
            _threadpool_ptr->pushTask(t);
        }
    }

    // 析构函数
    ~TcpServer() {}

private:
    uint16_t _port;             // 监听端口号
    std::string _ip;            // 监听IP地址
    int _listensock;             // 服务器监听套接字
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 线程池指针
};
  1. 服务处理函数 service()

    • service函数接收四个参数:客户端套接字描述符(int sock)、客户端IP地址(const std::string &clientip)、客户端端口号(const uint16_t &clientport)和线程名称(const std::string &thread_name)。
    • 函数在一个无限循环中读取客户端发送的数据,并将接收到的数据回显给客户端。
    • 当读取到0字节(s == 0)时,表示客户端已关闭连接,服务器也关闭连接并退出循环。
    • 若读取过程中发生错误,记录错误信息并退出循环。
    • 通过write函数将接收到的数据回写给客户端,确保数据双向传输。
  2. TCP服务器类 TcpServer

    • 类内定义了服务器监听套接字描述符_listensock、监听的端口号_port、监听的IP地址_ip和一个线程池_threadpool_ptr
    • 构造函数初始化服务器对象,接收端口号和可选的IP地址。
    • initServer方法负责初始化服务器,包括创建套接字、绑定地址和开始监听连接请求。
    • start方法启动服务器主循环,首先启动线程池,然后不断地等待并处理新的客户端连接请求。当有新的连接请求时,通过accept函数接收连接,并创建一个Task对象,将客户端连接信息和service函数封装进去,然后将任务推送到线程池中执行。
    • 析构函数确保在服务器实例销毁时关闭监听套接字。

运行结果:

ThreadPool代码

lockGuard.hpp

#pragma once

#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx) : pmtx_(mtx){};
    void lock() { pthread_mutex_lock(pmtx_); }
    void unlock() { pthread_mutex_unlock(pmtx_); }
    ~Mutex() {}

private:
    pthread_mutex_t *pmtx_;
};

class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx) : mtx_(mtx)
    {
        mtx_.lock();
    }
    ~lockGuard()
    {
        mtx_.unlock();
    }

private:
    Mutex mtx_;
};

log.hpp

#pragma once

#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

// #define LOGFILE "./threadpool.log"

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);

    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

thread.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

typedef void *(*fun_t)(void *);

class ThreadData
{
public:
    void *args_;
    std::string name_;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void *args)
        : func_(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
        name_ = nameBuffer;
        tdata_.args_ = args;
        tdata_.name_ = name_;
    }
    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void *)&tdata_);
    }
    void join()
    {
        pthread_join(tid_, nullptr);
    }
    std::string name()
    {
        return name_;
    }
    ~Thread()
    {
    }

private:
    std::string name_;
    fun_t func_;
    ThreadData tdata_;
    pthread_t tid_;
};

threadPool.hpp

#pragma once
 
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"
 
// 定义默认线程数量
const int g_thread_num = 5;
 
// 类模板ThreadPool,代表一个线程池,可以处理不同类型的任务(T)
template <class T>
class ThreadPool
{
public:
    // 获取线程池内部使用的互斥锁
    pthread_mutex_t *getMutex()
    {
        return &lock;
    }
 
    // 判断任务队列是否为空
    bool isEmpty()
    {
        return task_queue_.empty();
    }
 
    // 线程等待条件变量
    void waitCond()
    {
        pthread_cond_wait(&cond, &lock);
    }
 
    // 从任务队列中取出并移除一个任务
    T getTask()
    {
        T t = task_queue_.front();
        task_queue_.pop();
        return t;
    }
 
private:
    // ThreadPool构造函数,初始化线程池,创建指定数量的工作线程
    ThreadPool(int thread_num = g_thread_num) : num_(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);
        for (int i = 1; i <= num_; i++)
        {
            threads_.push_back(new Thread(i, &ThreadPool::routine, this));
        }
    }
 
    // 删除拷贝构造函数和赋值操作符,避免线程池实例的拷贝
    ThreadPool(const ThreadPool<T> &other) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;
 
public:
    // 获取线程池的单例实例
    static ThreadPool<T> *getThreadPool(int num = g_thread_num)
    {
        // 使用双重检查锁定模式确保线程安全地初始化单例
        if (nullptr == thread_ptr) 
        {
            // 加锁
            lockGuard lockguard(&mutex);
 
            // 如果在加锁后仍然没有初始化,则创建一个新的线程池实例
            if (nullptr == thread_ptr)
            {
                thread_ptr = new ThreadPool<T>(num);
            }
 
            // 不需要显式解锁,因为lockGuard会在作用域结束时自动解锁
        }
        return thread_ptr;
    }
 
    // 启动线程池中的所有工作线程
    void run()
    {
        for (auto &iter : threads_)
        {
            iter->start();
            // 记录线程启动成功的日志消息
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }
 
    // 静态方法,作为工作线程的执行入口
    static void *routine(void *args)
    {
        // 解封装传入的参数
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;
 
        // 工作线程循环执行,直到收到终止信号
        while (true)
        {
            T task;
            
            // 上锁,同步访问任务队列
            {
                lockGuard lockguard(tp->getMutex());
 
                // 等待非空任务到来
                while (tp->isEmpty())
                    tp->waitCond();
 
                // 从任务队列中取出一个任务
                task = tp->getTask();
            }
 
            // 执行任务
            task(td->name_);
 
            // 这里假设任务完成后会自动重置循环条件,否则需要显式判断是否退出循环
        }
    }
 
    // 将新任务推送到线程池的任务队列中
    void pushTask(const T &task)
    {
        // 加锁,同步访问任务队列
        lockGuard lockguard(&lock);
 
        // 将任务放入队列,并通知条件变量,有一个新的任务可被处理
        task_queue_.push(task);
        pthread_cond_signal(&cond);
    }
 
    // 线程池析构函数,清理所有线程资源
    ~ThreadPool()
    {
        // 确保所有工作线程完成其任务后再销毁
        for (auto &iter : threads_)
        {
            iter->join();
            delete iter;
        }
 
        // 销毁互斥锁和条件变量
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }
 
private:
    // 存储工作线程实例的容器
    std::vector<Thread *> threads_;
    
    // 工作线程的数量
    int num_;
 
    // 任务队列,用于存放待执行的任务
    std::queue<T> task_queue_;
 
    // 单例实例指针
    static ThreadPool<T> *thread_ptr;
 
    // 用于保护线程池单例初始化的全局互斥锁
    static pthread_mutex_t mutex;
 
    // 用于控制线程同步的互斥锁
    pthread_mutex_t lock;
 
    // 条件变量,用于实现线程间的通信,如通知工作线程有新任务到来
    pthread_cond_t cond;
};
 
// 初始化静态成员变量
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
 
template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

八、实现回显、字符转换、在线字典查询服务

tcp_server.hpp

 三个服务函数

// tcp_server.hpp
#pragma once

// 引入必要的头文件,包括C++标准库和POSIX网络编程相关的头文件
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>

// 引入自定义的日志模块和线程池模块
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

// 定义三个静态服务函数,分别实现不同的客户端请求处理逻辑

// service函数:实现回显服务,从客户端接收数据并在控制台打印,同时将接收到的数据原样返回给客户端
static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    char buffer[1024];
    while (true)
    {
        // 从客户端读取数据
        ssize_t bytesReceived = read(sock, buffer, sizeof(buffer) - 1);
        
        if (bytesReceived > 0)
        {
            // 结束字符串,便于打印
            buffer[bytesReceived] = 0;
            
            // 打印客户端信息和接收到的消息
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;

            // 将接收到的消息原样写回给客户端
            write(sock, buffer, bytesReceived);
        }
        else if (bytesReceived == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else // 读取数据出错
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
    }

    // 关闭与客户端的连接
    close(sock);
}

// change函数:实现字符转换服务,将客户端发送的小写字母转换为大写后返回
static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    char buffer[1024];
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;

        // 转换输入字符串中小写字母为大写
        std::string convertedMessage;
        for (char *c = buffer; *c; ++c)
            convertedMessage.push_back(islower(*c) ? toupper(*c) : *c);

        // 将转换后的消息写回给客户端
        write(sock, convertedMessage.c_str(), convertedMessage.size());
    }
    else if (s == 0)
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    // 关闭与客户端的连接
    close(sock);
}

// dictOnline函数:实现在线字典查询服务,根据客户端发送的单词查询预定义字典并返回结果
static void dictOnline(int sock, const std::string &clientip,
                       const uint16_t &clientport, const std::string &thread_name)
{
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dictionary = {
        {"producer", "生产者"},
        {"consumer", "消费者"},
        {"udp", "用户数据报协议"},
        {"tcp", "传输控制协议"},
        {"http", "超文本传输协议"}
    };

    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;

        // 查找字典中是否存在该单词及其对应含义
        std::string response;
        auto it = dictionary.find(buffer);
        if (it == dictionary.end())
            response = "我不知道...";
        else
            response = it->second;

        // 将查询结果写回给客户端
        write(sock, response.c_str(), response.size());
    }
    else if (s == 0)
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    // 关闭与客户端的连接
    close(sock);
}
  1. service函数

    • service函数负责实现最基础的回显服务,即服务器接收到客户端发送的数据后,原样返回给客户端。
    • 首先,函数通过read系统调用从给定的套接字sock中读取客户端发送的数据,存储在缓冲区buffer中。
    • 当读取到有效数据时(read返回值大于0),将在控制台上打印客户端的IP地址、端口号以及发送的消息,并将接收到的消息原样通过write系统调用返回给客户端。
    • read返回值为0,表示客户端已经关闭连接,服务端也会相应地关闭连接。
    • 若出现读取错误(read返回负数),函数将记录错误日志,并关闭连接。
  2. change函数

    • change函数实现了一个简单的字符转换服务,将客户端发送的所有小写字母转换成大写字母后再发送回去。
    • 读取客户端数据的过程与service函数相同,但在读取之后,函数遍历接收到的字符,利用islowertoupper函数将小写字母转换为大写字母,然后构建一个新的字符串convertedMessage
    • 最后,将转换后的大写字符串发送回给客户端。
  3. dictOnline函数

    • dictOnline函数实现了在线字典查询服务,允许客户端发送一个单词请求,服务器在其内部维护的一个预定义字典(这里是通过std::unordered_map实现)中查找该单词的含义。
    • 类似地,先通过read读取客户端发送的单词。
    • 查找字典中是否存在该单词,若存在,则将对应的含义发送回给客户端;若不存在,则返回一个默认提示信息。
    • 注意这里字典是静态局部变量,因此在整个函数生命周期内只初始化一次,提高了效率。

TcpServer类

class TcpServer
{
private:
    // 设置服务器可挂起的最大连接数
    const static int gbacklog = 20;

public:
    // 构造函数,初始化服务器监听端口和IP地址,默认监听所有网络接口(0.0.0.0)
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1), _port(port),
          _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {
    }

    // 初始化服务器,包括创建socket、绑定端口/IP、设置监听
    void initServer()
    {
        // 1. 创建socket,AF_INET代表IPv4,SOCK_STREAM代表TCP协议
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "Failed to create socket, error: %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "Created socket successfully, listensock: %d", _listensock);

        // 2. 绑定socket到指定IP地址和端口
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "Failed to bind socket, error: %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 设置监听,允许最多gbacklog个连接排队
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "Failed to listen on socket, error: %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "Initialized server successfully");
    }

    // 启动服务器并开始接受客户端连接
    void start()
    {
        // 启动线程池
        _threadpool_ptr->run();

        while (true)
        {
            // 4. 等待并接受来自客户端的连接请求
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "Failed to accept connection, error: %d:%s", errno, strerror(errno));
                continue;
            }

            // 获取已连接客户端的IP地址和端口
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "Accepted connection, servicesock: %d | %s : %d |\n", servicesock, client_ip.c_str(), client_port);

            // 根据需求选择不同的服务函数,并将其封装为Task对象,推送到线程池中处理
            Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);
        }
    }

    // 析构函数,确保资源正确释放
    ~TcpServer() {}

private:
    // 服务器监听的端口号
    uint16_t _port;

    // 服务器监听的IP地址
    std::string _ip;

    // 服务器监听用的套接字描述符
    int _listensock;

    // 线程池实例,用于并发处理客户端连接请求
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

tcp_server.cc

#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

tcp_client.cc

tcp_client.cc 是一个简单的TCP客户端程序,它通过命令行参数获取服务器的IP地址和端口号,然后尝试与服务器建立连接,并进行交互。

// tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 定义一个帮助函数,用于输出程序的使用说明
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// 主函数,接收命令行参数:服务器IP地址和端口号
int main(int argc, char *argv[])
{
    // 检查命令行参数数量是否正确(应为3个,包括程序名本身)
    if (argc != 3)
    {
        // 如果参数数量不正确,则输出使用说明并退出程序
        usage(argv[0]);
        exit(1);
    }

    // 获取命令行参数中的服务器IP地址和端口号
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    // 客户端状态标志,标识当前客户端是否已连接至服务器
    bool alive = false;

    // 客户端套接字描述符
    int sock = 0;

    // 用于暂存用户输入的行数据
    std::string line;

    // 主循环,持续监听用户输入并与其进行交互
    while (true)
    {
        // 如果当前没有与服务器建立连接,则尝试创建并建立连接
        if (!alive)
        {
            // 创建一个AF_INET协议族下的SOCK_STREAM类型套接字(即TCP套接字)
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }

            // 客户端无需bind到本地地址,操作系统会自动为其分配一个可用的源端口
            // 准备服务器的地址结构体
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));

            // 设置地址族为IPv4,端口号转换为主机字节序
            server.sin_family = AF_INET;
            server.sin_port = htons(serverport);

            // 将服务器IP地址转换为二进制格式
            server.sin_addr.s_addr = inet_addr(serverip.c_str());

            // 尝试连接到服务器
            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3); 
            }

            // 输出连接成功的提示
            std::cout << "connect success" << std::endl;

            // 设置alive标志为真,表明已连接至服务器
            alive = true;
        }

        // 提示用户输入,并读取一行
        std::cout << "请输入# ";
        std::getline(std::cin, line);

        // 如果用户输入"quit",则跳出循环,结束客户端程序
        if (line == "quit")
            break;

        // 将用户输入的数据发送给服务器
        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if (s > 0)
        {
            // 接收服务器的回应数据
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);

            // 如果接收到数据长度大于0,则输出服务器的回显内容
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
            }
            // 若接收的数据长度为0,通常意味着服务器关闭了连接,此时客户端也需要关闭连接并重置alive标志
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        // 发送数据失败时,同样关闭连接并重置alive标志
        else
        {
            alive = false;
            close(sock);
        }
    }

    // 关闭套接字并退出程序
    return 0;
}
  1. main函数入口

    • 检查命令行参数的数量是否为3,如果不是则输出帮助信息并退出程序。
    • 解析服务器的IP地址和端口号。
    • 进入一个无限循环,持续尝试或保持与服务器的连接。
  2. 建立TCP连接

    • 如果当前没有活跃的连接(alive为false),则创建一个TCP套接字(socket),使用AF_INET表示IPv4协议,SOCK_STREAM表示使用TCP协议。
    • 不需要客户端显示地bind本地端口,操作系统会自动分配一个可用端口。
    • 填充sockaddr_in结构体,设置服务器的IP地址和端口号。
    • 使用connect函数尝试连接服务器,如果连接成功,则设置alive为true,并输出“connect success”。
  3. 用户交互

    • 在循环中,提示用户输入消息,并通过getline读取一行文本。
    • 如果用户输入的是"quit",跳出循环,结束程序。
    • 发送用户输入的消息到服务器,使用send函数。
    • 接收服务器的回复,使用recv函数,并将接收到的数据打印出来作为服务器的回显。
  4. 错误处理与重连机制:若sendrecv过程中发生错误,将alive设置为false,关闭套接字(close(sock)),进入下一轮循环重新尝试连接服务器。

通过这个TCP客户端程序,用户可以向指定服务器发送消息并接收服务器的回复,直到用户选择退出程序。在每次交互中,客户端都会检查网络连接的状态,确保在连接断开时能够尝试重新连接。

九、TCP协议通讯流程

1、服务器初始化

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来;

2、建立连接的过程(三次握手

  • 创建套接字: 客户端同样通过socket()系统调用创建一个新的套接字,生成用于与服务器通信的文件描述符。

  • 发起连接请求: 调用connect()函数,向服务器的IP地址和端口发起连接请求。此操作会引发TCP的“三次握手”过程:

  • 第一次握手: 客户端发送一个带有SYN(同步序列编号)标志的TCP报文段,该报文段包含一个随机生成的初始序列号(ISN)。此时,客户端进入SYN_SENT状态,等待服务器的确认。

  • 第二次握手: 服务器接收到客户端的SYN报文段后,回应一个SYN-ACK(同步确认)报文段。该报文段不仅确认了客户端的SYN(设置ACK标志),还包含了服务器自己的初始序列号。服务器进入SYN_RCVD状态。

  • 第三次握手: 客户端收到服务器的SYN-ACK报文段后,发送一个ACK(确认)报文段,确认服务器的SYN(设置ACK标志并使用服务器的ISN+1作为确认序列号)。至此,客户端和服务器双方均确认了对方的初始序列号,连接建立成功,客户端进入ESTABLISHED状态,服务器也从SYN_RCVD状态切换到ESTABLISHED状态。

经过上述 三次握手过程,客户端与服务器之间的TCP连接正式建立,双方可以开始进行双向的数据传输。在整个过程中, connect()函数在客户端一侧会阻塞,直到三次握手完成或发生错误。而在服务器一侧,通常通过 accept()函数阻塞等待客户端的连接请求,并在接收到有效连接请求后返回一个新的已连接套接字供后续通信使用。

3、数据传输的过程

  • 在数据传输过程中,TCP(Transmission Control Protocol)协议作为互联网层与应用层之间的关键桥梁,为网络通信提供了可靠、有序且面向连接的全双工服务。全双工模式意味着,在同一条TCP连接上,通信双方能够在同一时刻独立地进行数据的发送与接收,犹如两条并行的高速公路,使得信息能够在双向通道中同时流动,显著提升了通信效率。
  • 当服务器通过accept()系统调用成功接纳一个客户端的连接请求后,一个新的TCP连接便正式建立起来。此时,服务器进入待命状态,立即调用read()函数尝试从该连接的socket中读取数据。这个过程就如同守候在一个信息管道入口,若此时客户端尚未发送任何数据,服务器端的read()函数会暂时陷入阻塞状态,耐心等待数据流的到来。
  • 与此同时,客户端在连接建立后,开始执行其业务逻辑,调用write()函数向服务器发送请求。这些请求数据沿着已建立的TCP连接,如同信使般穿越网络,准确无误地送达服务器端。服务器的read()函数感知到数据到达,立即解除阻塞状态,从socket中取出客户端的请求进行处理。
  • 在服务器专心处理客户端请求的同时,客户端并不闲着,它调用read()函数进入阻塞状态,静候服务器对请求的响应。这种同步阻塞模式确保了客户端能够及时接收到服务器端的反馈,保持通信的连贯性。
  • 服务器完成请求处理后,通过write()函数将处理结果打包成数据包,沿原路返回给客户端。如同投递员将信件放入邮筒,这些结果数据被安全、高效地传递至客户端的socket。发送完毕后,服务器再次调用read()函数,重新进入阻塞等待状态,准备接收客户端可能发出的下一条请求。

4、断开连接的过程(四次挥手)

  • 当客户端完成所有业务交互,不再有新的请求需要发送时,它会选择主动发起连接的关闭流程,即所谓的“断开连接”。客户端通过调用close()函数,向服务器发送一个特殊的TCP控制报文——FIN(Finish,结束)标志位被置为1的报文段。这标志着客户端已经没有数据要发送,期望结束该连接。这是断开连接过程中的第一次“挥手”。
  • 服务器端在接收到客户端发送的FIN报文后,立即做出响应。它会向客户端发送一个ACK(Acknowledgment,确认)报文段,确认序号为收到的FIN报文的序号加1,表明已正确接收并理解了客户端的断开意图。与此同时,服务器端的read()函数会返回值0,这是一个重要信号,提示应用程序客户端已关闭写入,即不会再有新的数据到来。这是断开连接过程中的第二次“挥手”。
  • 服务器端应用程序在得知客户端关闭连接后,通常会进行必要的清理工作,如释放资源、更新状态等,然后调用自身的close()函数,向客户端发送一个FIN报文段,明确告知其服务器也已完成数据发送,希望关闭连接。这是断开连接过程中的第三次“挥手”。
  • 最后,客户端收到服务器发送的FIN报文后,同样以一个ACK报文段作为回应,确认序号为收到的FIN报文的序号加1,表示已知悉服务器关闭连接的信息。至此,双方均确认对方已无数据待发送,且都同意关闭连接,断开连接的四次“挥手”过程宣告完成。这就是网络通信中著名的“四次挥手”(Four-way Handshake)机制,它确保了TCP连接能够有序、可靠地关闭,避免了数据丢失或混乱,保障了网络环境的稳定性和效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1609812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TorchEEG文档_学习笔记1【代码详解】

文章目录 一、用户文档1.安装Pytorch2.安装TorchEEG3.安装与图算法的插件 二、教程1.使用TorchEEG完成深度学习工作流程2datasets模块3.transforms模块4.models模块5.trainer模块6.使用Vanilla PyTorch训练模型 一、用户文档 1.安装Pytorch TorchEEG依赖于PyTorch,根据系统、…

PTA L2-047 锦标赛

题目 解析 把每一场比赛看作满二叉树的一个节点&#xff0c;父节点递归遍历子节点的结果&#xff0c;进行试填。 代码 #include <bits/stdc.h>using i64 long long;struct Node {int win, lose; };void solve() {int k;std::cin >> k;int siz (1 << k);…

常见UI组件(二)

一、文本输入 1.1 概述 TextInput为文本输入组件&#xff0c;用于接收用户输入的文本内容 1.2 参数 Entry Component struct Index {build() {Column({space : 50}) {TextInput({placeholder:请输入用户名}).width(70%)TextInput({text:当前内容}).width(70%)}.width(100%).…

基于springboot+vue+Mysql的广场舞团管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

Java | Leetcode Java题解之第38题外观数列

题目&#xff1a; 题解&#xff1a; class Solution {public String countAndSay(int n) {String[] arr {"","1","11","21","1211","111221","312211","13112221","1113213211",…

六、项目发布 -- 4. 电子书详情页API开发、电子书列表API开发

电子书详情页API的编写 同理如下app.get中路由、回调&#xff1b;回调中要连接数据库、接收前端传过来的值、到数据库中做查询&#xff0c;然后回调&#xff08;如果回调失败返回什么JSON&#xff0c;如果回调成功返回什么JSON&#xff09;&#xff1b;最后千万别忘记了关闭数…

面试:java中常见的锁

文章目录 概述并发编程三大特性java中常见的锁synchronized关键字&#xff08;隐式锁&#xff09;ReentrantLock 类ReadWriteLock 接口StampedLock 类 小结 概述 在Java中&#xff0c;锁是用于控制多个线程对共享资源的访问的一种机制。它有两个主要作用&#xff1a; 保证线程…

Pyqt5中设置matplotlib绘图区背景透明

Pyqt5中设置matplotlib绘图区背景透明 一、前言 最近组里接了学校的某项目&#xff0c;作者是团队里面负责做前端展示的&#xff0c;但是说实话&#xff0c;感觉QT做前端展示真不是很方便&#xff0c;开发过程中遇到不少棘手的问题。 其中一个卡了我一段时间的是界面画图表时…

ASP.NET基于Web的招投标系统的设计与实现

摘 要 招标拍卖的历史悠久&#xff0c;在近两千年的发展历程中&#xff0c;人们对拍卖的理论和技术做了大量的探讨。随着计算机网络技术的迅猛发展和日益成熟&#xff0c;为了提高招投标及采购工作的效率&#xff0c;为廉政建设和防止腐败提供技术保障&#xff0c;传统的拍…

计算机科学与导论 第十六章 安全

文章预览&#xff1a; 16.1引言16.1.1 安全目标16.1.2 攻击 16.2机密性16.2.1 对称密钥密码术16.2.2 非对称密钥密码术 16.1引言 为了安全&#xff0c;信息需要避开未授权的使用(机密性),保护信息不受到未授权的篡改(完整性)&#xff0c;并且对于得到授权的实体来说是需要时可…

PSAvatar:一种基于点的可变形形状模型,用于3D高斯溅射的实时头部化身创建

PSAvatar: A Point-based Morphable Shape Model for Real-Time Head Avatar Creation with 3D Gaussian Splatting PSAvatar&#xff1a;一种基于点的可变形形状模型&#xff0c;用于3D高斯溅射的实时头部化身创建 Zhongyuan Zhao1,2, Zhenyu Bao1,2, Qing Li1, Guoping Qiu3,…

OpenWrt One/AP-24.XY 开源路由器发布,OpenWRT与Banana Pi社区合作

OpenWrt One/AP-24.XY 开源路由器 2024 年&#xff0c;OpenWrt 项目将迎来20 周年&#xff01;OpenWrt 开源社区官方通过推出社区自己的第一个完全上游支持的硬件设计来庆祝这一周年纪念日。并与联发科&#xff0c;Banana Pi开源社区紧密合作&#xff0c;共同完成硬件的设计与…

运行django

确保app被注册 urls.py中编写url 视图对应关系 命令行启动 python manage.py runserver

图数据库Neo4J入门——Neo4J下载安装+Cypher基本操作+《西游记》人物关系图实例

这里写目录标题 一、效果图二、环境准备三、数据库设计3.1 人物节点设计3.2 关系设计 四、操作步骤4.1 下载、安装、启动Neo4J服务4.1.1 配置Neo4J环境变量4.1.2 启动Neo4J服务器4.1.3 启动Ne04J客户端 4.2 创建节点4.3 创建关系&#xff08;从已有节点创建关系&#xff09;4.4…

esp32 rst:0xf (BROWNOUT_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)

供电不足导致的 1、第1次出现 电脑usb电压不够&#xff0c;只有3.8V&#xff0c;给esp32s3供电&#xff0c;然后esp32带的模块太多&#xff0c;运行启动就一直报错 2、第2次出现 esp32带的模块单独电池供电&#xff0c;然后一运行MAX98357播放声音就重启 然后换模块单独充电…

月球地形数据介绍(LOLA)

月球地形数据介绍 LOLA介绍LOLA数据的处理与发布数据类型和格式投影坐标系SIMPLE CYLINDRICALPOLAR STEREOGRAPHIC 数据下载与浏览 LOLA介绍 目前最新的月球地形高程数据来源于美国2009年发射的LRO探测器。 “月球勘测轨道器”(Lunar Reconnaissance Orbiter&#xff0c;LRO)…

给一个新项目配置conda环境的完整流程,安装pytorch,dgl

创建环境&#xff0c;并指定python的版本&#xff0c;我这边指定为3.7&#xff1a; conda create --name [自定义的环境名] python3.7我这边假定我的环境名为grand&#xff1a; conda create --name grand python3.7创建成功后&#xff0c;初始化一下conda&#xff1a; source …

easyx库的介绍

前言 如果想要摆脱黑窗口的限制那么easyx图形库是一个好的选择 easyx的初认识 easyx是针对c的图形库&#xff0c;可以帮助c/c上手图形和游戏编程 所以要用easyx必须要用.cpp的后缀 1 easyx的原理 window的图形编程&#xff0c;最终都由window的底层API来实现 2 easyx的颜色 …

2010年认证杯SPSSPRO杯数学建模B题(第一阶段)交通拥堵问题全过程文档及程序

2010年认证杯SPSSPRO杯数学建模 交通拥堵问题 B题 Braess 悖论 原题再现&#xff1a; Dietrich Braess 在 1968 年的一篇文章中提出了道路交通体系当中的Braess 悖论。它的含义是&#xff1a;有时在一个交通网络上增加一条路段&#xff0c;或者提高某个路段的局部通行能力&a…

CRMEB PRO安装系统配置清单

统在安装完成之后&#xff0c;需要对系统进行一系列的配置&#xff0c;才能正常使用全部的功能&#xff0c;以下是官方整理的配置清单