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

目录

一、相关函数 

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 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"};#define LOGFILE "./threadpool.log"void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif (level == DEBUG)return;
#endifchar 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; // 设置协议族为IPv4local.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; // 设置协议族为IPv4local.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);// 多进程v2pid_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 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"};// #define LOGFILE "./threadpool.log"void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif (level == DEBUG)return;
#endifchar 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 = "我不知道...";elseresponse = 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.mzph.cn/diannao/863.shtml

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

相关文章

ColBERT和ColBERTv2:兼具Bi-encoder和cross-encoder优势的多向量排序模型

文章目录 简介ColBERTColBert 原理ColBERT如何训练ColBERT 如何使用离线索引用ColBERT 实现top-k Re-ranking用ColBERT 实现top-k 端到端的检索 ColBERTv2ColBERTv2原理SupervisionRepresentation IndexingRetrieval 总结参考资料 简介 ColBERT是一种多向量排序模型&#xff0…

数据分析案例-中国黄金股票市场的EDA与价格预测

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

Redis的Stream 和 实现队列的方式【List、SortedSet、发布订阅、Stream、Java】

Redis队列与Stream、Redis 6多线程详解 Redis队列与StreamStream总述常用操作命令生产端消费端单消费者消费组消息消费 Redis队列几种实现的总结基于List的 LPUSHBRPOP 的实现基于Sorted-Set的实现PUB/SUB&#xff0c;订阅/发布模式基于Stream类型的实现与Java的集成 消息队列问…

算法打卡day39

今日任务&#xff1a; 1&#xff09;卡码网57. 爬楼梯&#xff08;70. 爬楼梯进阶版&#xff09; 2&#xff09;322.零钱兑换 3&#xff09;279.完全平方数 4&#xff09;复习day14 卡码网57. 爬楼梯&#xff08;70. 爬楼梯进阶版&#xff09; 题目链接&#xff1a;57. 爬楼梯…

ipv4Bypass:一款基于IPv6实现的IPv4安全绕过与渗透测试工具

关于ipv4Bypass ipv4Bypass是一款基于IPv6实现的安全绕过与渗透测试工具&#xff0c;该工具专为红队研究人员设计&#xff0c;可以帮助广大研究人员通过IPv6绕过目标安全策略&#xff0c;以此来检测安全检测机制的健壮性。 20世纪90年代是互联网爆炸性发展时期&#xff0c;随着…

Llama 3王者归来,可与GPT-4分庭抗礼,开源模型即将追上闭源模型了?

“有史以来最强大的开源大模型”Llama 3引爆AI圈&#xff0c;马斯克点赞&#xff0c;英伟达高级科学家Jim Fan直言&#xff0c;Llama 3将成为AI大模型发展历程的“分水岭”&#xff0c;AI顶尖专家吴恩达称Llama3是他收到的最好的礼物。 4月18日&#xff0c;AI圈再迎重磅消息&a…

写一个uniapp的登录注册页面

目录 一、效果图 二、代码 1、登录 &#xff08;1&#xff09;页面布局代码 &#xff08;2&#xff09;逻辑实现代码 &#xff08;3&#xff09;css样式 2、注册 &#xff08;1&#xff09;页面布局代码 &#xff08;2&#xff09;逻辑实现代码 &#xff08;3&#x…

一个完全用rust写的开源操作系统-Starry

1. Starry Starry是2023年全国大学生计算机系统能力大赛操作系统设计赛-内核实现赛的二等奖作品。Starry是在组件化OS的arceos的基础上&#xff0c;进行二次开发的操作系统内核&#xff0c;使用宏内核架构&#xff0c;能够运行Linux应用的内核。 原始的操作系统大赛的仓库为 …

51-42 NÜWA:女娲,统一的多模态预训练模型

21年11月&#xff0c;微软、北大联合发布了NUWA模型&#xff0c;一个统一的多模态预训练模型&#xff0c;在 8 个下游任务上效果惊艳。目前该项目已经发展成为一系列工作&#xff0c;而且都公开了源代码。 Abstract 本文提出了一种统一的多模态预训练模型N̈UWA&#xff0c;该…

【精简改造版】大型多人在线游戏BrowserQuest服务器Golang框架解析(1)——功能清单

