目录
一、相关函数
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客户端会执行以下动作:
-
发起连接请求:
connect
函数会触发TCP三次握手的过程,即客户端发送一个SYN(同步)分节给服务器,请求建立连接。 -
等待响应:客户端会等待服务器回应SYN+ACK分节,然后发送ACK(确认)分节作为响应。
-
连接建立:一旦三次握手成功完成,连接就建立了,此时套接字的状态转变为
ESTABLISHED
,客户端可以在该套接字上进行读写操作。 -
错误处理:如果在一定时间内没有收到服务器的响应,或者由于其他原因无法建立连接(比如网络问题、服务器拒绝连接等),
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
中。成功连接后,应用程序就可以通过write
、read
或其他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是无连接的协议,所以在发送数据时需要指定目的地址,因此使用
- 数据接收:
- 对应地,在接收UDP数据时,不仅要接收数据,还需要得到发送数据的源地址和端口号,因此使用
recvfrom()
函数,它不仅能返回接收到的数据,还能填充提供给它的sockaddr
结构体。
- 对应地,在接收UDP数据时,不仅要接收数据,还需要得到发送数据的源地址和端口号,因此使用
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服务器。该服务器具有以下功能:
-
构造函数:
TcpServer(uint16_t port, std::string ip = "")
:初始化服务器对象,接收一个监听端口号port
和一个可选的服务器绑定IP地址ip
。默认情况下,如果不提供IP地址,服务器将在所有可用网络接口上监听。
-
initServer():
- 创建TCP套接字。
- 使用
struct sockaddr_in
结构体存储服务器地址信息。 - 绑定服务器套接字到指定的IP地址和端口号。
- 开始监听客户端的连接请求,并设置监听队列的最大长度为
gbacklog
(默认20)。
-
service()静态函数:
- 用于处理与单个客户端的连接服务逻辑。
- 通过
read()
函数读取客户端发送过来的数据,然后回显到控制台。 - 若读取到的数据长度为0,表示客户端关闭连接,服务器也结束对该客户端的服务。
- 若读取过程中发生错误,则记录错误并结束服务。
- 使用
write()
函数将读取到的数据回传给客户端。
-
start():
- 设置信号处理,忽略
SIGCHLD
信号,这样操作系统会在子进程结束后自动回收资源。 - 服务器进入无限循环,不断地通过
accept()
函数接受新的客户端连接请求。 - 当接收到新的连接请求时,获取客户端的IP地址和端口号,并调用
service()
函数处理客户端连接。 - 处理完客户端连接后,关闭已接受的客户端连接套接字。
- 设置信号处理,忽略
-
析构函数:
- 类似于其他类的析构函数,
~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.com
的80
端口是否开放,可以执行:
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()
系统调用创建子进程来处理每个客户端连接。以下是不同之处的详细说明:
-
启动服务器(start()方法)的变化:
-
在
start()
方法内,当服务器通过accept()
函数成功接受一个客户端连接后,调用fork()
创建一个子进程。 -
子进程中:
- 关闭监听套接字(
listensock
),因为它仅用于监听新的连接请求,无需在处理现有客户端连接的子进程中保持打开。 - 调用
service()
函数处理客户端连接。 - 在
service()
函数结束后,子进程调用exit(0)
退出,释放资源。
- 关闭监听套接字(
-
主进程中:同样关闭已接受的客户端连接套接字,但在主进程中这样做是为了让主进程能够继续监听新的客户端连接,而不是去处理已连接的客户端通信。
-
-
多进程处理客户端连接的优势:
- 并发处理:父进程可以继续接受新的客户端连接请求,而子进程独立处理已连接的客户端,从而实现并发处理多个客户端连接。
- 资源隔离:每个客户端连接都在各自的子进程中处理,使得各个连接间的资源相互独立,避免了共享资源的竞争问题。
-
注意点:在实际部署中,频繁创建和销毁子进程可能会带来一定的开销,尤其是当客户端连接数量很大时。在某些情况下,可能选择多线程而非多进程的方式来处理并发连接,这取决于具体应用场景和性能要求。
-
在上述提供的TCP服务器类中,
TcpServer
的start
方法中,在主进程每次接受到客户端连接请求并创建子进程后,都会关闭已接受的客户端连接套接字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; // 线程池指针
};
-
服务处理函数 service():
service
函数接收四个参数:客户端套接字描述符(int sock
)、客户端IP地址(const std::string &clientip
)、客户端端口号(const uint16_t &clientport
)和线程名称(const std::string &thread_name
)。- 函数在一个无限循环中读取客户端发送的数据,并将接收到的数据回显给客户端。
- 当读取到0字节(
s == 0
)时,表示客户端已关闭连接,服务器也关闭连接并退出循环。 - 若读取过程中发生错误,记录错误信息并退出循环。
- 通过
write
函数将接收到的数据回写给客户端,确保数据双向传输。
-
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);
}
-
service函数:
service
函数负责实现最基础的回显服务,即服务器接收到客户端发送的数据后,原样返回给客户端。- 首先,函数通过
read
系统调用从给定的套接字sock
中读取客户端发送的数据,存储在缓冲区buffer
中。 - 当读取到有效数据时(
read
返回值大于0),将在控制台上打印客户端的IP地址、端口号以及发送的消息,并将接收到的消息原样通过write
系统调用返回给客户端。 - 若
read
返回值为0,表示客户端已经关闭连接,服务端也会相应地关闭连接。 - 若出现读取错误(
read
返回负数),函数将记录错误日志,并关闭连接。
-
change函数:
change
函数实现了一个简单的字符转换服务,将客户端发送的所有小写字母转换成大写字母后再发送回去。- 读取客户端数据的过程与
service
函数相同,但在读取之后,函数遍历接收到的字符,利用islower
和toupper
函数将小写字母转换为大写字母,然后构建一个新的字符串convertedMessage
。 - 最后,将转换后的大写字符串发送回给客户端。
-
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;
}
-
main函数入口:
- 检查命令行参数的数量是否为3,如果不是则输出帮助信息并退出程序。
- 解析服务器的IP地址和端口号。
- 进入一个无限循环,持续尝试或保持与服务器的连接。
-
建立TCP连接:
- 如果当前没有活跃的连接(
alive
为false),则创建一个TCP套接字(socket
),使用AF_INET
表示IPv4协议,SOCK_STREAM
表示使用TCP协议。 - 不需要客户端显示地
bind
本地端口,操作系统会自动分配一个可用端口。 - 填充
sockaddr_in
结构体,设置服务器的IP地址和端口号。 - 使用
connect
函数尝试连接服务器,如果连接成功,则设置alive
为true,并输出“connect success”。
- 如果当前没有活跃的连接(
-
用户交互:
- 在循环中,提示用户输入消息,并通过
getline
读取一行文本。 - 如果用户输入的是"quit",跳出循环,结束程序。
- 发送用户输入的消息到服务器,使用
send
函数。 - 接收服务器的回复,使用
recv
函数,并将接收到的数据打印出来作为服务器的回显。
- 在循环中,提示用户输入消息,并通过
-
错误处理与重连机制:若
send
或recv
过程中发生错误,将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状态。
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连接能够有序、可靠地关闭,避免了数据丢失或混乱,保障了网络环境的稳定性和效率。