河南便宜网站建设价格低舆情监测系统
一:一对一服务器与客户端模式
项目简介: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));// 设置地址族为IPv4serverAddress.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);}// 获取服务器IPstruct 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));// 设置地址族为 IPv4serverAddress.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,否则返回 falsebool 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,否则返回 falsebool 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 方法启动服务器,监听端口号为 202588bool 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,否则返回 falsebool Connect(const char* ip, int port);// 发送数据到连接的服务器// 参数 pBuf 是要发送的数据缓冲区,len 是要发送的数据长度// 返回值为整数类型,表示实际发送的字节数int Send(char* pBuf, int len);// 从连接的服务器接收数据// 参数 strLen 是接收缓冲区的长度// 返回值为整数类型,表示实际接收的字节数int Receive(int strLen);// 设置 Socket 的阻塞模式// 参数 isBlocking 为 true 表示设置为阻塞模式,false 表示设置为非阻塞模式// 返回值为布尔类型,若设置成功则返回 true,否则返回 falsebool SetBlocking(bool isBlocking);// 关闭 Socket 连接的指定模式// 参数 mode 是 SocketEnum::ShutdownMode 枚举类型,用于指定关闭的模式// 返回值为布尔类型,若关闭操作成功则返回 true,否则返回 falsebool 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,否则返回 falsebool operator==(const CSocket* socket);// 判断 Socket 是否已经退出连接// 返回值为布尔类型,若已退出则返回 true,否则返回 falsebool IsExit();
private:// 设置 Socket 的错误信息// 参数 error 是 SocketEnum::SocketError 枚举类型,用于指定具体的错误类型void SetSocketError(SocketEnum::SocketError error);// 设置 Socket 的错误信息,根据当前 Socket 的状态自动判断错误类型void SetSocketError(void);// 判断 Socket 是否有效// 返回值为布尔类型,若有效则返回 true,否则返回 falsebool 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) {// 假设连接成功,设置连接状态为 trueisConnected = true;// 初始化错误信息为成功状态socketError = SocketEnum::Success;// 初始化 Windows Sockets DLLif (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {// 如果初始化失败,设置错误信息为 WSAStartupErrorSetSocketError(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));// 设置地址族为 IPv4serverAddress.sin_family = AF_INET;// 将 IP 地址字符串转换为网络字节序的长整型long lip = inet_addr(ip);// 检查 IP 地址是否合法if (lip == INADDR_NONE) {// 如果 IP 地址不合法,设置错误信息为 InvalidAddressSetSocketError(SocketEnum::InvalidAddress);// 标记连接失败isConnected = false;} else {// 检查端口号是否合法if (port < 0) {// 如果端口号不合法,设置错误信息为 InvalidPortSetSocketError(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) {// 如果设置失败,返回 falsereturn false;}// 更新 isBlocking 成员变量isBlocking = isBlock;// 设置成功,返回 truereturn 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) {// 初始化接收数据长度为 0recvCount = 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;// 重置接收数据长度为 0recvCount = 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:// 如果是未连接或无效文件描述符的错误,设置错误信息为 NotconnectedSetSocketError(SocketEnum::Notconnected);break;case WSAEINTR:// 如果是操作被中断的错误,设置错误信息为 InterruptedSetSocketError(SocketEnum::Interrupted);break;case WSAEACCES:case WSAEAFNOSUPPORT:case WSAEINVAL:case WSAEMFILE:case WSAENOBUFS:case WSAEPROTONOSUPPORT:// 如果是无效套接字相关的错误,设置错误信息为 InvalidSocketSetSocketError(SocketEnum::InvalidSocket);break;case WSAECONNREFUSED:// 如果是连接被拒绝的错误,设置错误信息为 ConnectionRefusedSetSocketError(SocketEnum::ConnectionRefused);break;case WSAETIMEDOUT:// 如果是连接超时的错误,设置错误信息为 TimedoutSetSocketError(SocketEnum::Timedout);break;case WSAEINPROGRESS:// 如果是操作正在进行中的错误,设置错误信息为 EinprogressSetSocketError(SocketEnum::Einprogress);break;case WSAECONNABORTED:// 如果是连接被中止的错误,设置错误信息为 ConnectionAbortedSetSocketError(SocketEnum::ConnectionAborted);break;case WSAEWOULDBLOCK:// 如果是操作会阻塞的错误,设置错误信息为 EwouldblockSetSocketError(SocketEnum::Ewouldblock);break;case WSAENOTSOCK:// 如果不是套接字的错误,设置错误信息为 InvalidSocketSetSocketError(SocketEnum::InvalidSocket);break;case WSAECONNRESET:// 如果是连接被重置的错误,设置错误信息为 ConnectionResetSetSocketError(SocketEnum::ConnectionReset);break;case WSANO_DATA:// 如果是没有数据的错误,设置错误信息为 InvalidAddressSetSocketError(SocketEnum::InvalidAddress);break;case WSAEADDRINUSE:// 如果是地址已被使用的错误,设置错误信息为 AddressInUseSetSocketError(SocketEnum::AddressInUse);break;case WSAEFAULT:// 如果是无效指针的错误,设置错误信息为 InvalidPointerSetSocketError(SocketEnum::InvalidPointer);break;default:// 如果是其他未知错误,设置错误信息为 UnknownErrorSetSocketError(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,否则返回 falsereturn (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_LENcsocket->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,否则返回 falsebool Connect(const char* ip, int port);// 发送数据到连接的服务器// 参数 pBuf 是要发送的数据缓冲区,len 是要发送的数据长度// 返回值为整数类型,表示实际发送的字节数int Send(char* pBuf, int len);// 从连接的服务器接收数据// 参数 strLen 是接收缓冲区的长度// 返回值为整数类型,表示实际接收的字节数int Receive(int strLen);// 设置 Socket 的阻塞模式// 参数 isBlocking 为 true 表示设置为阻塞模式,false 表示设置为非阻塞模式// 返回值为布尔类型,若设置成功则返回 true,否则返回 falsebool SetBlocking(bool isBlocking);// 关闭 Socket 连接的指定模式// 参数 mode 是 SocketEnum::ShutdownMode 枚举类型,用于指定关闭的模式// 返回值为布尔类型,若关闭操作成功则返回 true,否则返回 falsebool 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,否则返回 falsebool IsExit();
private:// 设置 Socket 的错误信息// 参数 error 是 SocketEnum::SocketError 枚举类型,用于指定具体的错误类型void SetSocketError(SocketEnum::SocketError error);// 根据 Windows Sockets 错误码自动设置 Socket 的错误信息void SetSocketError(void);// 判断 Socket 是否有效// 返回值为布尔类型,若 Socket 有效则返回 true,否则返回 falsebool 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), // 发送数据长度初始化为 0recvCount(0), // 接收数据长度初始化为 0isBlocking(true), // 初始化为阻塞模式socketError(SocketEnum::InvalidSocket), // 初始化为无效套接字错误socketType(_socketType) // 使用传入的套接字类型进行初始化
{// 构造函数体为空,成员变量初始化已在初始化列表完成
}
// 连接到指定 IP 地址和端口的服务器
bool CSocket::Connect(const char* ip, int port)
{// 先假设连接会成功isConnected = true;// 先将错误状态设为成功socketError = SocketEnum::Success;// 初始化 Windows Sockets DLLif (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){// 初始化服务器地址结构体的内存为 0memset(&serverAddress, 0, sizeof(sockaddr_in));// 设置地址族为 IPv4serverAddress.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){// 若设置失败,返回 falsereturn false;}// 更新成员变量表示的阻塞状态isBlocking = isBlock;// 设置成功,返回 truereturn 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)
{// 初始化接收数据长度为 0recvCount = 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;// 重置接收数据长度为 0recvCount = 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__