主页 > 手机  > 

【网络编程】之TCP实现客户端远程控制服务器端及断线重连

【网络编程】之TCP实现客户端远程控制服务器端及断线重连

【网络编程】之TCP实现客户端远程控制服务器端及断线重连 TCP网络通信实现客户端简单远程控制主机基本功能演示通信过程代码实现服务器模块执行命令模块popen系列函数 客户端模块服务器主程序 windows作为客户端与服务器通信#pragma comment介绍 客户端使用状态机断线重连代码实现运行结果

TCP网络通信实现客户端简单远程控制主机 基本功能演示

客户端与服务器端连接后,可以通过Linux中的指令来控制它:

但是为了防止客户端恶意破坏服务器,我们必须创建一个配置文件,只有在这个配置文件里的命令,客户端才能执行。

通信过程 服务器端创建监听套接字,监听客户端,等待客户端连接。客户端发起连接请求。连接成功,开始通信。客户端发送命令。服务器端接收到命令,并创建一个新线程,线程在线程调用的函数中设置为分离状态,主线程不需要对子线程等待。新线程执行一系列函数后给客户端返回执行结果。 代码实现 服务器模块

TcpServer.hpp:

#pragma once #include<unistd.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #include<sys/types.h> #include<cstdio> #include<cstdlib> #include"Log.hpp" #include<functional> #include"ThreadPool.hpp" #include"InetAddr.hpp" // 定义错误码 enum { SOCKETERROR = 1, // 套接字创建失败 BINDERROR, // 套接字绑定失败 USAGEERROR // 用法错误 }; // 定义服务函数的类型别名 using funcservice = function<void(int sockfd, InetAddr addr)>; // 用于多线程任务的数据结构 struct Threaddata { int _sockfd; // 客户端连接的套接字 InetAddr _addr; // 客户端的地址信息 funcservice _exec; // 执行的服务函数 Threaddata(int sockfd, InetAddr addr, funcservice exec) : _sockfd(sockfd), _addr(addr), _exec(exec) {} }; // 定义业务逻辑函数类型别名 using funcexec = function<string(const string&)>; // 定义 TcpServer 类 class TcpServer { private: int _listensock; // 监听套接字 uint16_t _port; // 监听端口号 bool _is_running; // 服务器运行状态 funcexec _exec; // 业务逻辑处理函数 public: // 构造函数,初始化成员变量 TcpServer(uint16_t port, funcexec exec) : _listensock(-1), _port(port), _is_running(false), _exec(exec) {} // 初始化服务器,包括创建套接字、绑定地址和监听 void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字 if (_listensock == -1) { LOG(FATAL, "socket error"); exit(1); // 套接字创建失败,退出 } LOG(INFO, "socket success"); struct sockaddr_in addr; addr.sin_family = AF_INET; // 使用 IPv4 addr.sin_port = htons(_port); // 设置端口号(网络字节序) addr.sin_addr.s_addr = INADDR_ANY; // 监听本机所有网卡 // 绑定地址 if (bind(_listensock, (struct sockaddr*)&addr, sizeof(addr)) == -1) { LOG(FATAL, "bind error"); exit(1); // 绑定失败,退出 } LOG(INFO, "bind success"); // 开始监听,最大连接数为 5 if (listen(_listensock, 5) == -1) { LOG(FATAL, "listen error"); exit(1); // 监听失败,退出 } } // 服务函数,处理客户端请求 void Service(int sockfd, InetAddr addr) { LOG(INFO, "new connect: %s:%d", inet_ntoa(addr.addr().sin_addr), ntohs(addr.addr().sin_port)); while (_is_running) { char buffer[1024]; // 接收缓冲区 memset(buffer, 0, sizeof(buffer)); // 清空缓冲区 int n = recv(sockfd, buffer, sizeof(buffer), 0); // 接收数据 string sender = "[" + addr.ip() + ":" + to_string(addr.port()) + "]#"; // 记录客户端信息 if (n == -1) { perror("recv"); break; } else if (n == 0) { LOG(INFO, "%sclient close", sender.c_str()); break; // 客户端断开连接 } else { buffer[n] = 0; // 确保字符串以 '\0' 结束 LOG(INFO, "%s%s", sender.c_str(), buffer); // 执行业务逻辑函数 string result = _exec(buffer); // 返回处理结果给客户端 string echoserver = "[echo server]#\n" + result; send(sockfd, echoserver.c_str(), echoserver.size(), 0); } } close(sockfd); // 关闭客户端连接 } // 线程入口函数 static void* ThreadRun(void* arg) { pthread_detach(pthread_self()); // 设置线程分离,防止资源泄漏 Threaddata* data = (Threaddata*)arg; // 获取任务数据 data->_exec(data->_sockfd, data->_addr); // 执行服务函数 delete data; // 释放任务数据 return nullptr; } // 主循环,接受客户端连接并分配任务 void Loop() { _is_running = true; while (_is_running) { struct sockaddr_in peer; // 客户端地址 socklen_t len = sizeof(peer); int sockfd = accept(_listensock, (struct sockaddr*)&peer, &len); // 接受连接 if (sockfd == -1) { perror("accept"); break; } InetAddr addr(peer); // 将客户端地址封装为 InetAddr 对象 // 多线程版本 Threaddata* data = new Threaddata(sockfd, addr, std::bind(&TcpServer::Service, this, placeholders::_1, placeholders::_2)); pthread_t tid; pthread_create(&tid, nullptr, ThreadRun, data); // 创建线程处理任务 } _is_running = false; } // 析构函数,清理资源 ~TcpServer() { if (_listensock != -1) { close(_listensock); // 关闭监听套接字 } } }; 执行命令的函数在外面传入类中,当新线程接收数据后回调这个函数处理任务(创建子进程处理执行命令并返回数据)。 执行命令模块 #pragma once #include<string> #include<set> #include<fstream> #include<iostream> #include"Log.hpp" using namespace std; const static string seq = " "; const static string commandpath = "./command.txt"; class ExecuteCommand { private: set<string> _CommandSet;//安全命令集合 string _cond_path;//命令的路径 private: void LoadCommandSet()//加载安全命令集合 { ifstream infile(commandpath, ios::in);//打开文件以读取的方式 if(!infile.is_open())//判断文件是否打开成功 { LOG(FATAL, "open command.txt failed"); //打开文件失败 return; } string line; while(getline(infile, line))//读取文件中的安全命令 { _CommandSet.insert(line);//插入到安全命令集合中 } infile.close();//关闭文件 } bool IsSecure(const string& Command)//判断是否是安全命令 { if(Command.empty())//判断命令是否为空 { return false; } //先把核心命令提取出来(不要后面的选项) int pos = Command.find(seq);//找到空格的位置 string core = Command.substr(0, pos);//提取核心命令 //判断是否在安全命令集合中 if(_CommandSet.find(core) != _CommandSet.end())//在安全命令集合中 { return true; } return false;//不在安全命令集合中 } public: ExecuteCommand(const string path = commandpath):_cond_path(path) { LoadCommandSet();//加载安全命令集合 } string Execute(const string& Command) { //1.先fork && pipe //2.再exec 执行命令 //3. 执行命令前 dup2 //这些都可以通过库函数popen来实现 //(这个函数是一个标准库函数,用于创建一个管道,然后调用fork产生一个子进程,然后调用exec执行一个命令) if(!IsSecure(Command))//判断是否是安全命令 { LOG(WARNING, "command is not secure"); return "command is not secure"; } else//是安全的命令 { FILE* fp = popen(Command.c_str(), "r");//执行命令 if(fp == nullptr)//判断是否执行成功 { LOG(WARNING, "command execute failed"); return "command execute failed"; } string result; char output[1024] = {0};//定义一个缓冲区 while(fgets(output, sizeof(output)-1, fp) != nullptr)//读取命令的输出 { result += output; } pclose(fp);//关闭文件 return result; } } ~ExecuteCommand(){} }; 需求:1. 执行安全的命令 2.将命令执行的结果发送给客户端。实现: 执行命令:不能在子线程中执行命令,因为子线程还要接收来自某个客户端的数据,所有应该子线程创建一个子进程调用exec系列的函数执行命令。返回数据:进程之间具有独立性,我们选择使用匿名管道进行进程间通信。安全的命令可以通过创建一个配置文件,然后创建命令对象时,将配置文件加载进集合(文件IO慢),如果命令前缀在集合中就执行,反之直接返回提示信息。

