当前位置: 首页 > news >正文

“深入浅出”系列之Linux篇:(12)C++网络编程

一:一对一服务器与客户端模式

项目简介:Socket编程技术、多线程技术、文件操作等。C++【点对点聊天软件】:一个服务器、一个客户端、主线程用来发送数据,启动一个子线程用来接收数据,服务器记录聊天内容。

1:服务器

// Server.cpp
// 引入Windows系统相关的头文件,提供Windows API函数、数据类型和宏定义等
#include <windows.h>
// 引入进程和线程相关的头文件,用于创建和管理线程
#include <process.h>
// 引入标准输入输出流头文件,用于进行控制台的输入输出操作
#include <iostream>
// 引入时间处理相关的头文件,用于获取和处理时间信息
#include "time.h"
// 使用标准命名空间,避免每次使用标准库中的类和函数时都要加std::前缀
using namespace std;
// 链接ws2_32.lib库,该库提供了Windows Sockets API的实现,用于网络编程
#pragma comment(lib,"ws2_32.lib")
// 文件操作实现类,用于管理日志文件的读写操作
class FileLog
{
private:
    // 临界区对象,用于线程同步,确保在多线程环境下对文件的操作是安全的
    CRITICAL_SECTION cs;
    // 文件句柄,用于标识和操作打开的文件
    HANDLE fileHandle;
    // 进入临界区,防止其他线程同时访问共享资源(这里是文件)
    void Lock()
    {
        EnterCriticalSection(&cs);
    }
    // 离开临界区,允许其他线程访问共享资源
    void UnLock()
    {
        LeaveCriticalSection(&cs);
    }
public:
    // 构造函数,初始化临界区和文件句柄
    FileLog()
    {
        // 初始化临界区对象
        InitializeCriticalSection(&cs);
        // 先将文件句柄初始化为无效句柄
        fileHandle = INVALID_HANDLE_VALUE;
    }
    // 析构函数,在对象销毁时关闭文件并删除临界区
    ~FileLog()
    {
        // 如果文件句柄有效,则关闭文件
        if (fileHandle != INVALID_HANDLE_VALUE)
        {
            // 关闭文件句柄
            CloseHandle(fileHandle);
        }
        // 删除临界区对象
        DeleteCriticalSection(&cs);
    }
    // 打开文件的方法,返回一个布尔值表示是否成功打开
    BOOL Open(const char* fileName);
    // 向文件中写入内容的方法,返回当前对象的引用,方便链式调用
    FileLog& Write(const char* content);
    // 向文件中写入一行内容的方法,返回当前对象的引用,方便链式调用
    FileLog& WriteLine(const char* content);
    // 从文件中读取内容的方法,返回一个布尔值表示是否成功读取
    BOOL Read(char* buf, int size);
    // 关闭文件的方法,返回一个布尔值表示是否成功关闭
    BOOL Close();
};
// 定义一个结构体,用于在多线程之间传递参数,包含套接字指针和文件日志对象指针
typedef struct _receiveStruct
{
    // 指向套接字的指针
    SOCKET* Socket;
    // 指向文件日志对象的指针
    FileLog* fileLog;
    // 构造函数,用于初始化结构体成员
    _receiveStruct(SOCKET* _socket, FileLog* _fileLog) :Socket(_socket), fileLog(_fileLog) {}
} ReceiveStruct;
// 打开文件的具体实现
BOOL FileLog::Open(const char* fileName)
{
    // 如果文件句柄无效,则尝试打开文件
    if (fileHandle == INVALID_HANDLE_VALUE)
    {
        // 创建或打开文件,以读写模式打开,允许其他进程同时读写
        fileHandle = CreateFile((LPCWSTR)fileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
            OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        // 如果文件打开成功
        if (fileHandle != INVALID_HANDLE_VALUE)
        {
            // 将文件指针移动到文件末尾
            SetFilePointer(fileHandle, 0, NULL, FILE_END);
            return TRUE;
        }
    }
    return FALSE;
}
// 向文件中写入内容的具体实现
FileLog& FileLog::Write(const char* content)
{
    // 进入临界区,确保线程安全
    Lock();
    // 如果文件句柄有效
    if (fileHandle != INVALID_HANDLE_VALUE)
    {
        // 用于存储实际写入的字节数
        DWORD dwSize = 0;
        // 向文件中写入内容
        WriteFile(fileHandle, content, strlen(content), &dwSize, NULL);
    }
    // 离开临界区,允许其他线程访问文件
    UnLock();
    // 返回当前对象的引用,方便链式调用
    return *this;
}
// 向文件中写入一行内容的具体实现
FileLog& FileLog::WriteLine(const char* content)
{
    // 进入临界区,确保线程安全
    Lock();
    // 如果文件句柄有效
    if (fileHandle != INVALID_HANDLE_VALUE)
    {
        // 用于存储实际写入的字节数
        DWORD dwSize = 0;
        // 向文件中写入内容
        WriteFile(fileHandle, content, strlen(content), &dwSize, NULL);
    }
    // 离开临界区,允许其他线程访问文件
    UnLock();
    // 调用Write方法写入换行符
    return FileLog::Write("\r\n");
}
// 从文件中读取内容的具体实现
BOOL FileLog::Read(char* buf, int size)
{
    // 用于标记读取操作是否成功
    BOOL isOK = FALSE;
    // 进入临界区,确保线程安全
    Lock();
    // 如果文件句柄有效
    if (fileHandle != INVALID_HANDLE_VALUE)
    {
        // 用于存储实际读取的字节数
        DWORD dwSize = 0;
        // 从文件中读取内容到缓冲区
        isOK = ReadFile(fileHandle, buf, size, &dwSize, NULL);
    }
    // 离开临界区,允许其他线程访问文件
    UnLock();
    return isOK;
}
// 关闭文件的具体实现
BOOL FileLog::Close()
{
    // 用于标记关闭操作是否成功
    BOOL isOK = FALSE;
    // 进入临界区,确保线程安全
    Lock();
    // 如果文件句柄有效
    if (fileHandle != INVALID_HANDLE_VALUE)
    {
        // 关闭文件句柄
        isOK = CloseHandle(fileHandle);
        // 将文件句柄置为无效句柄
        fileHandle = INVALID_HANDLE_VALUE;
    }
    // 离开临界区,允许其他线程访问文件
    UnLock();
    return isOK;
}
// 获取当天日期的字符串
string GetDate(const char* format)
{
    // 存储当前时间的秒数
    time_t tm;
    // 指向本地时间结构体的指针
    struct tm* now;
    // 存储格式化后的时间字符串
    char timebuf[20];
    // 获取当前时间的秒数
    time(&tm);
    // 将时间转换为本地时间结构体
    now = localtime(&tm);
    // 格式化时间字符串
    strftime(timebuf, sizeof(timebuf) / sizeof(char), format, now);
    return string(timebuf);
}
// 接收数据线程的函数
void receive(PVOID param)
{
    // 将传入的参数转换为ReceiveStruct结构体指针
    ReceiveStruct* receiveStruct = (ReceiveStruct*)param;
    // 用于存储接收到的数据
    char buf[2048];
    // 用于存储接收到的字节数
    int bytes;
    while (1)
    {
        // 接收数据
        if ((bytes = recv(*receiveStruct->Socket, buf, sizeof(buf), 0)) == SOCKET_ERROR) {
            // 输出接收数据失败的信息
            cout << "接收数据失败!\n";
            // 终止当前线程
            _endthread();
        }
        // 在接收到的数据末尾添加字符串结束符
        buf[bytes] = '\0';
        // 输出客户端发送的消息
        cout << "客户端说:" << buf << endl;
        // 将客户端消息记录到日志文件中
        receiveStruct->fileLog->Write("客户端    ").WriteLine(GetDate("%Y-%m-%d %H:%M:%S").c_str()).WriteLine(buf);
    }
}
// 获取本机IP
in_addr getHostName(void)
{
    // 用于存储本地主机名
    char host_name[255];
    // 获取本地主机名称
    if (gethostname(host_name, sizeof(host_name)) == SOCKET_ERROR) {
        // 输出获取主机名失败的信息
        cout << "Error " << WSAGetLastError() << " when getting local host name.";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 从主机名数据库中得到对应的“IP”
    struct hostent* phe = gethostbyname(host_name);
    if (phe == 0) {
        // 输出主机名查找失败的信息
        cout << "Yow! Bad host lookup.";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 用于存储获取到的IP地址
    struct in_addr addr;
    // 将IP地址复制到addr结构体中
    memcpy(&addr, phe->h_addr_list[0], sizeof(struct in_addr));
    return addr;
}
// 启动服务器
SOCKET StartServer(void)
{
    // 用于存储服务器套接字
    SOCKET serverSocket;
    // 创建套接字
    if ((serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {
        // 输出创建套接字失败的信息
        cout << "创建套接字失败!";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 服务器监听的端口号
    short port = 202588;
    // 用于存储服务器地址信息
    struct sockaddr_in serverAddress;
    // 初始化指定的内存区域
    memset(&serverAddress, 0, sizeof(sockaddr_in));
    // 设置地址族为IPv4
    serverAddress.sin_family = AF_INET;
    // 监听所有可用的网络接口
    serverAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    // 将端口号转换为网络字节序
    serverAddress.sin_port = htons(port);
    // 绑定
    if (bind(serverSocket, (sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
        // 输出套接字绑定失败的信息
        cout << "套接字绑定到端口失败!端口:" << port;
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 进入侦听状态
    if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
        // 输出侦听失败的信息
        cout << "侦听失败!";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 获取服务器IP
    struct in_addr addr = getHostName();
    // 输出服务器监听的地址和端口信息
    cout << "Server " << inet_ntoa(addr) << " : " << port << " is listening......" << endl;
    return serverSocket;
}
// 接收客户端连接
SOCKET ReceiveConnect(SOCKET& serverSocket)
{
    // 用于和客户端通信的套接字
    SOCKET clientSocket;
    // 用于和客户端通信的套接字地址
    struct sockaddr_in clientAddress;
    // 初始化存放客户端信息的内存
    memset(&clientAddress, 0, sizeof(clientAddress));
    // 客户端地址结构体的长度
    int addrlen = sizeof(clientAddress);
    // 接受连接
    if ((clientSocket = accept(serverSocket, (sockaddr*)&clientAddress, &addrlen)) == INVALID_SOCKET) {
        // 输出接受客户端连接失败的信息
        cout << "接受客户端连接失败!";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 输出接受客户端连接的信息
    cout << "Accept connection from " << inet_ntoa(clientAddress.sin_addr) << endl;
    return clientSocket;
}
// 发送数据
void SendMsg(SOCKET& clientSocket, FileLog& fileLog)
{
    // 用于存储要发送的数据
    char buf[2048];
    while (1) {
        // 提示用户输入要发送的消息
        cout << "服务器说:";
        // 从标准输入读取用户输入的消息
        gets_s(buf);
        // 发送数据
        if (send(clientSocket, buf, strlen(buf), 0) == SOCKET_ERROR) {
            // 输出发送数据失败的信息
            cout << "发送数据失败!" << endl;
            // 程序暂停3秒
            Sleep(3000);
            // 退出程序
            exit(-1);
        }
        // 将服务器发送的消息记录到日志文件中
        fileLog.Write("服务器   ").WriteLine(GetDate("%Y-%m-%d %H:%M:%S").c_str()).WriteLine(buf);
    }
}
// 主函数,程序入口
int main(int argc, char* argv[])
{
    // 用于保存函数WSAStartup返回的Windows Sockets初始化信息
    WSADATA wsa;
    // 初始化Windows Sockets库
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        // 输出套接字初始化失败的信息
        cout << "套接字初始化失败!";
        // 程序暂停3秒
        Sleep(3000);
        // 退出程序
        exit(-1);
    }
    // 启动服务器,获取服务器套接字
    SOCKET serverSocket = StartServer();
    // 接收客服端的链接,获取客户端套接字
    SOCKET clientSocket = ReceiveConnect(serverSocket);
    // 创建文件日志对象
    FileLog fileLog;
    // 打开记录聊天内容文件,以当天日期命名
    fileLog.Open(GetDate("%Y%m%d").append(".log").c_str());
    // 创建接收数据的结构体对象
    ReceiveStruct receiveStruct(&clientSocket, &fileLog);
    // 启动一个接收数据的线程
    _beginthread(receive, 0, &receiveStruct);
    // 发送数据
    SendMsg(clientSocket, fileLog);
    // 关闭文件
    fileLog.Close();
    // 关闭客户端套接字
    closesocket(clientSocket);
    // 关闭服务器套接字
    closesocket(serverSocket);
    // 清理套接字占用的资源
    WSACleanup();
    
    return 0;
}

2:【客户端代码】

// Client.cpp  
// 引入 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <windows.h>
// 引入进程和线程相关的头文件,用于创建和管理线程
#include <process.h>
// 引入标准输入输出流头文件,用于进行控制台的输入输出操作
#include <iostream>
// 使用标准命名空间,避免每次使用标准库中的类和函数时都要加 std:: 前缀
using namespace std;
// 链接 ws2_32.lib 库,该库提供了 Windows Sockets API 的实现,用于网络编程
#pragma comment(lib,"ws2_32.lib")
// 接收数据的线程函数
void Receive(PVOID param)
{
    // 用于存储接收到的数据的缓冲区
    char buf[2096];
    while (1)
    {
        // 将传入的参数转换为 SOCKET 指针
        SOCKET* sock = (SOCKET*)param;
        // 用于存储接收到的字节数
        int bytes;
        // 接收数据
        if ((bytes = recv(*sock, buf, sizeof(buf), 0)) == SOCKET_ERROR) {
            // 输出接收数据失败的信息
            printf("接收数据失败!\n");
            // 终止程序
            exit(-1);
        }
        // 在接收到的数据末尾添加字符串结束符
        buf[bytes] = '\0';
        // 输出服务器发送的消息
        cout << "服务器说:" << buf << endl;
    }
}
// 获取服务器 IP 地址
unsigned long GetServerIP(void)
{
    // 用于存储用户输入的 IP 地址字符串
    char ipStr[20];
    // 用 0 填充 ipStr 数组,确保其内容为空
    memset(ipStr, 0, sizeof(ipStr));
    // 提示用户输入要连接的服务器 IP 地址
    cout << "请输入你要链接的服务器 IP:";
    // 从标准输入读取用户输入的 IP 地址
    cin >> ipStr;
    // 用于存储转换后的 IP 地址
    unsigned long ip;
    // 将字符串形式的 IP 地址转换为无符号长整型
    if ((ip = inet_addr(ipStr)) == INADDR_NONE) {
        // 输出不合法 IP 地址的信息
        cout << "不合法的 IP 地址:";
        // 程序暂停 3 秒
        Sleep(3000);
        // 终止程序
        exit(-1);
    }
    return ip;
}
// 连接服务器
void Connect(SOCKET& sock)
{
    // 获取服务器的 IP 地址
    unsigned long ip = GetServerIP();
    // 服务器监听的端口号
    short port = 202588;
    // 输出正在连接的服务器地址和端口信息
    cout << "Connecting to " << inet_ntoa(*(in_addr*)&ip) << " : " << port << endl;
    // 用于存储服务器地址信息
    struct sockaddr_in serverAddress;
    // 用 0 填充 serverAddress 结构体,确保其内容为空
    memset(&serverAddress, 0, sizeof(sockaddr_in));
    // 设置地址族为 IPv4
    serverAddress.sin_family = AF_INET;
    // 设置服务器的 IP 地址
    serverAddress.sin_addr.S_un.S_addr = ip;
    // 将端口号转换为网络字节序
    serverAddress.sin_port = htons(port);
    // 建立和服务器的连接
    if (connect(sock, (sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
        // 输出建立连接失败的信息及错误码
        cout << "建立连接失败:" << WSAGetLastError();
        // 程序暂停 3 秒
        Sleep(3000);
        // 终止程序
        exit(-1);
    }
}
// 发送数据
void SendMsg(SOCKET& sock)
{
    // 用于存储要发送的数据的缓冲区
    char buf[2048];
    while (1) {
        // 从控制台读取一行数据
        gets_s(buf);
        // 输出提示信息
        cout << "我说:";
        // 发送数据给服务器
        if (send(sock, buf, strlen(buf), 0) == SOCKET_ERROR) {
            // 输出发送数据失败的信息
            cout << "发送数据失败!";
            // 终止程序
            exit(-1);
        }
    }
}
// 主函数,程序入口
int main(int argc, char* argv[]) {
    // 用于保存函数 WSAStartup 返回的 Windows Sockets 初始化信息
    WSADATA wsa;
    // 初始化 Windows Sockets 库
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        // 输出套接字初始化失败的信息
        cout << "套接字初始化失败!";
        // 程序暂停 3 秒
        Sleep(3000);
        // 终止程序
        exit(-1);
    }
    // 创建套接字
    SOCKET sock;
    if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {
        // 输出创建套接字失败的信息
        cout << "创建套接字失败!";
        // 终止程序
        exit(-1);
    }
    // 连接服务器
    Connect(sock);
    // 启动接收数据线程
    _beginthread(Receive, 0, &sock);
    // 发送数据
    SendMsg(sock);
    // 清理套接字占用的资源
    WSACleanup();
    
    return 0;
}

【输出结果】

运行服务器:

图片

运行客户端:

图片

3:日志文件

图片

二:一对多服务器与客户端模式

【项目思路】:启动服务器,服务器启动后会创建一个子线程,用于向客户端发送信息,用一个死循环用于接收客户端的请求,客户端请求成功后,会将客户端的连接保存到一个集合中。

【详细简介】:保存客户端连接的类。客户端连接成功后,服务器会创建一个子线程用于接收客户端的信息,客户端同样也会创建一个子线程接收服务器的信息。这样客户端和服务器就能进行通讯,如果有哪一方退出,另一方对应的接收数据的线程就会自动终止。

图片

退出1个客户端后,服务器对应的接收数据的线程自动终止.如下图:

图片

1:【服务器代码】

SServer.h

// 如果没有定义 __SSERVER_H__ 宏,则进行以下定义
// 这是为了防止头文件被重复包含,避免出现重复定义的编译错误
#ifndef __SSERVER_H__
#define __SSERVER_H__
// 引入 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <windows.h>
// 引入自定义的 Socket 枚举类型头文件,用于处理与 Socket 相关的枚举类型
#include "SocketEnum.h"
// 引入自定义的 CSocket 类头文件,该类可能封装了 Socket 的一些操作
#include "CSocket.h"
// 定义 SServer 类,用于实现服务器的相关功能
class SServer
{
public:
    // 启动服务器的方法
    // 参数 port 表示服务器要监听的端口号
    // 返回值为布尔类型,若服务器成功启动则返回 true,否则返回 false
    bool Start(int port);
    // 接收客户端请求的方法
    // 返回值为 CSocket 指针,指向一个代表客户端连接的 CSocket 对象
    // 当有客户端连接时,该方法会返回一个新的 CSocket 对象用于与该客户端进行通信
    CSocket* Accept(); 
    // 设置 Socket 错误信息的方法
    // 参数 error 是 SocketEnum::SocketError 枚举类型,用于指定具体的错误类型
    void SetSocketError(SocketEnum::SocketError error);
    // 析构函数,在对象销毁时自动调用
    // 通常用于释放对象所占用的资源,如关闭 Socket 连接、释放内存等
    ~SServer();
    // 关闭服务器的方法
    // 用于关闭服务器的 Socket 连接,停止服务器的运行
    void Close();
    // 关闭服务器 Socket 连接的指定模式的方法
    // 参数 mode 是 SocketEnum::ShutdownMode 枚举类型,用于指定关闭的模式
    // 返回值为布尔类型,若关闭操作成功则返回 true,否则返回 false
    bool ShutDown(SocketEnum::ShutdownMode mode);
private: 
    // 服务器的 Socket 描述符,用于标识服务器的 Socket 连接
    SOCKET ssocket;
    // 缓冲区指针,用于存储从客户端接收的数据或要发送给客户端的数据
    char* buffer;
    // 服务器地址结构体,用于存储服务器的 IP 地址和端口号等信息
    struct sockaddr_in serverAddress;
    // Socket 错误信息,用于记录服务器在运行过程中出现的错误类型
    SocketEnum::SocketError socketError;
    // 服务器是否启动的标志位,用于判断服务器当前是否处于启动状态
    bool isStart;
    // 用于保存函数 WSAStartup 返回的 Windows Sockets 初始化信息
    WSADATA wsa;
};
// 结束 __SSERVER_H__ 宏的定义
#endif __SSERVER_H__

Server.cpp:main()函数所在的源文件

#include <windows.h>
#include <process.h>
#include <iostream>
using namespace std;
#pragma comment(lib,"ws2_32.lib") 
#include "SServer.h"
#include "CSocket.h"
#include <vector>
#include "ClientList.h"
const int BUF_LEN=1024;

void recv(PVOID pt)
{
	CSocket* csocket=(CSocket*)pt;
	if(csocket!=NULL)
	{
		int count= csocket->Receive(BUF_LEN); 
		if(count==0)
		{  
			ClientList* list=ClientList::GetInstance(); 
			list->Remove(csocket);
			cout<<"一个用户下线,在线人数:"<<list->Count()<<endl;
			_endthread(); //用户下线,终止接收数据线程
		}
	} 
}

void sends(PVOID pt)
{
	ClientList* list=(ClientList*)pt;
	while(1)
	{
		char* buf=new char[BUF_LEN] ;
		cin>>buf;
		int bufSize=0;
		while(buf[bufSize++]!='\0'); 
		for(int i=list->Count()-1;i>=0;i--)
		{
			(*list)[i]->Send(buf,bufSize);  
		} 
		delete buf;
	}
}

int main(int argc, char* argv[])
{
	SServer server;
	bool isStart=server.Start(202588);
	if(isStart)
	{
		cout<<"server start success..."<<endl;
	}else
	{
		cout<<"server start error"<<endl;
	} 
	ClientList* list=ClientList::GetInstance();
	_beginthread(sends,0,list);//启动一个线程广播数据
	while(1)
	 {
		CSocket* csocket=server.Accept();

		list->Add(csocket);
		cout<<"新上线一个用户,在线人数:"<<list->Count()<<endl;
		_beginthread(recv,0,csocket);//启动一个接收数据的线程
	 }

	getchar();

	return 0;
} 

ClientList.h 存放客户端的请求,只能有一个实例。

#include <windows.h>  // 包含 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <process.h>  // 包含进程和线程相关的头文件,用于创建和管理线程
#include <iostream>   // 包含标准输入输出流头文件,用于进行控制台的输入输出操作
using namespace std;  // 使用标准命名空间,避免每次使用标准库中的类和函数时都要加 std:: 前缀
// 链接 ws2_32.lib 库,该库提供了 Windows Sockets API 的实现,用于网络编程
#pragma comment(lib,"ws2_32.lib") 
#include "SServer.h"  // 包含自定义的 SServer 类头文件,用于实现服务器的相关功能
#include "CSocket.h"  // 包含自定义的 CSocket 类头文件,用于处理与客户端的 Socket 通信
#include <vector>     // 包含向量容器的头文件,用于存储多个元素
#include "ClientList.h" // 包含自定义的 ClientList 类头文件,用于管理客户端列表
// 定义缓冲区的长度
const int BUF_LEN = 1024;

// 接收数据的线程函数
// 参数 pt 是一个指向 CSocket 对象的指针,用于接收客户端发送的数据
void recv(PVOID pt)
{
    // 将传入的参数转换为 CSocket 指针
    CSocket* csocket = (CSocket*)pt;
    // 检查 CSocket 指针是否有效
    if (csocket != NULL)
    {
        // 调用 CSocket 对象的 Receive 方法接收数据,并返回接收到的字节数
        int count = csocket->Receive(BUF_LEN);
        // 如果接收到的字节数为 0,表示客户端断开连接
        if (count == 0)
        {
            // 获取 ClientList 类的单例对象
            ClientList* list = ClientList::GetInstance();
            // 从客户端列表中移除该客户端
            list->Remove(csocket);
            // 输出一个用户下线的信息以及当前在线人数
            cout << "一个用户下线,在线人数:" << list->Count() << endl;
            // 终止当前接收数据的线程
            _endthread(); 
        }
    }
}
// 发送数据的线程函数
// 参数 pt 是一个指向 ClientList 对象的指针,用于向所有客户端广播数据
void sends(PVOID pt)
{
    // 将传入的参数转换为 ClientList 指针
    ClientList* list = (ClientList*)pt;
    while (1)
    {
        // 动态分配内存用于存储要发送的数据
        char* buf = new char[BUF_LEN];
        // 从标准输入读取用户输入的数据
        cin >> buf;
        // 计算输入数据的长度
        int bufSize = 0;
        while (buf[bufSize++] != '\0');
        // 遍历客户端列表,向每个客户端发送数据
        for (int i = list->Count() - 1; i >= 0; i--)
        {
            (*list)[i]->Send(buf, bufSize);
        }
        // 释放动态分配的内存,避免内存泄漏
        delete buf;
    }
}
// 主函数,程序的入口点
int main(int argc, char* argv[])
{
    // 创建 SServer 类的对象,用于启动服务器
    SServer server;
    // 调用 SServer 对象的 Start 方法启动服务器,监听端口号为 202588
    bool isStart = server.Start(202588);
    // 根据服务器启动结果输出相应的信息
    if (isStart)
    {
        cout << "server start success..." << endl;
    }
    else
    {
        cout << "server start error" << endl;
    }
    // 获取 ClientList 类的单例对象,用于管理客户端列表
    ClientList* list = ClientList::GetInstance();
    // 启动一个线程用于向所有客户端广播数据
    _beginthread(sends, 0, list);
    while (1)
    {
        // 调用 SServer 对象的 Accept 方法接收客户端的连接请求,并返回一个 CSocket 对象
        CSocket* csocket = server.Accept();
        // 将新连接的客户端添加到客户端列表中
        list->Add(csocket);
        // 输出一个新用户上线的信息以及当前在线人数
        cout << "新上线一个用户,在线人数:" << list->Count() << endl;
        // 为每个新连接的客户端启动一个接收数据的线程
        _beginthread(recv, 0, csocket);
    }
    // 等待用户输入一个字符,防止程序立即退出
    getchar();
    return 0;
}

ClientList.cpp

#include "ClientList.h"
// 定义一个类型别名 Iter,它是 vector<CSocket*> 容器的迭代器类型
// 迭代器用于遍历 vector 容器中的元素
typedef vector<CSocket*>::iterator Iter; 
// ClientList 类的构造函数
ClientList::ClientList()
{
    // 初始化临界区对象 g_cs
    // 临界区用于实现线程同步,确保在多线程环境下对共享资源(这里是 _list 容器)的访问是安全的
    InitializeCriticalSection(&g_cs); 
}
// ClientList 类的析构函数
ClientList::~ClientList()
{
    // 删除临界区对象 g_cs
    // 在对象销毁时,需要释放临界区所占用的资源
    DeleteCriticalSection(&g_cs); 
}
// 向客户端列表中添加一个 CSocket 对象
void ClientList::Add(CSocket* socket)
{
    // 检查传入的 CSocket 指针是否有效
    if(socket != NULL)
    {
        // 进入临界区,防止其他线程同时访问 _list 容器
        // 这样可以避免多个线程同时修改 _list 导致的数据不一致问题
        EnterCriticalSection(&g_cs);
        // 将 CSocket 指针添加到 _list 容器的末尾
        _list.push_back(socket);
        // 离开临界区,允许其他线程访问 _list 容器
        LeaveCriticalSection(&g_cs); 
    }
}
// 获取客户端列表中元素的数量
int ClientList::Count() const
{
    // 直接返回 _list 容器中元素的数量
    return _list.size();
}
// 重载 [] 运算符,用于通过索引访问客户端列表中的元素
CSocket* ClientList::operator[](size_t index)
{
    // 断言检查索引是否在有效范围内
    // 如果索引越界,程序会触发断言错误并终止
    assert(index >= 0 && index < _list.size()); 
    // 返回 _list 容器中指定索引位置的 CSocket 指针
    return _list[index];
}
// 从客户端列表中移除指定的 CSocket 对象
void ClientList::Remove(CSocket* socket)
{
    // 调用 Find 方法查找指定的 CSocket 对象在 _list 容器中的迭代器位置
    Iter iter = Find(socket);
    // 进入临界区,防止其他线程同时访问 _list 容器
    EnterCriticalSection(&g_cs);
    // 检查是否找到了指定的 CSocket 对象
    if(iter != _list.end())
    {
        // 释放该 CSocket 对象所占用的内存
        delete *iter; 
        // 从 _list 容器中移除该元素
        _list.erase(iter);
    }
    // 离开临界区,允许其他线程访问 _list 容器
    LeaveCriticalSection(&g_cs); 
}
// 在客户端列表中查找指定的 CSocket 对象,并返回其迭代器位置
Iter ClientList::Find(CSocket* socket)
{
    // 进入临界区,防止其他线程同时访问 _list 容器
    EnterCriticalSection(&g_cs);
    // 初始化迭代器指向 _list 容器的起始位置
    Iter iter = _list.begin();
    // 遍历 _list 容器
    while(iter != _list.end())
    {
        // 检查当前迭代器指向的 CSocket 指针是否与要查找的指针相等
        if(*iter == socket)
        {
            // 如果找到,返回该迭代器
            return iter;
        }
        // 移动迭代器到下一个元素
        iter++;
    }
    // 离开临界区,允许其他线程访问 _list 容器
    LeaveCriticalSection(&g_cs); 
    // 如果未找到,返回 _list 容器的末尾迭代器
    return iter;
}
// 清空客户端列表,并释放所有 CSocket 对象所占用的内存
void ClientList::Clear()
{
    // 进入临界区,防止其他线程同时访问 _list 容器
    EnterCriticalSection(&g_cs);
    // 从后往前遍历 _list 容器
    for(int i = _list.size() - 1; i >= 0; i--)
    {
        // 释放当前 CSocket 对象所占用的内存
        delete _list[i];
    }
    // 清空 _list 容器
    _list.clear();
    // 离开临界区,允许其他线程访问 _list 容器
    LeaveCriticalSection(&g_cs); 
}
// 定义并初始化 ClientList 类的静态成员变量 g_cs
// 静态成员变量属于类本身,而不是类的某个对象
CRITICAL_SECTION ClientList::g_cs;
// 定义并初始化 ClientList 类的静态成员变量 _list
// 用于存储 CSocket 指针的 vector 容器
vector<CSocket*> ClientList::_list ;

CSocket.h

// 防止头文件被重复包含的预处理器指令
// 如果未定义 __CSOCKET_H__,则执行下面的代码块
#ifndef __CSOCKET_H__
#define __CSOCKET_H__
// 包含 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <windows.h>
// 包含自定义的 Socket 枚举类型头文件,用于处理与 Socket 相关的枚举类型
#include "SocketEnum.h"
// 包含标准输入输出流头文件,用于进行控制台的输入输出操作
#include <iostream>
// 使用标准命名空间,避免每次使用标准库中的类和函数时都要加 std:: 前缀
using namespace std;
// 包含自定义的 ClientList 类头文件,可能用于管理客户端列表
#include "ClientList.h"
// 定义 CSocket 类,用于封装 Socket 操作
class CSocket
{
public:
    // 构造函数
    // 参数 _socketType 表示 Socket 的类型,默认为 TCP 类型
    CSocket(SocketEnum::SocketType _socketType = SocketEnum::Tcp);
    // 析构函数,在对象销毁时自动调用,用于释放资源
    ~CSocket();
    // 连接到指定 IP 地址和端口的服务器
    // 参数 ip 是服务器的 IP 地址,port 是服务器监听的端口号
    // 返回值为布尔类型,若连接成功则返回 true,否则返回 false
    bool Connect(const char* ip, int port);
    // 发送数据到连接的服务器
    // 参数 pBuf 是要发送的数据缓冲区,len 是要发送的数据长度
    // 返回值为整数类型,表示实际发送的字节数
    int Send(char* pBuf, int len);
    // 从连接的服务器接收数据
    // 参数 strLen 是接收缓冲区的长度
    // 返回值为整数类型,表示实际接收的字节数
    int Receive(int strLen);
    // 设置 Socket 的阻塞模式
    // 参数 isBlocking 为 true 表示设置为阻塞模式,false 表示设置为非阻塞模式
    // 返回值为布尔类型,若设置成功则返回 true,否则返回 false
    bool SetBlocking(bool isBlocking);
    // 关闭 Socket 连接的指定模式
    // 参数 mode 是 SocketEnum::ShutdownMode 枚举类型,用于指定关闭的模式
    // 返回值为布尔类型,若关闭操作成功则返回 true,否则返回 false
    bool ShutDown(SocketEnum::ShutdownMode mode);
    // 获取接收到的数据
    // 返回值为字符指针,指向存储接收数据的缓冲区
    char* GetData();
    // 获取当前 Socket 的错误信息
    // 返回值为 SocketEnum::SocketError 枚举类型,表示具体的错误类型
    SocketEnum::SocketError GetSocketError();
    // 设置 Socket 句柄
    // 参数 socket 是要设置的 Socket 句柄
    void SetSocketHandle(SOCKET socket);
    // 关闭 Socket 连接
    void Close();
    // 重载 == 运算符,用于比较两个 CSocket 对象是否相等
    // 参数 socket 是要比较的另一个 CSocket 对象的指针
    // 返回值为布尔类型,若两个对象相等则返回 true,否则返回 false
    bool operator==(const CSocket* socket);
    // 判断 Socket 是否已经退出连接
    // 返回值为布尔类型,若已退出则返回 true,否则返回 false
    bool IsExit();
private:
    // 设置 Socket 的错误信息
    // 参数 error 是 SocketEnum::SocketError 枚举类型,用于指定具体的错误类型
    void SetSocketError(SocketEnum::SocketError error);
    // 设置 Socket 的错误信息,根据当前 Socket 的状态自动判断错误类型
    void SetSocketError(void);
    // 判断 Socket 是否有效
    // 返回值为布尔类型,若有效则返回 true,否则返回 false
    bool IsSocketValid(void);
    // Socket 句柄,用于标识 Socket 连接
    SOCKET csocket;
    // 连接状态标志,true 表示已连接,false 表示未连接
    bool isConnected;
    // 服务器地址结构体,用于存储服务器的 IP 地址和端口号等信息
    struct sockaddr_in serverAddress;
    // 存储接收数据的缓冲区指针
    char* buffer;
    // 发送数据的长度
    int sendCount;
    // 接收数据的长度
    int recvCount;
    // 是否为阻塞模式的标志,true 表示阻塞模式,false 表示非阻塞模式
    bool isBlocking;
    // 当前 Socket 的错误信息,用枚举类型表示
    SocketEnum::SocketError socketError;
    // Socket 的类型,用枚举类型表示
    SocketEnum::SocketType socketType;
    // 用于保存函数 WSAStartup 返回的 Windows Sockets 初始化信息
    WSADATA wsa;
};
// 结束 __CSOCKET_H__ 宏的定义
#endif

CSocket.cpp

#include "CSocket.h"
// CSocket 类的构造函数
// 使用初始化列表对成员变量进行初始化
// csocket 初始化为 INVALID_SOCKET,表示无效的套接字
// isConnected 初始化为 false,表示未连接状态
// buffer 初始化为 NULL,表示没有分配缓冲区
// sendCount 和 recvCount 初始化为 0,表示发送和接收的数据长度为 0
// isBlocking 初始化为 true,表示默认使用阻塞模式
// socketError 初始化为 SocketEnum::InvalidSocket,表示套接字无效的错误状态
// socketType 使用传入的参数 _socketType 进行初始化
CSocket::CSocket(SocketEnum::SocketType _socketType) : csocket(INVALID_SOCKET), isConnected(false), buffer(NULL),
                                                      sendCount(0), recvCount(0), isBlocking(true),
                                                      socketError(SocketEnum::InvalidSocket),
                                                      socketType(_socketType) {
}
// 连接到指定 IP 地址和端口的服务器
bool CSocket::Connect(const char* ip, int port) {
    // 假设连接成功,设置连接状态为 true
    isConnected = true;
    // 初始化错误信息为成功状态
    socketError = SocketEnum::Success;
    // 初始化 Windows Sockets DLL
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        // 如果初始化失败,设置错误信息为 WSAStartupError
        SetSocketError(SocketEnum::WSAStartupError);
        // 标记连接失败
        isConnected = false;
    }
    // 如果 WSAStartup 成功
    if (isConnected) {
        // 创建一个 TCP 套接字
        if ((csocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {
            // 如果套接字创建失败,设置错误信息
            SetSocketError();
            // 标记连接失败
            isConnected = false;
        }
    }
    // 如果套接字创建成功
    if (isConnected) {
        // 清空服务器地址结构体
        memset(&serverAddress, 0, sizeof(sockaddr_in));
        // 设置地址族为 IPv4
        serverAddress.sin_family = AF_INET;
        // 将 IP 地址字符串转换为网络字节序的长整型
        long lip = inet_addr(ip);
        // 检查 IP 地址是否合法
        if (lip == INADDR_NONE) {
            // 如果 IP 地址不合法,设置错误信息为 InvalidAddress
            SetSocketError(SocketEnum::InvalidAddress);
            // 标记连接失败
            isConnected = false;
        } else {
            // 检查端口号是否合法
            if (port < 0) {
                // 如果端口号不合法,设置错误信息为 InvalidPort
                SetSocketError(SocketEnum::InvalidPort);
                // 标记连接失败
                isConnected = false;
            } else {
                // 设置服务器地址的 IP 地址
                serverAddress.sin_addr.S_un.S_addr = lip;
                // 将端口号转换为网络字节序
                serverAddress.sin_port = htons(port);
                // 尝试连接到服务器
                if (connect(csocket, (sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
                    // 如果连接失败,设置错误信息
                    SetSocketError();
                    // 标记连接失败
                    isConnected = false;
                }
            }
        }
    }
    // 返回连接状态
    return isConnected;
}
// 设置套接字的阻塞模式
bool CSocket::SetBlocking(bool isBlock) {
    // 根据 isBlock 的值设置 block 变量,0 表示阻塞模式,1 表示非阻塞模式
    int block = isBlock ? 0 : 1;
    // 使用 ioctlsocket 函数设置套接字的阻塞模式
    if (ioctlsocket(csocket, FIONBIO, (ULONG*)&block) != 0) {
        // 如果设置失败,返回 false
        return false;
    }
    // 更新 isBlocking 成员变量
    isBlocking = isBlock;
    // 设置成功,返回 true
    return true;
}
// 发送数据到服务器
int CSocket::Send(char* pBuf, int len) {
    // 检查套接字是否有效以及是否已连接
    if (!IsSocketValid() || !isConnected) {
        return 0;
    }
    // 检查发送缓冲区和长度是否合法
    if (pBuf == NULL || len < 1) {
        return 0;
    }
    // 调用 send 函数发送数据
    sendCount = send(csocket, pBuf, len, 0);
    // 如果发送失败或没有发送任何数据
    if (sendCount <= 0) {
        // 输出错误信息
        cout << GetSocketError() << endl;
    }
    // 返回实际发送的字节数
    return sendCount;
}
// 从服务器接收数据
int CSocket::Receive(int strLen) {
    // 初始化接收数据长度为 0
    recvCount = 0;
    // 检查套接字是否有效以及是否已连接
    if (!IsSocketValid() || !isConnected) {
        return recvCount;
    }
    // 检查接收缓冲区长度是否合法
    if (strLen < 1) {
        return recvCount;
    }
    // 如果之前有分配的缓冲区,释放它
    if (buffer != NULL) {
        delete[] buffer;
        buffer = NULL;
    }
    // 分配新的接收缓冲区
    buffer = new char[strLen];
    // 设置错误信息为成功状态
    SetSocketError(SocketEnum::Success);
    // 循环接收数据
    while (1) {
        // 调用 recv 函数接收数据
        recvCount = recv(csocket, buffer, strLen, 0);
        // 如果接收到数据
        if (recvCount > 0) {
            // 在接收到的数据末尾添加字符串结束符
            buffer[recvCount] = '\0';
            // 检查是否接收到退出指令
            if (IsExit()) {
                // 如果是退出指令,将数据回发给服务器
                Send(buffer, recvCount);
                // 释放接收缓冲区
                delete[] buffer;
                buffer = NULL;
                // 重置接收数据长度为 0
                recvCount = 0;
                // 跳出循环
                break;
            } else {
                // 输出接收到的数据
                cout << buffer << endl;
            }
        }
    }
    // 返回实际接收的字节数
    return recvCount;
}
// 检查是否接收到退出指令("EXIT" 或 "exit" 等)
bool CSocket::IsExit() {
    // 获取接收到的数据长度
    int len = strlen(buffer);
    int i = 0;
    // 定义退出指令的长度
    int size = 4;
    // 检查数据长度是否等于退出指令的长度
    if (len == size) {
        // 定义退出指令字符串
        char* exit = "EXIT";
        // 逐个字符比较
        for (i = 0; i < size; i++) {
            // 忽略大小写进行比较
            if (buffer[i] != *(exit + i) && buffer[i] - 32 != *(exit + i)) {
                // 如果不匹配,跳出循环
                break;
            }
        }
    }
    // 如果所有字符都匹配,返回 true,表示接收到退出指令
    return i == size;
}
// 设置套接字的错误信息
void CSocket::SetSocketError(SocketEnum::SocketError error) {
    // 更新 socketError 成员变量
    socketError = error;
}
// 根据 WSAGetLastError 的返回值设置套接字的错误信息
void CSocket::SetSocketError(void) {
    // 获取最近一次 Windows Sockets 操作的错误代码
    int nError = WSAGetLastError();
    switch (nError) {
        case EXIT_SUCCESS:
            // 如果没有错误,设置错误信息为成功状态
            SetSocketError(SocketEnum::Success);
            break;
        case WSAEBADF:
        case WSAENOTCONN:
            // 如果是未连接或无效文件描述符的错误,设置错误信息为 Notconnected
            SetSocketError(SocketEnum::Notconnected);
            break;
        case WSAEINTR:
            // 如果是操作被中断的错误,设置错误信息为 Interrupted
            SetSocketError(SocketEnum::Interrupted);
            break;
        case WSAEACCES:
        case WSAEAFNOSUPPORT:
        case WSAEINVAL:
        case WSAEMFILE:
        case WSAENOBUFS:
        case WSAEPROTONOSUPPORT:
            // 如果是无效套接字相关的错误,设置错误信息为 InvalidSocket
            SetSocketError(SocketEnum::InvalidSocket);
            break;
        case WSAECONNREFUSED:
            // 如果是连接被拒绝的错误,设置错误信息为 ConnectionRefused
            SetSocketError(SocketEnum::ConnectionRefused);
            break;
        case WSAETIMEDOUT:
            // 如果是连接超时的错误,设置错误信息为 Timedout
            SetSocketError(SocketEnum::Timedout);
            break;
        case WSAEINPROGRESS:
            // 如果是操作正在进行中的错误,设置错误信息为 Einprogress
            SetSocketError(SocketEnum::Einprogress);
            break;
        case WSAECONNABORTED:
            // 如果是连接被中止的错误,设置错误信息为 ConnectionAborted
            SetSocketError(SocketEnum::ConnectionAborted);
            break;
        case WSAEWOULDBLOCK:
            // 如果是操作会阻塞的错误,设置错误信息为 Ewouldblock
            SetSocketError(SocketEnum::Ewouldblock);
            break;
        case WSAENOTSOCK:
            // 如果不是套接字的错误,设置错误信息为 InvalidSocket
            SetSocketError(SocketEnum::InvalidSocket);
            break;
        case WSAECONNRESET:
            // 如果是连接被重置的错误,设置错误信息为 ConnectionReset
            SetSocketError(SocketEnum::ConnectionReset);
            break;
        case WSANO_DATA:
            // 如果是没有数据的错误,设置错误信息为 InvalidAddress
            SetSocketError(SocketEnum::InvalidAddress);
            break;
        case WSAEADDRINUSE:
            // 如果是地址已被使用的错误,设置错误信息为 AddressInUse
            SetSocketError(SocketEnum::AddressInUse);
            break;
        case WSAEFAULT:
            // 如果是无效指针的错误,设置错误信息为 InvalidPointer
            SetSocketError(SocketEnum::InvalidPointer);
            break;
        default:
            // 如果是其他未知错误,设置错误信息为 UnknownError
            SetSocketError(SocketEnum::UnknownError);
            break;
    }
}
// 检查套接字是否有效
bool CSocket::IsSocketValid(void) {
    // 如果错误信息为成功状态,返回 true,表示套接字有效
    return socketError == SocketEnum::Success;
}
// 获取套接字的错误信息
SocketEnum::SocketError CSocket::GetSocketError() {
    // 返回 socketError 成员变量
    return socketError;
}
// CSocket 类的析构函数
CSocket::~CSocket() {
    // 调用 Close 函数关闭套接字并释放资源
    Close();
}
// 关闭套接字连接并释放资源
void CSocket::Close() {
    // 如果有分配的接收缓冲区,释放它
    if (buffer != NULL) {
        delete[] buffer;
        buffer = NULL;
    }
    // 关闭套接字的读写操作
    ShutDown(SocketEnum::Both);
    // 关闭套接字
    if (closesocket(csocket) != SocketEnum::Error) {
        // 将套接字句柄设置为无效值
        csocket = INVALID_SOCKET;
    }
    // 注释掉的代码,WSACleanup 用于清理 Windows Sockets DLL 的资源,这里可能不需要在每个套接字关闭时都调用
    /* WSACleanup();//清理套接字占用的资源 */
}
// 关闭套接字的读写操作
bool CSocket::ShutDown(SocketEnum::ShutdownMode mode) {
    // 调用 shutdown 函数关闭套接字的读写操作
    SocketEnum::SocketError nRetVal = (SocketEnum::SocketError)shutdown(csocket, SocketEnum::Both);
    // 根据 shutdown 函数的返回值设置错误信息
    SetSocketError();
    // 如果操作成功,返回 true,否则返回 false
    return (nRetVal == SocketEnum::Success) ? true : false;
}
// 获取接收到的数据
char* CSocket::GetData() {
    // 返回接收缓冲区的指针
    return buffer;
}
// 设置套接字句柄
void CSocket::SetSocketHandle(SOCKET socket) {
    // 检查传入的套接字句柄是否有效
    if (socket != SOCKET_ERROR) {
        // 更新 csocket 成员变量
        csocket = socket;
        // 标记连接成功
        isConnected = true;
        // 设置错误信息为成功状态
        socketError = SocketEnum::Success;
    }
}
// 重载 == 运算符,用于比较两个 CSocket 对象是否相等
bool CSocket::operator==(const CSocket* socket) {
    // 比较两个 CSocket 对象的套接字句柄是否相等
    return csocket == socket->csocket;
}

SocketEnum.h

// 防止头文件被重复包含的预处理指令
// 如果尚未定义 __ENUMTYPE_H__ 宏,则执行下面的代码块
#ifndef __ENUMTYPE_H__
#define __ENUMTYPE_H__
// 定义一个结构体 SocketEnum,用于封装与套接字操作相关的枚举类型
struct SocketEnum
{
    // 定义一个枚举类型 SocketType,用于表示套接字的类型
    typedef enum 
    {
        // 无效的套接字类型
        Invalid,
        // TCP 套接字类型
        Tcp,
        // UDP 套接字类型
        Udp
    } SocketType;
    // 定义一个枚举类型 SocketError,用于表示套接字操作过程中可能出现的错误类型
    typedef enum 
    {
        // 通用错误,通常表示操作失败
        Error = -1,             
        // 操作成功
        Success = 0,            
        // 无效的套接字,例如未正确创建或已关闭的套接字
        InvalidSocket,
        // 无效的地址,例如指定的 IP 地址格式错误
        InvalidAddress,         
        // 无效的端口号,例如端口号超出有效范围
        InvalidPort,            
        // 连接被拒绝,通常是因为服务器未监听指定端口或拒绝了连接请求
        ConnectionRefused,      
        // 连接超时,在规定时间内未能建立连接
        Timedout,               
        // 操作会阻塞,通常在非阻塞模式下表示操作无法立即完成
        Ewouldblock,            
        // 套接字未连接,尝试在未连接的套接字上进行操作
        Notconnected,           
        // 操作正在进行中,通常表示有异步操作正在执行
        Einprogress,            
        // 操作被中断,例如系统信号中断了操作
        Interrupted,            
        // 连接被中止,可能是由于网络问题或服务器异常关闭
        ConnectionAborted,      
        // 协议错误,例如使用了不支持的协议
        ProtocolError,          
        // 无效的缓冲区,例如传入的缓冲区为空或大小不足
        InvalidBuffer,          
        // 连接被重置,通常是因为对方异常关闭了连接
        ConnectionReset,        
        // 地址已被使用,尝试绑定一个已经被其他套接字使用的地址和端口
        AddressInUse,           
        // 无效的指针,例如传入了空指针或无效的内存地址
        InvalidPointer,         
        // Windows Sockets 初始化失败
        WSAStartupError,
        // 绑定地址和端口失败
        BindError,
        // 监听连接失败
        ListenError,
        // 未知错误,无法归类到其他已知错误类型
        UnknownError
    } SocketError;
    // 定义一个枚举类型 ShutdownMode,用于表示关闭套接字连接的模式
    typedef enum 
    {
        // 只关闭接收功能,仍然可以发送数据
        Receives = 0,   
        // 只关闭发送功能,仍然可以接收数据
        Sends = 1,      
        // 同时关闭接收和发送功能
        Both = 2        
    } ShutdownMode;   
};
// 结束防止头文件重复包含的预处理指令
#endif __ENUMTYPE_H__

2:【客户端代码】

Client.cpp

// Client.cpp 
// 引入 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <windows.h>
// 引入进程和线程相关的头文件,用于创建和管理线程
#include <process.h>
// 引入标准输入输出流头文件,用于进行控制台的输入输出操作
#include <iostream>
// 使用标准命名空间,避免每次使用标准库中的类和函数时都要加 std:: 前缀
using namespace std;
// 链接 ws2_32.lib 库,该库提供了 Windows Sockets API 的实现,用于网络编程
#pragma comment(lib,"ws2_32.lib") 
// 引入自定义的 CSocket 类头文件,用于处理与服务器的 Socket 通信
#include "CSocket.h"
// 定义缓冲区的长度
const int BUF_LEN = 1024;
// 定义服务器的 IP 地址,这里使用本地回环地址
const char* serverIp = "127.0.0.1";
// 定义服务器监听的端口号
const int serverPort = 202588;
// 接收数据的线程函数
// 参数 pt 是一个指向 CSocket 对象的指针,用于接收服务器发送的数据
void recv(PVOID pt)
{
    // 将传入的参数转换为 CSocket 指针
    CSocket* csocket = (CSocket*)pt;
    // 检查 CSocket 指针是否有效
    if (csocket != NULL)
    {
        // 调用 CSocket 对象的 Receive 方法接收数据,缓冲区长度为 BUF_LEN
        csocket->Receive(BUF_LEN);
        // 终止当前接收数据的线程
        _endthread();
    }
}
// 主函数,程序的入口点
int main(int argc, char* argv[])
{
    // 创建 CSocket 类的对象,用于与服务器进行通信
    CSocket csocket;
    // 调用 CSocket 对象的 Connect 方法连接到指定的服务器 IP 地址和端口号
    bool connect = csocket.Connect(serverIp, serverPort);
    // 设置 CSocket 对象为非阻塞模式
    csocket.SetBlocking(false);
    // 根据连接结果输出相应的信息
    if (connect)
    {
        // 连接成功,输出连接信息
        cout << "connected" << endl;
        // 启动一个线程用于接收服务器发送的数据
        uintptr_t threadId = _beginthread(recv, 0, &csocket);
        while (1)
        {
            // 定义一个缓冲区用于存储用户输入的数据
            char buf[BUF_LEN];
            // 从标准输入读取用户输入的数据
            cin >> buf;
            // 调用 CSocket 对象的 Send 方法将用户输入的数据发送给服务器
            csocket.Send(buf, strlen(buf));
            // 检查是否接收到退出指令
            if (csocket.IsExit())
            {
                // 调用 CSocket 对象的 Close 方法关闭与服务器的连接
                csocket.Close();
                // 输出退出成功的信息
                cout << "exit success" << endl;
                // 跳出循环,结束程序
                break;
            }
        }
    }
    else
    {
        // 连接失败,输出未连接的信息
        cout << "not connect" << endl;
        // 输出连接失败的错误信息
        cout << csocket.GetSocketError() << endl;
    }
    // 等待用户输入一个字符,防止程序立即退出
    getchar();
    return 0;
}

CSocket.h

// 防止头文件被重复包含的预处理指令
// 如果尚未定义 __CSOCKET_H__,则执行下面的代码块
#ifndef __CSOCKET_H__
#define __CSOCKET_H__
// 引入 Windows 系统相关的头文件,提供 Windows API 函数、数据类型和宏定义等
#include <windows.h>
// 引入自定义的 Socket 枚举类型头文件,用于处理与 Socket 相关的枚举类型
#include "SocketEnum.h"
// 引入标准输入输出流头文件,用于进行控制台的输入输出操作
#include <iostream>
// 使用标准命名空间,避免每次使用标准库中的类和函数时都要加 std:: 前缀
using namespace std;
// 定义 CSocket 类,用于封装 Socket 操作
class CSocket
{
public:
    // 构造函数
    // 参数 _socketType 表示 Socket 的类型,默认为 TCP 类型
    CSocket(SocketEnum::SocketType _socketType = SocketEnum::Tcp);
    // 析构函数,在对象销毁时自动调用,用于释放资源
    ~CSocket();
    // 连接到指定 IP 地址和端口的服务器
    // 参数 ip 是服务器的 IP 地址,port 是服务器监听的端口号
    // 返回值为布尔类型,若连接成功则返回 true,否则返回 false
    bool Connect(const char* ip, int port);
    // 发送数据到连接的服务器
    // 参数 pBuf 是要发送的数据缓冲区,len 是要发送的数据长度
    // 返回值为整数类型,表示实际发送的字节数
    int Send(char* pBuf, int len);
    // 从连接的服务器接收数据
    // 参数 strLen 是接收缓冲区的长度
    // 返回值为整数类型,表示实际接收的字节数
    int Receive(int strLen);
    // 设置 Socket 的阻塞模式
    // 参数 isBlocking 为 true 表示设置为阻塞模式,false 表示设置为非阻塞模式
    // 返回值为布尔类型,若设置成功则返回 true,否则返回 false
    bool SetBlocking(bool isBlocking);
    // 关闭 Socket 连接的指定模式
    // 参数 mode 是 SocketEnum::ShutdownMode 枚举类型,用于指定关闭的模式
    // 返回值为布尔类型,若关闭操作成功则返回 true,否则返回 false
    bool ShutDown(SocketEnum::ShutdownMode mode);
    // 获取接收到的数据
    // 返回值为常量字符指针,指向存储接收数据的缓冲区
    // 该方法使用 const 修饰,表示不会修改对象的状态
    const char* GetData() const;
    // 获取当前 Socket 的错误信息
    // 返回值为 SocketEnum::SocketError 枚举类型,表示具体的错误类型
    SocketEnum::SocketError GetSocketError();
    // 设置 Socket 句柄
    // 参数 socket 是要设置的 Socket 句柄
    void SetSocketHandle(SOCKET socket);
    // 关闭 Socket 连接
    void Close();
    // 判断是否接收到退出指令
    // 返回值为布尔类型,若接收到退出指令则返回 true,否则返回 false
    bool IsExit();
private:
    // 设置 Socket 的错误信息
    // 参数 error 是 SocketEnum::SocketError 枚举类型,用于指定具体的错误类型
    void SetSocketError(SocketEnum::SocketError error);
    // 根据 Windows Sockets 错误码自动设置 Socket 的错误信息
    void SetSocketError(void);
    // 判断 Socket 是否有效
    // 返回值为布尔类型,若 Socket 有效则返回 true,否则返回 false
    bool IsSocketValid(void);
    // Socket 句柄,用于标识 Socket 连接
    SOCKET csocket;
    // 连接状态标志,true 表示已连接,false 表示未连接
    bool isConnected;
    // 服务器地址结构体,用于存储服务器的 IP 地址和端口号等信息
    struct sockaddr_in serverAddress;
    // 存储接收数据的缓冲区指针
    char* buffer;
    // 发送数据的长度
    int sendCount;
    // 接收数据的长度
    int recvCount;
    // 是否为阻塞模式的标志,true 表示阻塞模式,false 表示非阻塞模式
    bool isBlocking;
    // 当前 Socket 的错误信息,用枚举类型表示
    SocketEnum::SocketError socketError;
    // Socket 的类型,用枚举类型表示
    SocketEnum::SocketType socketType;
    // 用于保存函数 WSAStartup 返回的 Windows Sockets 初始化信息
    WSADATA wsa;
};
// 结束防止头文件重复包含的预处理指令
#endif
CSocket.cpp
#include "CSocket.h"
// 构造函数,使用初始化列表对成员变量进行初始化
// _socketType 为传入的套接字类型,默认为 TCP 类型
CSocket::CSocket(SocketEnum::SocketType _socketType):
    csocket(INVALID_SOCKET), // 初始化为无效套接字句柄
    isConnected(false),      // 初始化为未连接状态
    buffer(NULL),            // 接收数据的缓冲区初始化为空
    sendCount(0),            // 发送数据长度初始化为 0
    recvCount(0),            // 接收数据长度初始化为 0
    isBlocking(true),        // 初始化为阻塞模式
    socketError(SocketEnum::InvalidSocket), // 初始化为无效套接字错误
    socketType(_socketType)  // 使用传入的套接字类型进行初始化
{
    // 构造函数体为空,成员变量初始化已在初始化列表完成
}
// 连接到指定 IP 地址和端口的服务器
bool CSocket::Connect(const char* ip, int port)
{
    // 先假设连接会成功
    isConnected = true;
    // 先将错误状态设为成功
    socketError = SocketEnum::Success;
    // 初始化 Windows Sockets DLL
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        // 若初始化失败,设置错误为 WSA 启动错误
        SetSocketError(SocketEnum::WSAStartupError);
        // 标记连接失败
        isConnected = false;
    }
    // 如果 WSA 初始化成功
    if (isConnected)
    {
        // 创建一个 TCP 套接字
        if ((csocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
        {
            // 若套接字创建失败,设置相应错误
            SetSocketError();
            // 标记连接失败
            isConnected = false;
        }
    }
    // 如果套接字创建成功
    if (isConnected)
    {
        // 初始化服务器地址结构体的内存为 0
        memset(&serverAddress, 0, sizeof(sockaddr_in));
        // 设置地址族为 IPv4
        serverAddress.sin_family = AF_INET;
        // 将点分十进制的 IP 地址转换为网络字节序的长整型
        long lip = inet_addr(ip);
        if (lip == INADDR_NONE)
        {
            // 若 IP 地址无效,设置相应错误
            SetSocketError(SocketEnum::InvalidAddress);
            // 标记连接失败
            isConnected = false;
        }
        else
        {
            if (port < 0)
            {
                // 若端口号无效,设置相应错误
                SetSocketError(SocketEnum::InvalidPort);
                // 标记连接失败
                isConnected = false;
            }
            else
            {
                // 设置服务器地址的 IP 部分
                serverAddress.sin_addr.S_un.S_addr = lip;
                // 将端口号转换为网络字节序
                serverAddress.sin_port = htons(port);
                // 尝试连接到服务器
                if (connect(csocket, (sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR)
                {
                    // 若连接失败,设置相应错误
                    SetSocketError();
                    // 标记连接失败
                    isConnected = false;
                }
            }
        }
    }
    // 返回连接结果
    return isConnected;
}
// 设置套接字的阻塞模式
bool CSocket::SetBlocking(bool isBlock)
{
    // 根据传入的参数确定是否阻塞,0 为阻塞,1 为非阻塞
    int block = isBlock? 0 : 1;
    // 使用 ioctlsocket 函数设置套接字的阻塞模式
    if (ioctlsocket(csocket, FIONBIO, (ULONG *)&block) != 0)
    {
        // 若设置失败,返回 false
        return false;
    }
    // 更新成员变量表示的阻塞状态
    isBlocking = isBlock;
    // 设置成功,返回 true
    return true;
}
// 判断是否接收到退出指令("EXIT" 或 "exit" 等)
bool CSocket::IsExit()
{
    // 获取接收到的数据长度
    int len = strlen(buffer);
    int i = 0;
    // 退出指令的长度
    int size = 4;
    if (len == size)
    {
        // 定义退出指令字符串
        char* exit = "EXIT";
        for (i = 0; i < size; i++)
        {
            // 忽略大小写比较字符
            if (buffer[i] != *(exit + i) && buffer[i] - 32 != *(exit + i))
            {
                break;
            }
        }
    }
    // 如果所有字符都匹配,说明接收到退出指令
    return i == size;
}
// 向服务器发送数据
int CSocket::Send(char* pBuf, int len)
{
    // 检查套接字是否有效且已连接
    if (!IsSocketValid() || !isConnected)
    {
        // 若不满足条件,返回 0 表示未发送数据
        return 0;
    }
    // 检查发送缓冲区和长度是否有效
    if (pBuf == NULL || len < 1)
    {
        // 若无效,返回 0 表示未发送数据
        return 0;
    }
    // 调用 send 函数发送数据
    sendCount = send(csocket, pBuf, len, 0);
    if (sendCount <= 0)
    {
        // 若发送失败,设置相应错误
        SetSocketError();
    }
    // 返回实际发送的数据长度
    return sendCount;
}
// 从服务器接收数据
int CSocket::Receive(int strLen)
{
    // 初始化接收数据长度为 0
    recvCount = 0;
    // 检查套接字是否有效且已连接
    if (!IsSocketValid() || !isConnected)
    {
        // 若不满足条件,返回 0 表示未接收数据
        return recvCount;
    }
    // 检查接收缓冲区长度是否有效
    if (strLen < 1)
    {
        // 若无效,返回 0 表示未接收数据
        return recvCount;
    }
    // 若之前有缓冲区,释放它
    if (buffer != NULL)
    {
        delete buffer;
        buffer = NULL;
    }
    // 分配新的接收缓冲区
    buffer = new char[strLen];
    // 设置错误状态为成功
    SetSocketError(SocketEnum::Success);
    while (1)
    {
        // 调用 recv 函数接收数据
        recvCount = recv(csocket, buffer, strLen, 0);
        if (recvCount > 0)
        {
            // 在接收到的数据末尾添加字符串结束符
            buffer[recvCount] = '\0';
            if (IsExit())
            {
                // 若接收到退出指令,释放缓冲区
                delete buffer;
                buffer = NULL;
                // 重置接收数据长度为 0
                recvCount = 0;
                // 跳出循环
                break;
            }
            else
            {
                // 若未接收到退出指令,输出接收到的数据
                cout << buffer << endl;
            }
        }
    }
    // 返回实际接收的数据长度
    return recvCount;
}
// 设置套接字的错误信息
void CSocket::SetSocketError(SocketEnum::SocketError error)
{
    // 将传入的错误信息赋值给成员变量
    socketError = error;
}
// 根据 Windows Sockets 错误码自动设置错误信息
void CSocket::SetSocketError(void)
{
    // 获取最近一次 Windows Sockets 操作的错误码
    int nError = WSAGetLastError();
    switch (nError)
    {
        case EXIT_SUCCESS:
            // 若操作成功,设置错误为成功
            SetSocketError(SocketEnum::Success);
            break;
        case WSAEBADF:
        case WSAENOTCONN:
            // 若套接字未连接或文件描述符无效,设置相应错误
            SetSocketError(SocketEnum::Notconnected);
            break;
        case WSAEINTR:
            // 若操作被中断,设置相应错误
            SetSocketError(SocketEnum::Interrupted);
            break;
        case WSAEACCES:
        case WSAEAFNOSUPPORT:
        case WSAEINVAL:
        case WSAEMFILE:
        case WSAENOBUFS:
        case WSAEPROTONOSUPPORT:
            // 若套接字无效,设置相应错误
            SetSocketError(SocketEnum::InvalidSocket);
            break;
        case WSAECONNREFUSED:
            // 若连接被拒绝,设置相应错误
            SetSocketError(SocketEnum::ConnectionRefused);
            break;
        case WSAETIMEDOUT:
            // 若连接超时,设置相应错误
            SetSocketError(SocketEnum::Timedout);
            break;
        case WSAEINPROGRESS:
            // 若操作正在进行中,设置相应错误
            SetSocketError(SocketEnum::Einprogress);
            break;
        case WSAECONNABORTED:
            // 若连接被中止,设置相应错误
            SetSocketError(SocketEnum::ConnectionAborted);
            break;
        case WSAEWOULDBLOCK:
            // 若操作会阻塞,设置相应错误
            SetSocketError(SocketEnum::Ewouldblock);
            break;
        case WSAENOTSOCK:
            // 若不是套接字操作,设置相应错误
            SetSocketError(SocketEnum::InvalidSocket);
            break;
        case WSAECONNRESET:
            // 若连接被重置,设置相应错误
            SetSocketError(SocketEnum::ConnectionReset);
            break;
        case WSANO_DATA:
            // 若没有数据,设置相应错误
            SetSocketError(SocketEnum::InvalidAddress);
            break;
        case WSAEADDRINUSE:
            // 若地址已被使用,设置相应错误
            SetSocketError(SocketEnum::AddressInUse);
            break;
        case WSAEFAULT:
            // 若指针无效,设置相应错误
            SetSocketError(SocketEnum::InvalidPointer);
            break;
        default:
            // 若为未知错误,设置相应错误
            SetSocketError(SocketEnum::UnknownError);
            break;
    }
}
// 判断套接字是否有效
bool CSocket::IsSocketValid(void)
{
    // 若错误状态为成功,则认为套接字有效
    return socketError == SocketEnum::Success;
}
// 获取当前套接字的错误信息
SocketEnum::SocketError CSocket::GetSocketError()
{
    // 返回成员变量表示的错误信息
    return socketError;
}
// 析构函数,在对象销毁时调用
CSocket::~CSocket()
{
    // 调用 Close 函数关闭套接字并释放资源
    Close();
}
// 关闭套接字连接并释放资源
void CSocket::Close()
{
    // 若有接收缓冲区,释放它
    if (buffer != NULL)
    {
        delete buffer;
        buffer = NULL;
    }
    // 关闭套接字的读写操作
    ShutDown(SocketEnum::Both);
    // 关闭套接字句柄
    if (closesocket(csocket) != SocketEnum::Error)
    {
        // 将套接字句柄设为无效
        csocket = INVALID_SOCKET;
    }
    // 清理 Windows Sockets DLL 占用的资源
    WSACleanup();
}
// 关闭套接字的指定模式(读、写或两者)
bool CSocket::ShutDown(SocketEnum::ShutdownMode mode)
{
    // 调用 shutdown 函数关闭套接字
    SocketEnum::SocketError nRetVal = (SocketEnum::SocketError)shutdown(csocket, SocketEnum::Both);
    // 设置相应的错误信息
    SetSocketError();
    // 根据返回值判断操作是否成功
    return (nRetVal == SocketEnum::Success)? true : false;
}
// 获取接收到的数据
const char* CSocket::GetData() const
{
    // 返回接收数据的缓冲区指针
    return buffer;
}
// 设置套接字句柄
void CSocket::SetSocketHandle(SOCKET socket)
{
    if (socket != SOCKET_ERROR)
    {
        // 若传入的套接字句柄有效,更新成员变量
        csocket = socket;
        // 标记为已连接
        isConnected = true;
        // 设置错误状态为成功
        socketError = SocketEnum::Success;
    }
}
SocketEnum.h
// 防止头文件被重复包含的预处理指令
// 如果还没有定义 __ENUMTYPE_H__ 这个宏,就执行下面的代码块
#ifndef __ENUMTYPE_H__
#define __ENUMTYPE_H__
// 定义一个结构体 SocketEnum,用于封装与套接字操作相关的枚举类型
struct SocketEnum
{
    // 定义一个枚举类型 SocketType,用于表示套接字的类型
    typedef enum 
    {
        // 无效的套接字类型,通常表示未正确初始化或不支持的类型
        Invalid,
        // TCP(传输控制协议)类型的套接字,提供面向连接的、可靠的数据传输
        Tcp,
        // UDP(用户数据报协议)类型的套接字,提供无连接的、不可靠的数据传输
        Udp
    } SocketType;
    // 定义一个枚举类型 SocketError,用于表示套接字操作过程中可能出现的错误
    typedef enum 
    {
        // 通用错误,表示操作失败,但未明确具体原因
        Error = -1,             
        // 操作成功,没有发生错误
        Success = 0,            
        // 无效的套接字,可能是套接字未创建、已关闭或句柄无效
        InvalidSocket,
        // 无效的地址,例如指定的 IP 地址格式错误或不可用
        InvalidAddress,         
        // 无效的端口号,如端口号超出有效范围(0 - 65535)
        InvalidPort,            
        // 连接被拒绝,通常是因为服务器未监听指定端口或拒绝了连接请求
        ConnectionRefused,      
        // 连接超时,在规定时间内未能建立连接
        Timedout,               
        // 操作会阻塞,在非阻塞模式下表示操作不能立即完成
        Ewouldblock,            
        // 套接字未连接,尝试对未连接的套接字进行操作
        Notconnected,           
        // 操作正在进行中,可能是有异步操作未完成
        Einprogress,            
        // 操作被中断,可能是由于系统信号或其他外部因素
        Interrupted,            
        // 连接被中止,可能是网络问题或服务器异常关闭
        ConnectionAborted,      
        // 协议错误,例如使用了不支持的协议或协议参数错误
        ProtocolError,          
        // 无效的缓冲区,如传入的缓冲区为空或大小不足
        InvalidBuffer,          
        // 连接被重置,通常是因为对方异常关闭了连接
        ConnectionReset,        
        // 地址已被使用,尝试绑定一个已经被其他套接字使用的地址和端口
        AddressInUse,           
        // 无效的指针,传入的指针为空或指向无效的内存地址
        InvalidPointer,         
        // Windows Sockets 初始化失败,调用 WSAStartup 函数时出错
        WSAStartupError,
        // 绑定地址和端口失败,通常是因为地址或端口不可用
        BindError,
        // 监听连接失败,调用 listen 函数时出错
        ListenError,
        // 未知错误,无法归类到其他已知的错误类型
        UnknownError
    } SocketError;
    // 定义一个枚举类型 ShutdownMode,用于表示关闭套接字连接的模式
    typedef enum 
    {
        // 只关闭接收功能,仍然可以发送数据
        Receives = 0,   
        // 只关闭发送功能,仍然可以接收数据
        Sends = 1,      
        // 同时关闭接收和发送功能
        Both = 2        
    } ShutdownMode;   
};
// 结束防止头文件重复包含的预处理指令
#endif __ENUMTYPE_H__
3:项目输出结果:

图片

相关文章:

  • nvm 让 Node.js 版本切换更灵活
  • 记录一些面试遇到的问题
  • Linux系统之配置HAProxy负载均衡服务器
  • powermock,mock使用笔记
  • 重生之我在 CSDN 学习 KMP 算法
  • Linux——Docker容器内MySQL密码忘记了如何查看
  • 信息管理之信息的萃取方法--使用渐进归纳法逐步提取高可见性笔记
  • os-copilot安装和使用体验测评
  • PHP 矩形面积和周长的程序(Program for Area And Perimeter Of Rectangle)
  • 前端网络安全面试题及答案
  • MATLAB实现遗传算法优化风电_光伏_光热_储热优化
  • Mysql创建库、表练习
  • RoboDexVLM:基于视觉-语言模型的任务规划和运动控制,实现灵巧机器人操作
  • 中原银行:从“小机+传统数据库”升级为“OceanBase+通用服务器”,30 +系统成功上线|OceanBase DB大咖说(十五)
  • pypi 配置国内镜像
  • IDEA Generate POJOs.groovy 踩坑小计 | 生成实体 |groovy报错
  • 数据库安装
  • 测试理论快速入门
  • 记录Linux安装mysql8
  • vue3页面html导出word文档
  • 2人恶意传播刘国梁谣言被处罚,媒体:以法律利剑劈谣斩邪,加快推进依法治体
  • 联合国:欢迎俄乌伊斯坦布尔会谈,希望实现全面停火
  • 刘小涛任江苏省委副书记
  • 六省会共建交通枢纽集群,中部离经济“第五极”有多远?
  • 上海市税务局:收到对刘某某存在涉税问题的举报,正依法依规办理
  • 李家超:明日起香港特区护照持有人可免签入境阿联酋