“深入浅出”系列之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__