上述两个步骤都不需要我们自己去实现,C语言库中提供了一个这样的函数,可以帮我们完成上述功能,我们来介绍这个函数:

popen系列函数 FILE *popen(const char *command, const char *type)

函数功能:popen 是一个 C 库函数,提供了一种简单的方式来创建一个管道(pipe),并启动一个子进程以执行外部命令。popen 允许父进程与子进程之间进行单向通信。

参数:

const char *command:你需要执行的命令及其选项,例如:

ls -l

const char *type:

"r": 打开管道用于读取子进程的标准输出。

"w": 打开管道用于写入子进程的标准输入。

返回值:

成功:返回一个指向管道文件的 FILE 指针。

失败:返回 NULL。

头文件:<stdio.h>

int pclose(FILE *stream);

功能:

关闭管道文件。回收子进程,防止它变成僵尸进程。

参数:

FILE *stream:由 popen 返回的指向管道的 FILE 指针。

返回值:

成功: 返回子进程的退出状态(以 waitpid 的方式返回,可以通过 WEXITSTATUS 宏提取退出码)。

失败: 返回 -1,并设置 errno。

头文件:<stdio.h>

客户端模块

和echo功能中的客户端模块基本一致。

#include <iostream> #include <string> #include <unistd.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> using namespace std; void Usage(char* s) { cout << "Usage:\n\t" << s << " serverip" << "serverport" << endl; exit(1); } int main(int argc,char* argv[]) { if(argc != 3) { Usage(argv[0]); return 1; } string ip = argv[1]; uint16_t port = stoi(argv[2]); int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字 if(sockfd == -1) { perror("socket creat error"); return 1; } //客户端需要bind,但是不需要我们显示的bind //客户端也不需要listen,监听请求是服务器程序的工作 struct sockaddr_in addr;//服务器的地址信息 addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET,ip.c_str(),&addr.sin_addr.s_addr); if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr)) == -1) { perror("connect error"); return 1; } while(true) { string message; cout << "please input message:"; getline(cin,message);//获取用户输入的信息,一行一行的获取 send(sockfd,message.c_str(),message.size()+1,0); char buffer[1024]; memset(buffer,0,sizeof(buffer)); int n = recv(sockfd,buffer,sizeof(buffer),0); if(n == -1) { perror("recv error"); break; } else if(n == 0) { cout << "server close" << endl; break; } else { buffer[n] = 0; cout << buffer; } } close(sockfd); return 0; } 服务器主程序