1.匿名登录 2.服务连接 3.新手引导 4.随机出生点 5.界面布局 6.玩法帮助 7.NPC会话 8.成就系统 9.成就达成 10.用户聊天 11.战斗&信息展示 12.药水使用 13.副本传送 14.玩家死亡 15.超时断开

实验:使用FTP+yum实现自制yum仓库

实验准备 FTP服务器端&#xff1a;centos-1&#xff08;IP:10.9.25.33&#xff09; 客户端&#xff1a;centos-2 两台机器保证网络畅通&#xff0c;原yum仓库可用&#xff0c;已关闭防火墙和selinux FTP服务器端 ①安装vsftpd并运行&#xff0c;设定开机自启动 安装vsftpd…

金融数字化能力成熟度指引

1 范围 本文件提出了金融数字化能力成熟度模型、成熟度计算方法&#xff0c;明确了不同维度金融数字化转型能力 相应的分档要求。 本文件适用于金融机构衡量金融科技应用和数字化转型发展水平&#xff0c;检视自身数字化发展优势与短板&#xff0c; 加快数字化转型&#xff0c…

金蝶云星空和金蝶云星空单据接口对接

金蝶云星空和金蝶云星空单据接口对接 来源系统:金蝶云星空 金蝶K/3Cloud结合当今先进管理理论和数十万家国内客户最佳应用实践&#xff0c;面向事业部制、多地点、多工厂等运营协同与管控型企业及集团公司&#xff0c;提供一个通用的ERP服务平台。K/3Cloud支持的协同应用包括但…

Linux Makefile

1.开发背景 linux 下编译程序需要用到对应的 Makefile&#xff0c;用于编译应用程序。 2.开发需求 编写 Makefile 编译应用程序 1&#xff09;支持多个源文件 2&#xff09;支持多个头文件 3&#xff09;支持只编译修改的文件&#xff0c;包括源文件和头文件 4&#xff09;支持…

Web程序设计-实验03 JavaScript语言基础

题目 【实验主题】 素数问题求解。计算&#xff08;判断&#xff09; 1~100中哪些是素数、哪些是合数。 素数也称为质数&#xff0c;是只能被1及其自身整除的自然数。与素数相对应的是合数&#xff0c;合数可以被分解为若干个素数的乘积&#xff0c;这些素数称为这个合数的质…

数据结构从入门到实战——顺序表的应用

目录 一、基于动态顺序表实现通讯录 二、代码实现 2.1 通讯录的初始化 2.2 通讯录的销毁 2.3 通讯录的展示 2.4 通讯录添加联系人信息 2.5 通讯录删除联系人信息 2.6 通讯录修改联系人信息 2.7 通讯录的查找联系人信息 2.8 将通讯录中联系人信息保存到文件中 2.9…

【Windows10】Anaconda3安装+pytorch+tensorflow+pycharm

文章目录 一、下载anaconda0.双击下载的文件1. 选择All users2. 安装路径3. 勾选环境变量和安装python4.安装完成5.添加环境变量6.测试是否安装成功 二、安装pytorch&#xff08;先看四&#xff01;先检查一下自己电脑是不是只能安装GPU版的1.查看conda图形化界面2.在安装pytor…

【图解计算机网络】网络协议分层解析

网络协议分层解析 网络协议分层应用层传输层网络层数据链路层 TCP/IP分层模型通讯示例 网络协议分层 网络协议分层一共有OSI七层网络协议&#xff0c;TCP/IP四层网络网络协议&#xff0c;还有五层网络协议。 七层由于分层太多过于复杂&#xff0c;实际应用中并没有使用&#x…

代码编辑工具PilotEditPro18.4版本在Windows系统的下载与安装配置

目录 前言一、PilotEdit Pro安装二、使用配置总结 前言 “ PilotEdit Pro是一个功能强大且功能丰富的文本和代码编辑器&#xff0c;可满足程序员、开发人员和IT专业人员的不同需求。定位为一个多功能的编辑解决方案&#xff0c;PilotEdit Pro以其对广泛的文本和代码文件格式的…

Nginx莫名奇妙返回了404

描述 nginx作为反向代理&#xff0c;代理python的服务&#xff0c;但是通过代理访问服务的时候&#xff0c;报了404的错误。 难受的是客户现场没有查看日志的权限&#xff0c;只有查看配置文件的权限&#xff0c;我们检测了几遍配置文件也没有找到问题&#xff0c;哎~ 问题引…