基于C++实现Socket交互式服务端
作者:程序员老舅
在 Windows 操作系统中,原生提供了强大的网络编程支持,允许开发者使用 Socket API 进行网络通信,通过 Socket API,开发者可以创建、连接、发送和接收数据,实现网络通信。本文将深入探讨如何通过调用原生网络 API 实现同步远程通信,并介绍了一个交互式 Socket 类的封装,提升了编写交互式服务器的便利性。
1. 交互式套接字类
为了更好地利用原生网络 API,我们引入了一个交互式 Socket 类的封装。这个类抽象了底层的网络细节,提供了简单而强大的接口,使得服务器端的交互式功能更容易实现。我们将详细介绍这个类的设计和使用方法。
MySocket 类是一个 C++ 套接字类,封装了在 Windows 平台上使用原生网络 API 进行同步远程通信的基本功能,该类需要使用多字节编码模式,服务端与客户端均需要引入此类,在项目头文件中均需要新建MySocket.hpp文件。
完整代码如下所示;
#pragma once #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") class MySocket { protected: SOCKET m_hSocket; public: // 获取对端Socket用户IP端口等 BOOL GetPeerName(char* rSocketAddress, UINT& rSocketPort) { sockaddr_in name = { AF_INET }; int lenname = sizeof(name); if (getpeername(m_hSocket, (sockaddr*)&name, &lenname) < 0) return false; strcpy(rSocketAddress, inet_ntoa(name.sin_addr)); rSocketPort = htons(name.sin_port); return true; } // 获取本机Socket用户IP端口等 BOOL GetSockName(char* rSocketAddress, UINT& rSocketPort) { sockaddr_in name = { AF_INET }; int lenname = sizeof(name); if (getsockname(m_hSocket, (sockaddr*)&name, &lenname) < 0) return false; strcpy(rSocketAddress, inet_ntoa(name.sin_addr)); rSocketPort = htons(name.sin_port); return true; } // 获取当前用户SocketID BOOL GetSocketID() { return m_hSocket; } // 创建套接字 BOOL Create(UINT nSocketPort = 0, int nSockType = SOCK_STREAM, LPCTSTR lpszSocketAddress = NULL) { // 创建套接字 m_hSocket = socket(AF_INET, nSockType, 0); if (m_hSocket == INVALID_SOCKET) return false; // 设置IP地址和端口 sockaddr_in sa = { AF_INET }; sa.sin_port = htons(nSocketPort); if (lpszSocketAddress) sa.sin_addr.s_addr = inet_addr(lpszSocketAddress); // 绑定套接字和IP地址端口 return !bind(m_hSocket, (sockaddr*)&sa, sizeof(sa)); } // 接受客户请求 BOOL Accept(MySocket& rConnectedSock, LPSTR szIp = NULL, UINT* nPort = NULL) { sockaddr_in sa = { AF_INET }; int nLen = sizeof(sa); rConnectedSock.m_hSocket = accept(this->m_hSocket, (sockaddr*)&sa, &nLen); if (rConnectedSock.m_hSocket == INVALID_SOCKET) return false; if (szIp) strcpy(szIp, inet_ntoa(sa.sin_addr)); if (nPort) *nPort = htons(sa.sin_port); return true; } // 连接服务端 BOOL Connection(LPCSTR lpszHostAddress, UINT nPort) { sockaddr_in sa = { AF_INET }; sa.sin_port = htons(nPort); sa.sin_addr.s_addr = inet_addr(lpszHostAddress); return !connect(m_hSocket, (sockaddr*)&sa, sizeof(sa)); } // 侦听 BOOL Listen(int nConnectionBacklog = 5) { return !listen(m_hSocket, nConnectionBacklog); } // 逐条发送 int Send(const void* lpBuf, int nBufLen, int nFlags = 0) { return send(m_hSocket, (LPCSTR)lpBuf, nBufLen, nFlags); } // 发送整个缓冲区 int SendTo(const void* lpBuf, int nBufLen, UINT nHostPort, LPCSTR lpszHostAddress = NULL, int nFlags = 0) { sockaddr_in to = { AF_INET }; to.sin_port = htons(nHostPort); to.sin_addr.s_addr = inet_addr(lpszHostAddress); return sendto(m_hSocket, (LPCSTR)lpBuf, nBufLen, nFlags, (sockaddr*)&to, sizeof(to)); } // 逐条接收 int Receive(void* lpBuf, int nBufLen, int nFlags = 0) { return recv(m_hSocket, (LPTSTR)lpBuf, nBufLen, nFlags); } // 接收整个缓冲区 int ReceiveFrom(void* lpBuf, int nBufLen, char* rSocketAddress, UINT& rSocketPort, int nFlags = 0) { sockaddr_in from = { AF_INET }; int lenFrom = sizeof(from); int n = recvfrom(m_hSocket, (LPSTR)lpBuf, nBufLen, nFlags, (sockaddr*)&from, &lenFrom); strcpy(rSocketAddress, inet_ntoa(from.sin_addr)); rSocketPort = htons(from.sin_port); return n; } // 关闭套接字 void Close() { closesocket(m_hSocket); m_hSocket = INVALID_SOCKET; } MySocket() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); m_hSocket = INVALID_SOCKET; } ~MySocket() { Close(); } };
以下是对该类的概括:
类名:MySocket
功能:提供了基本的网络通信功能,包括创建套接字、获取对端和本机的信息、接受客户端连接、连接服务端、监听连接请求、发送和接收数据。
成员变量:
SOCKET m_hSocket:套接字句柄,用于标识一个套接字。
成员函数:
Create:创建套接字,并可指定类型、本地端口和地址。
Accept:接受客户请求,返回连接的套接字。
Connection:连接到服务端。
Listen:开始监听连接请求。
Send:逐条发送数据。
SendTo:发送整个缓冲区到指定地址。
Receive:逐条接收数据。
ReceiveFrom:接收整个缓冲区,并获取发送端地址和端口。
Close:关闭套接字。
初始化和清理:
构造函数 MySocket:初始化 Winsock 库和套接字句柄。
析构函数 ~MySocket:关闭套接字。
使用注意事项:
适用于简单的同步网络通信场景。
该类提供了一些基本的网络编程功能,适合用于创建简单的服务器端和客户端。需注意,这是一个同步实现的套接字类,适用于一些较为简单的网络通信需求。
2. 实现简单的通信
通过具体的代码示例,我们将演示如何使用交互式 Socket 类在 Windows 操作系统上实现同步远程通信。代码将包括服务器端和客户端的实现,以及它们之间的交互过程。通过这些示例,读者将更好地理解如何在实际项目中应用这些概念。
2.1 服务端流程
如下代码是一个简单的服务端程序,通过 MySocket 类建立基于 TCP 协议的服务器,通过sock.Create()创建套接字,然后通过sock.Accept()接收套接字,当有新的套接字连入时自动调用_beginthread()函数开启一个子线程维持套接字的运行,每一个子线程内部则都由ClientPro()函数来实现交互。
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <iostream> #include <process.h> #include "MySocket.hpp" using namespace std; void ClientPro(void* ptr) { // 初始化 MySocket* pSock = (MySocket*)ptr; MySocket server_socket = *pSock; server_socket.Send((const char *)"Welcome to LyServer", 19); // 获取客户端信息 char sIp[20]; UINT nPort; server_socket.GetPeerName(sIp, nPort); while (true) { char szBuffer[4096] = { 0 }; // 接收客户返回消息 int ref = server_socket.Receive(szBuffer, sizeof(szBuffer)); if (ref <= 0) { std::cout << "客户: " << sIp << ":" << nPort << " [已断开]" << std::endl; break; } std::cout << "地址: " << sIp << ":" << nPort << " 接收命令: " << szBuffer << std::endl; // 选择不同的命令 if (strcmp(szBuffer, "list\n") == 0) { std::cout << "输出文件" << std::endl; } else if (strcmp(szBuffer, "download\n") == 0) { std::cout << "下载文件" << std::endl; } else if (strcmp(szBuffer, "upload\n") == 0) { std::cout << "上传文件" << std::endl; } // 返回给客户端 server_socket.Send((char*)"ok", 2); } } int main(int argc, char *argv[]) { MySocket sock; if (!sock.Create(8233, SOCK_STREAM, "127.0.0.1")) { return -1; } // 获取本机信息 char sSevIp[20]; UINT nSevPort; sock.GetSockName(sSevIp, nSevPort); std::cout << "服务端: " << sSevIp << ":" << nSevPort << " 服务器启动成功" << std::endl; sock.Listen(5); // 获取客户端信息 char sIp[20]; UINT nPort; MySocket ptr; while (true) { // 当有新用户进来自动创建一个线程来维持会话 sock.Accept(ptr, sIp, &nPort); std::cout << "客户: " << sIp << ":" << nPort << " [已登录]" << std::endl; // 多线程 _beginthread(ClientPro, 0, &ptr); } return 0; }
以下是对该代码的概括:
功能:实现一个简单的基于 TCP 的服务器,监听指定端口(8233),接受客户端连接,创建一个线程处理每个客户端的会话。
主要函数和过程:
ClientPro 函数:处理每个客户端的会话。向客户端发送欢迎消息,接收客户端发送的命令,根据不同的命令执行相应的操作,并向客户端发送响应。该函数通过多线程在后台运行,使得服务器能够同时处理多个客户端。
main 函数:在主线程中创建 MySocket 类实例 sock,并调用 Create 函数创建服务器套接字。然后,通过 Listen 函数监听客户端连接。在循环中,通过 Accept 函数接受客户端连接,并为每个客户端创建一个新线程,用于处理客户端的会话。
通信协议:客户端和服务器之间通过简单的文本协议进行通信。客户端发送不同的命令(“list”、“download”、“upload”),服务器接收命令并执行相应的操作,然后向客户端发送响应(“ok”)。
线程创建:使用 _beginthread 函数在每个新连接上创建一个线程,用于处理该客户端的会话。
2.2 客户端流程
如下代码是一个简单的客户端程序,通过 MySocket 类实现与服务端的基于 TCP 协议的通信,通过sock.Connection()建立套接字链接,通过sock.Receive()接收数据,通过sock.Send()发送数据,其运行原理与原生套接字写法保持一致。
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <iostream> #include "MySocket.hpp" using namespace std; int main(int argc, char* argv[]) { MySocket sock; if (!sock.Create(0, SOCK_STREAM)) { return -1; } // 获取本机信息 char sClientIp[20]; UINT nClientPort; sock.GetSockName(sClientIp, nClientPort); std::cout << "服务端: " << sClientIp << ":" << nClientPort << " 服务器启动成功" << std::endl; if (!sock.Connection("127.0.0.1", 8233)) { cout << "连接服务器失败" << GetLastError() << endl; return -1; } char szBuffer[4096] = { 0 }; int ref = sock.Receive(szBuffer, sizeof(szBuffer)); szBuffer[ref] = 0; std::cout << "服务端回应: " << szBuffer << std::endl; while (true) { // 循环接受输入 input: memset(szBuffer, 0, 4096); std::cout << "Input CMD > "; // 接收输入命令 int inputLine = 0; while ((szBuffer[inputLine++] = getchar()) != '\n'); if (strlen(szBuffer) == 1) goto input; // 发送数据 sock.Send(szBuffer, 4096, 0); // 接收回显 memset(szBuffer, 0, 4096); sock.Receive(szBuffer, 4096, 0); std::cout << "服务端回显: " << szBuffer << std::endl; } sock.Close(); return 0; }
以下是对该代码的概括:
功能:实现一个基于 TCP 的客户端,连接到指定 IP 地址和端口(127.0.0.1:8233),与服务器建立连接后,可以输入命令并发送到服务器,接收并显示服务器的回显。
主要函数和过程:
main 函数:在主线程中创建 MySocket 类实例 sock,并调用 Create 函数创建客户端套接字。然后,通过 Connection 函数连接到服务器。接着,通过 Receive 函数接收服务器发送的欢迎消息,并显示在控制台。
在一个无限循环中,通过标准输入接收用户输入的命令,将命令发送到服务器,然后接收并显示服务器的回显。
通信协议:客户端和服务器之间通过简单的文本协议进行通信。客户端发送用户输入的命令,服务器执行命令并将结果回显给客户端。
输入循环:通过一个无限循环,不断接收用户输入的命令,并发送到服务器。如果用户输入空命令,程序会跳转回 input 标签重新接收输入。
错误处理:在连接服务器失败时,通过 GetLastError() 输出详细错误信息。
关闭套接字:在程序结束时,通过 sock.Close() 关闭套接字。
依次运行服务端和客户端,然后当客户端连接成功后此时的服务端即可收到连接请求,此时客户端可以执行各类简单的命令,如下图所示;
3.实现登录服务器
上述代码只是一个简单的演示案例,用来演示如何使用套接字编写交互程序,如下我们将继续完善这段代码,实现一个简单的带有登录功能的登录服务器程序,使用户可以在执行命令前具备简单的登录认证功能。
3.1 服务端流程
如下代码是一个简单的基于 Windows 的多线程服务器程序,通过 MySocket 类实现与客户端的基于 TCP 协议的通信,在交互模式下用户可输入多种命令,登录登出以及登陆后的命令执行功能。
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <iostream> #include <process.h> #include <vector> #include "MySocket.hpp" using namespace std; // 登录状态记录 typedef struct { char UserName[32]; int SocketID; }loginPool; // ------------------------------------------------------------------------ // 用户登录验证代码部分 std::vector<loginPool> login_pool_vect; // 检查用户ID是否存在与容器内,如果存在则返回用户名 bool is_login(std::vector<loginPool> &ptr, int socket_id) { for (int x = 0; x < ptr.size(); x++) { if (ptr[x].SocketID == socket_id) { return true; } } return false; } // 用户登录验证 bool login(char *username, char *password, int socket_id) { if ((strcmp(username, "lyshark") == 0) && (strcmp(password, "123123") == 0)) { // 如果在则增加一个socket登录标志 loginPool pool_ptr; pool_ptr.SocketID = socket_id; strcpy(pool_ptr.UserName, "lyshark"); login_pool_vect.push_back(pool_ptr); return true; } else if ((strcmp(username, "admin") == 0) && (strcmp(password, "123456") == 0)) { // 如果在则增加一个socket登录标志 loginPool pool_ptr; pool_ptr.SocketID = socket_id; strcpy(pool_ptr.UserName, "lyshark"); login_pool_vect.push_back(pool_ptr); return true; } return false; } // 根据传入ID从容器内弹出一个节点 bool logout(std::vector<loginPool> &ptr, int socket_id) { for (vector<loginPool>::iterator it = ptr.begin(); it != ptr.end(); it++) { if (it->SocketID == socket_id) { // 弹出指定结构体 ptr.erase(it); return true; } } return false; } // ------------------------------------------------------------------------ // 响应客户端的子线程(主要功能实现部分) void ClientPro(void* ptr) { // 初始化 MySocket* pSock = (MySocket*)ptr; MySocket server_socket = *pSock; server_socket.Send((const char *)"Welcome to LyShark Mini Server", 31); // 获取客户端信息 char sIp[20]; UINT nPort; server_socket.GetPeerName(sIp, nPort); while (true) { char szBuffer[4096] = { 0 }; int sid = pSock->GetSocketID(); int ref = server_socket.Receive(szBuffer, sizeof(szBuffer)); if (ref <= 0) { logout(login_pool_vect, sid); std::cout << "客户: " << sIp << ":" << nPort << " [已断开]" << std::endl; break; } std::cout << "地址: " << sIp << ":" << nPort << " 接收命令: " << szBuffer << std::endl; // 用户登录 if (strcmp(szBuffer, "login\n") == 0) { char recv_username[32] = { 0 }; char recv_password[32] = { 0 }; // 接收用户名和密码 pSock->Receive(recv_username, 32, 0); pSock->Receive(recv_password, 32, 0); // 验证登录状态 bool login_flag = login(recv_username, recv_password, sid); if (login_flag == TRUE) { std::cout << "用户: " << recv_username << " 已登录" << std::endl; pSock->Send("已登录", sizeof("已登录"), 0); } else { pSock->Send("账号或密码错误", sizeof("账号或密码错误"), 0); } } // 用户登出 else if (strcmp(szBuffer, "logout\n") == 0) { // 验证是否登录成功 int login_flag = is_login(login_pool_vect, sid); if (login_flag == TRUE) { std::cout << "用户已登出" << std::endl; logout(login_pool_vect, sid); pSock->Send("用户已登出", sizeof("用户已登出"), 0); } else { std::cout << "请先登录" << std::endl; pSock->Send("请先登录", sizeof("请先登录"), 0); } } // 遍历本机文件 else if (strcmp(szBuffer, "list\n") == 0) { // 验证是否登录成功 int login_flag = is_login(login_pool_vect, sid); if (login_flag == TRUE) { std::cout << "用户已登录,输出本机文件" << std::endl; pSock->Send("认证通过", sizeof("认证通过"), 0); // 循环输出数据包 for (int x = 0; x < 10; x++) { char sz[1024] = { 0 }; sprintf(sz, "count -> %d", x); pSock->Send(sz, sizeof(sz), 0); } } else { std::cout << "请先登录" << std::endl; pSock->Send("请先登录", sizeof("请先登录"), 0); } } } } int main(int argc, char *argv[]) { MySocket sock; if (!sock.Create(8233, SOCK_STREAM, "127.0.0.1")) { return -1; } // 获取本机信息 char sSevIp[20]; UINT nSevPort; sock.GetSockName(sSevIp, nSevPort); std::cout << "服务端: " << sSevIp << ":" << nSevPort << " 服务器启动成功" << std::endl; sock.Listen(5); // 获取客户端信息 char sIp[20]; UINT nPort; MySocket ptr; while (true) { sock.Accept(ptr, sIp, &nPort); std::cout << "客户: " << sIp << ":" << nPort << " [已登录]" << std::endl; // 多线程 _beginthread(ClientPro, 0, &ptr); } return 0; }
以下是对该代码的概括:
功能:
通过 MySocket 类实现基于 TCP 协议的多线程服务器,可以处理多个客户端的连接。
实现了用户登录验证功能,支持用户登录、登出和查看本机文件列表的操作。
主要结构和功能:
登录状态记录结构体 (loginPool):记录用户登录状态,包括用户名和套接字 ID。
用户登录验证相关函数:
is_login:检查指定套接字 ID 是否已登录。
login:验证用户名和密码,如果验证通过则将用户信息加入登录池。
logout:根据套接字 ID 从登录池中移除用户。
子线程主要处理函数 ClientPro:
初始化后发送欢迎消息给客户端。
接收客户端命令,处理用户登录、登出和查看本机文件列表的请求。
针对不同的命令进行相应的处理和回复。
主线程 main:
创建服务器套接字,并通过 Create 函数创建服务器套接字。
获取本机信息,包括 IP 地址和端口,并显示在控制台。
通过 Listen 函数监听客户端连接。
接受客户端连接,创建子线程处理每个客户端连接。
通信协议:服务器与客户端之间通过简单的文本协议进行通信,支持用户登录、登出和查看本机文件列表的操作。
多线程处理:通过 _beginthread 创建子线程处理每个客户端的连接,实现了多客户端并发处理。
用户登录验证:支持用户登录验证功能,通过用户名和密码验证用户身份,记录登录状态,处理用户登录、登出的请求。
3.2 客户端流程
如下代码是一个基于 Windows 的客户端程序,通过 MySocket 类实现与服务器的基于 TCP 协议的通信。
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <iostream> #include "MySocket.hpp" using namespace std; int main(int argc, char* argv[]) { MySocket sock; if (!sock.Create(0, SOCK_STREAM)) { return -1; } // 获取本机信息 char sClientIp[20]; UINT nClientPort; sock.GetSockName(sClientIp, nClientPort); if (!sock.Connection("127.0.0.1", 8233)) { cout << "连接服务器失败" << GetLastError() << endl; return -1; } char szBuffer[4096] = { 0 }; int ref = sock.Receive(szBuffer, sizeof(szBuffer)); szBuffer[ref] = 0; std::cout << "服务端回应: " << szBuffer << std::endl; while (true) { input: memset(szBuffer, 0, 4096); std::cout << "CMD > "; // 发送命令 int inputLine = 0; while ((szBuffer[inputLine++] = getchar()) != '\n'); if (strlen(szBuffer) == 1) goto input; // 执行登录 if (strcmp(szBuffer, "login\n") == 0) { // 发送命令 sock.Send(szBuffer, 4096, 0); char input_username[32] = { 0 }; char input_password[32] = { 0 }; // 发送用户名 printf("用户名: "); scanf("%s", &input_username); sock.Send(input_username, 32, 0); // 发送密码 printf("密码: "); scanf("%s", &input_password); sock.Send(input_password, 32, 0); // 获取登录状态 char recv_message[64] = { 0 }; sock.Receive(recv_message, 64, 0); std::cout << recv_message << std::endl; } // 登出用户 else if (strcmp(szBuffer, "logout\n") == 0) { // 发送命令 sock.Send(szBuffer, 4096, 0); // 获取返回消息 char recv_message[64] = { 0 }; sock.Receive(recv_message, 64, 0); std::cout << recv_message << std::endl; } // 遍历本机文件 else if (strcmp(szBuffer, "list\n") == 0) { // 发送命令 sock.Send(szBuffer, 4096, 0); // 获取返回消息 char recv_message[64] = { 0 }; sock.Receive(recv_message, 64, 0); std::cout << recv_message << std::endl; if (strcmp(recv_message, "请先登录") == 0) { goto input; } // 循环接收数据包 for (int x = 0; x < 10; x++) { char sz[1024] = { 0 }; sock.Receive(sz, 1024, 0); std::cout << sz << std::endl; } } } sock.Close(); return 0; }
以下是对该代码的概括:
功能:
通过 MySocket 类实现基于 TCP 协议的客户端,可以与服务器进行通信。
支持用户通过命令行输入与服务器进行简单的交互,包括登录、登出和查看本机文件列表的操作。
主要结构和功能:
用户交互循环:
使用一个循环,通过命令行输入命令,将命令发送给服务器,并根据服务器的回应进行相应的操作。
支持登录、登出和查看本机文件列表的操作。
命令处理:
对用户输入的不同命令,通过 sock.Send 将命令发送给服务器,并通过 sock.Receive 接收服务器的回应。
具体命令包括登录、登出和查看本机文件列表。
登录交互:
当用户输入 “login” 命令时,程序会提示用户输入用户名和密码,并将输入的用户名和密码发送给服务器进行登录验证。
接收服务器的回应,输出相应的登录状态信息。
登出交互:
当用户输入 “logout” 命令时,程序向服务器发送登出命令,接收服务器的回应并输出相应的信息。
查看本机文件列表交互:
当用户输入 “list” 命令时,程序向服务器发送查看本机文件列表的命令,接收服务器的回应并输出相应的信息。
如果用户未登录,则输出 “请先登录” 提示,并继续等待用户输入。
通信协议:客户端与服务器之间通过简单的文本协议进行通信,服务器回应的信息通过控制台输出。
与之前的程序不同,这段代码增加了简单的用户认证模式,当用户直接执行命令时则会提示客户端请先登录,无法执行命令;
此时通过login命令,并输入用户名lyshark密码123123则会提示已登录,此时就可以执行任意的命令参数了,如下图所示,当结束时还需要使用logout退出当前会话;
以上就是基于C++实现Socket交互式服务端的详细内容,更多关于C++ Socket交互式服务端的资料请关注脚本之家其它相关文章!