服务器主程序将命令处理函数bind并传给服务器类的成员,以供子线程调用该方法。

#include"TcpServer.hpp" // 引入自定义的头文件,用于创建并管理服务器 #include<memory> // 引入内存管理的头文件,用于创建智能指针 #include"ExecuteCommand.hpp" // 引入自定义的头文件,用于执行命令 #include<functional> // 引入函数式编程的头文件,用于创建函数对象 void Usage(char* s) { cout << "Usage:\n\t" << s << " serverport" << endl; // 打印程序的命令行使用格式,提示用户输入端口号 exit(USAGEERROR); } int main(int argc,char* argv[]) { // 判断命令行参数是否正确 if(argc != 2) // 程序需要接收一个参数:端口号 { Usage(argv[0]); // 如果参数不为2,则调用Usage函数打印使用说明 return 1; // 退出程序,返回错误码1 } // 将命令行参数转为端口号(uint16_t类型),这是服务器监听的端口号 uint16_t port = stoi(argv[1]); // 使用stoi将字符串转化为整数类型的端口号 // 创建TcpServer对象,并初始化 unique_ptr<TcpServer> server = make_unique<TcpServer>(port,std::bind(&ExecuteCommand::Execute, ExecuteCommand() , placeholders::_1 )); // 使用从命令行获得的端口号创建TcpServer实例 server->InitServer(); // 初始化服务器,进行绑定等操作 server->Loop(); // 启动服务器,开始接收和处理客户端请求 return 0; } windows作为客户端与服务器通信

我们修改一下客户端的echo代码即可,windows中的网络库与Linux上的有一些差异,在Udp通信的时候已经介绍过了:

#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <string.h> #include<string> #pragma comment(lib, "ws2_32.lib") // 自动链接 Winsock 库 using namespace std; const string ip = "47.98.179.70"; const uint16_t port = 8080; int main() { // 初始化 Winsock 库 WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 以指定版本初始化 { cerr << "WSAStartup failed" << endl; return 1; } // 创建套接字,使用IPv4地址族和TCP协议(SOCK_STREAM表示流式套接字) int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字 if (sockfd == INVALID_SOCKET) // 如果创建套接字失败,打印错误并返回 { cerr << "Socket creation failed: " << WSAGetLastError() << endl; WSACleanup(); return 1; } // 设置服务器的地址信息 struct sockaddr_in addr; // sockaddr_in结构体用于存储服务器的网络地址 addr.sin_family = AF_INET; // 使用IPv4地址族 addr.sin_port = htons(port); // 设置服务器的端口号(htons将端口号转换为网络字节序) inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr); // 将IP地址字符串转换为网络字节序的二进制格式 // 连接到服务器 if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) // 调用connect连接服务器 { cerr << "Connect failed: " << WSAGetLastError() << endl; // 如果连接失败,输出错误信息 closesocket(sockfd); WSACleanup(); return 1; } // 客户端和服务器之间进行通信 while (true) { string message; cout << "Please input message: "; // 提示用户输入消息 getline(cin, message); // 从标准输入获取一行字符串作为消息 // 将输入的消息发送到服务器 int bytesSent = send(sockfd, message.c_str(), message.size() + 1, 0); // 发送消息到服务器,+1用于包括消息结尾的'\0' if (bytesSent == SOCKET_ERROR) // 如果发送失败 { cerr << "Send failed: " << WSAGetLastError() << endl; break; } // 接收服务器返回的消息 char buffer[1024]; // 定义接收缓冲区,大小为1024字节 memset(buffer, 0, sizeof(buffer)); // 将缓冲区初始化为0 int n = recv(sockfd, buffer, sizeof(buffer), 0); // 从服务器接收数据 if (n == SOCKET_ERROR) // 如果接收数据失败 { cerr << "Recv failed: " << WSAGetLastError() << endl; // 输出错误信息 break; // 跳出循环,关闭连接 } else if (n == 0) // 如果服务器关闭了连接 { cout << "Server closed the connection." << endl; // 打印提示信息 break; // 跳出循环,结束通信 } else // 数据接收成功 { buffer[n] = 0; // 确保接收到的数据是一个合法的C字符串(添加终止符'\0') cout << "Server: " << buffer << endl; // 输出服务器返回的消息 } } // 关闭套接字,结束与服务器的通信 closesocket(sockfd); // Windows 关闭套接字时使用 closesocket() WSACleanup(); // 清理 Winsock 库 return 0; // 程序正常结束 }

运行结果:

#pragma comment介绍

这是一条预处理指令,是Microsoft Visual C++ 编译器MSVC的扩展指令,它的功能和gcc/g++中的-l选项类似,可以告诉编译器要链接的库的名称。

MSVC(Microsoft Visual C++)是由微软开发的一款集成开发环境(IDE)和编译器工具套件,用于开发基于 C、C++ 和 C++/CLI 的应用程序。

客户端使用状态机断线重连

状态机是一种用于描述系统行为数学模型,它的核心是状态,它通过定义不同的状态让系统执行不同的操作,且执行这些操作时得到的结果会更新状态。

代码实现 #include<iostream> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include<memory> #include<string> #include<string.h> // 枚举类型定义退出码 enum Exitcode { USAGEERR = 1, // 命令输入错误 SOCKETERR, // 创建 socket 失败 INET_PTONERR // 地址转换失败 }; // 枚举类型定义连接状态 enum Status { NEW, // 新建连接的状态 CONNECTED, // 已连接状态(连接或重连成功) DISCONNECTED, // 连接失败的状态 CONNECTEDING, // 正在连接的状态 CLOSED // 经历重连,但是失败了 }; // 常量定义 const int defaultsocketfd = -1; // 默认 socket 文件描述符 const int maxreconnectcount = 5; // 最大重连次数 const int defaultinterval = 2; // 每次重连的时间间隔(秒) // Clientconnect 类:封装与服务器的连接管理和通信 class Clientconnect { public: // 构造函数,初始化连接信息,包括 IP,端口,最大重连次数和重连间隔 Clientconnect(int16_t port, std::string ip, int maxcount = maxreconnectcount, int interval = defaultinterval): _port(port), _ip(ip), _maxcount(maxcount), _interval(interval), _socketfd(defaultsocketfd), _status(Status::NEW) {} // 连接服务器 void Connect() { // 1. 创建 socket _socketfd = socket(AF_INET, SOCK_STREAM, 0); if (_socketfd < 0) { std::cerr << "create socket failed" << std::endl; exit(Exitcode::SOCKETERR); } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(_port); // 设置端口 if (inet_pton(AF_INET, _ip.c_str(), &addr.sin_addr.s_addr) <= 0) // 地址转换 { std::cerr << "inet_pton error" << std::endl; exit(Exitcode::INET_PTONERR); } // 尝试连接 int n = connect(_socketfd, (struct sockaddr*)&addr, sizeof(addr)); if (n < 0) { Close(); // 连接失败,关闭文件描述符 _status = Status::DISCONNECTED; // 设置为连接失败状态 return; } _status = Status::CONNECTED; // 连接成功 } // 尝试重连 void Reconnect() { _status = Status::CONNECTEDING; // 设置为正在连接状态 int cnt = 0; while (true) { cnt++; std::cout << "正在重连中... 重连次数: " << cnt << std::endl; Connect(); // 尝试重新连接 // 如果连接成功,跳出循环 if (_status == Status::CONNECTED) { std::cout << "重连成功!" << std::endl; break; } // 如果超过最大重连次数,退出 if (cnt >= _maxcount) { _status = Status::CLOSED; // 设置为关闭状态 std::cout << "已达最大重连次数..." << std::endl; break; } sleep(_interval); // 重连间隔 } } // 基本的 I/O 处理 void Process() { while (true) { std::cout << "client#"; std::string message = "hello server!!"; int n = send(_socketfd, message.c_str(), message.size(), 0); // 发送数据 if (n < 0) { std::cerr << "send error" << std::endl; _status = Status::CLOSED; // 发送失败,关闭连接 } if (n > 0) { char buffer[1024] = {0}; n = recv(_socketfd, buffer, sizeof(buffer), 0); // 接收数据 if (n <= 0) { std::cerr << "recv error" << std::endl; Close(); _status = Status::DISCONNECTED; // 接收失败,设置为断开连接 break; } buffer[n] = 0; std::cout << "server#" << buffer << std::endl; } sleep(1); } } // 关闭连接 void Close() { if (_socketfd > 0) { close(_socketfd); // 关闭 socket _status = Status::CLOSED; // 设置为关闭状态 _socketfd = -1; } } // 获取当前连接的状态 Status status() { return _status; } private: std::string _ip; uint16_t _port; int _socketfd; int _maxcount; // 最大重连次数 int _interval; // 重连间隔 Status _status; // 当前连接状态 }; // 客户端类:负责管理客户端连接的生命周期 class Client { public: // 构造函数 Client(int16_t port, std::string ip): _connect(port, ip) {} // 执行客户端操作 void Excute() { while (true) { switch (_connect.status()) // 根据状态执行不同的操作 { case Status::NEW: // 初始状态,尝试连接 _connect.Connect(); break; case Status::CONNECTED: // 已连接状态,处理数据 _connect.Process(); break; case Status::DISCONNECTED: // 连接失败,尝试重连 _connect.Reconnect(); break; case Status::CLOSED: // 已关闭,退出 _connect.Close(); return; default: break; } } } ~Client() {} private: Clientconnect _connect; // 客户端连接对象 }; // 输出程序用法 void Usage(const std::string& process) { std::cout << process << " serverip serverport" << std::endl; exit(Exitcode::USAGEERR); } // 程序入口 int main(int argc, char* argv[]) { if (argc != 3) { Usage(argv[0]); } int16_t port = std::stoi(argv[2]); std::string ip = argv[1]; std::unique_ptr<Client> client = std::make_unique<Client>(port, ip); client->Excute(); return 0; }

状态说明:

NEW:初始状态,处于这个状态的客户端,还未尝试连接服务器。CONNECTED:连接服务器成功,处于这个状态的客户端,即将开始IO通信。CONNECTING:正在连接服务器的状态,这个状态一般是瞬时状态,发生在开始连接了,但是还未连接成功。DISCONNECTED:连接失败的状态,处于这个状态的服务器将会开始重连逻辑。CLOSED:客户端套接字关闭,一般发生在重连失败后。

状态机执行过程:

运行结果

我们编译客户端程序,并在本地云服务器创建一个具有echo功能的服务器程序,测试如下情况:

服务器未运行,客户端运行起来了。 连接肯定是失败的,进入重连逻辑。 服务器运行起来后,关闭服务器。 一开始连接是成功的,服务器和客户端可以正常的通信。但是服务器不再运行后,客户端又开始重连,未达到重连次数时,又运行起服务器就可以重连成功。否则重连失败。

标签:

【网络编程】之TCP实现客户端远程控制服务器端及断线重连由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【网络编程】之TCP实现客户端远程控制服务器端及断线重连