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

Linux-TCP套接字编程简易实践:实现EchoServer与远程命令执行及自定义协议(反)序列化

一.TCP Socket常用API

1.1socket()

NAMEsocket - create an endpoint for communicationSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);
  1. socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;
  2. 应用程序可以像读写文件一样用 read/write 在网络上收发数据;
  3. 如果 socket()调用出错则返回-1;
  4. 对于 IPv4, family 参数指定为 AF_INET;
  5. 对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
  6. protocol 参数的介绍从略,指定为 0 即可。

1.2bind() 

NAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  1. 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
  2. bind()成功返回 0,失败返回-1。
  3. bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;
  4. 前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度;

下面是tcp中相较于udp特有的常用api接口: 

1.3listen()

        listen函数用于将套接字置于被动监听模式,准备接受来自客户端的连接请求。它通常在服务器端调用。 

int listen(int sockfd, int backlog);
  1.  sockfd:已绑定(bind)的套接字描述符
  2. backlog:等待连接队列的最大长度

   listen()将主动套接字转换为被动套接字(监听套接字)内核会为这个监听套接字维护两个队列:未完成连接队列(SYN_RCVD状态)与已完成连接队列(ESTABLISHED状态)。backlog参数历史上被解释为这两个队列的总和上限。

该函数也是成功返回0,失败返回-1,错误码被设置。 

1.4accept()

accept()函数从已完成连接队列中取出一个连接,创建一个新的套接字用于与客户端通信。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  1. sockfd:监听套接字描述符
  2. addr:用于存储客户端地址信息的结构体指针
  3. addrlen:地址结构体的长度(值-结果参数)
  • accept()会阻塞,直到有新的连接到达(除非套接字设置为非阻塞)

  • 每次调用accept()都会返回一个新的套接字描述符(已连接套接字)

  • 原始监听套接字继续保持监听状态

  • 新的套接字专门用于与特定客户端通信

对于accept的返回值,成功返回0,失败返回-1,同时错误码被设置。 

1.5connect()

connect()函数用于客户端主动与服务器建立连接。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. sockfd:客户端套接字描述符

  2. addr:服务器地址信息结构体指针

  3. addrlen:地址结构体的长度

  • 对于TCP套接字,connect()会触发TCP三次握手过程

  • 连接建立后,套接字就可以用于数据传输

  • 如果套接字是非阻塞的,connect()可能返回EINPROGRESS错误

对于connect函数的返回值,也是成功返回0,失败返回-1同时错误码被设置。

二.通过对于tcp三次握手的简易图示来理解Tcp Socket常用api

         之后我们通过分配好的专用于通信的connfd,使用write/read或者recv/send来进行服务端于客户端的通信。因为在 UNIX/Linux 中,套接字也是文件描述符,所以支持文件 IO 接口(read/write)。recv/send 是套接字专用 API,提供更精细的控制(如 MSG_OOB 带外数据)。我们更推荐tcp通信时使用recv/send。

        这种设计既保证了接口的统一性(文件描述符),又实现了高效的网络通信。

TIPS:Recv与Send函数的介绍

Send:

#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);

 

Recv: 

#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);

三.TcpEchoServer与简单远程命令执行 

3.1popen函数介绍 

        我们先来介绍一个函数popen,popne是标准 C 库中的一个函数,用于创建管道并执行 shell 命令,实现进程间通信。它提供了一种简单的方式来与外部程序交互。 

#include <stdio.h>FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

        前者command 是我们要执行的命令,后者type则是有两个选项:"r" - 从命令读取输出,"w" - 向命令写入输入。我们这里使用r选项,可以将我们执行命令的结果打印到客户端屏幕上。

3.2TcpEchoServer与简单远程命令执行服务端代码

还是和之前一样,前文出现过的代码这里不再给出,有需要的读者请翻阅前文获取:

Tcpserver.cc 

#include "Comm.hpp"
#include "TcpServer.hpp"
#include "Command.hpp"
#include <memory>//echo server
std::string defaulthandle(const std::string& buffer,InetAddr& peer)
{std::string res = "echo#" + buffer;return res;
}//./tcpserver port
int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage:" << argv[0] << " port" << std::endl;exit(USAGE_ERR); }Output_To_Screen();//1.命令服务Command c;uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> s = std::make_unique<TcpServer>(port,defaulthandle);// std::unique_ptr<TcpServer> s = std::make_unique<TcpServer>(port,[&c](const std::string& buffer,InetAddr& peer){//     return c.Execute(buffer,peer);// });s->Init();s->Start();return 0;
}

TcpServer.hpp 

#pragma once
#include <iostream>
#include "ThreadPool.hpp"
#include "Comm.hpp"
#include "InterAddr.hpp"const int defaultsocket = -1;
const int defaultbacklog = 8;using namespace LogModule;using fun_c = std::function<std::string(const std::string&,InetAddr&)>;using task_t = std::function<void()>;class TcpServer : public NoCopy//服务器禁止拷贝和赋值
{
public:TcpServer(uint16_t port,fun_c func):_listensocket(-1),_port(port),_isrunning(false),_task(func){}void Init(){_listensocket = socket(AF_INET,SOCK_STREAM,0);if(_listensocket < 0){LOG(LogLevel::FATAL) << "Create Socket Error";exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) << "Create Socket Success: " << _listensocket; InetAddr Server(_port);int n1 = bind(_listensocket,Server.NetAddrPtr(),Server.NetAddrLen());if(n1 != 0){LOG(LogLevel::FATAL) << "Bind Socket Error";exit(BIND_ERR);}LOG(LogLevel::DEBUG) << "Bind Socket Success: " << _listensocket;int n2 = listen(_listensocket,defaultbacklog);if(n2 != 0){LOG(LogLevel::FATAL) << "Listen Socket Error";exit(LISTEN_ERR);}LOG(LogLevel::DEBUG) << "Listen Socket Success: " << _listensocket; }void Server(int socket,InetAddr peer)//1.为什么InetAddr不使用引用?                                   {                                    //注意start函数中的Client,因为他是个局部对象,当开多个窗口链接时。 char buffer[1024];               //旧的在栈上的Client会被自动释放,导致新来的将其覆盖           while(true)                      //2.为什么两个窗口链接时,被覆盖之后还能跑?{                                //注意cs_socket一旦accept成功,在TCP中就已经保存好了网络文件的相关所有信息所以还能链接通信。ssize_t n = read(socket,buffer,sizeof(buffer) - 1);//而打印相同端口号则是因为被覆盖(因为在执行任务时Client在执行echo和command时if(n > 0)                                          //,只是打印其内部信息并没有使用其存储的ip和端口号)。{buffer[n] = 0;std::cout << peer.StringAddr() << "#" << buffer << std::endl;std::string echo_string = _task(buffer,peer);write(socket,echo_string.c_str(),echo_string.size());}else if(n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";close(socket);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";close(socket);break;}}}static void* Rountinue(void* arg){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(arg);td->_this->Server(td->cs_socket,td->_addr);delete td;return nullptr;}struct ThreadData{ThreadData(TcpServer* This,InetAddr& addr,int csocket):_this(This),_addr(addr),cs_socket(csocket){}TcpServer* _this;InetAddr _addr;int cs_socket;};void Start(){_isrunning = true;while(_isrunning){//signal(SIGCHLD,SIG_IGN);//父进程忽略对子进程返回信号的处理sockaddr_in peer;socklen_t len = sizeof(peer);int cs_socket = accept(_listensocket,CONV(peer),&len);if(cs_socket < 0){LOG(LogLevel::ERROR) << "Accept Error";continue;}InetAddr Client(peer);//1.多进程版本// pid_t n = fork();// if(n == 0)// {//     //子进程//     //子进程创建子进程然后退出,让1号进程接管孤儿进程//     pid_t child_son = fork();//     if(child_son > 0)//     {//         exit(0);//     }//     //孙子进程,此时被1号进程接管//     Server(cs_socket,Client);//     close(cs_socket);//     exit(OK);// }// else if(n > 0)// {//     close(cs_socket);//     //父进程//     //两种不需要父进程等待子进程的方式//     //1.信号忽略 2.让1号进程接管孤儿进程//     pid_t id = waitpid(n,nullptr,0);//     (void)id;// }// else// {//     //创建子进程失败,系统资源不足//     LOG(LogLevel::ERROR) << "fork error";//     exit(FORK_ERR);// }//2.多线程版本pthread_t lwp;ThreadData* data = new ThreadData(this,Client,cs_socket);int n = pthread_create(&lwp,nullptr,Rountinue,data);(void)n;//3.线程池版本,适合处理短任务,在当前的echoserver中并不适用// ThreadPool_Module::ThreadPool<task_t>::GetThreadPool()->Enque([this,Client,cs_socket]()//     {//         this->Server(cs_socket,Client);//     }// );}_isrunning = false;}~TcpServer() {}
private:int _listensocket;uint16_t _port;bool _isrunning;fun_c _task;
};

Comm.hpp 

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include "log.hpp"class NoCopy
{public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy &) = delete;const NoCopy &operator = (const NoCopy&) = delete;
};enum Cause
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR,WRITE_ERR,READ_ERR
};#define CONV(addr) ((struct sockaddr*)&addr)

InterAddr.hpp 

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include <cstring>class InetAddr
{
public:InetAddr(struct sockaddr_in &addr) : _addr(addr){//网络转主机_port = ntohs(_addr.sin_port);           // _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPchar ipbuffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));_ip = ipbuffer;}InetAddr(std::string ip,uint16_t port):_ip(ip),_port(port){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}InetAddr(uint16_t port):_port(port),_ip(){// 主机转网络bzero(&_addr,sizeof(_addr));_addr.sin_family = AF_INET;//PF_INET_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = INADDR_ANY;}uint16_t Port() {return _port;}std::string Ip() {return _ip;}const struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr *NetAddrPtr(){return CONV(_addr);}socklen_t NetAddrLen(){return sizeof(_addr);}bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};

Command.hpp 

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Command.hpp"
#include "InterAddr.hpp"
#include "log.hpp"using namespace LogModule;class Command
{
public:// ls -a && rm -rf// ls -a; rm -rfCommand(){// 严格匹配_WhiteListCommands.insert("ls");_WhiteListCommands.insert("pwd");_WhiteListCommands.insert("ls -l");_WhiteListCommands.insert("touch haha.txt");_WhiteListCommands.insert("who");_WhiteListCommands.insert("whoami");}bool IsSafeCommand(const std::string &cmd){auto iter = _WhiteListCommands.find(cmd);return iter != _WhiteListCommands.end();}std::string Execute(const std::string &cmd, InetAddr &addr){// 1. 属于白名单命令// if(!IsSafeCommand(cmd))// {//     return std::string("坏人");// }std::string who = addr.StringAddr();// 2. 执行命令FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){return std::string("你要执行的命令不存在: ") + cmd;}std::string res;char line[1024];while(fgets(line, sizeof(line), fp)){res += line;}pclose(fp);std::string result = who + "execute done, result is: \n" + res;LOG(LogLevel::DEBUG) << result;return result;}~Command(){}
private:// 受限制的远程执行std::set<std::string> _WhiteListCommands;
};

3.3客户端代码 

        我们知道,有的时候客户端总会可能因为网络问题而断开连接,所以一般我们的客户端需要支持短线重连功能,下面给出非重连和重连两个版本的代码:

3.3.1非重连版本

#include "Comm.hpp"
#include "InterAddr.hpp"using namespace LogModule;
//./tcpclient server_ip server_port
int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage:" << argv[0] << " server_ip" << " server_port" << std::endl;exit(USAGE_ERR);}int c_socket = socket(AF_INET,SOCK_STREAM,0);if(c_socket < 0){LOG(LogLevel::FATAL) << "Create Socket Error";exit(SOCKET_ERR); }int port = std::stoi(argv[2]);InetAddr Server(argv[1],port);int n = connect(c_socket,Server.NetAddrPtr(),Server.NetAddrLen());if(n != 0){LOG(LogLevel::FATAL) << "Connect Error";exit(CONNECT_ERR);}LOG(LogLevel::DEBUG) << "Connect Success: " << c_socket;char buffer[1024];while(true){//提示用户输入消息std::string message;std::cout << "Please enter#";std::cin >> message;ssize_t n1 = write(c_socket,message.c_str(),message.size());if(n1 < 0){LOG(LogLevel::DEBUG) << Server.StringAddr() <<"服务端异常";close(c_socket);break;}n1 = read(c_socket,buffer,sizeof(buffer) - 1);buffer[n1] = 0;std::cout << buffer << std::endl;}return 0;
}

3.3.2支持断线重连版本 

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InterAddr.hpp"static const int defaultsockfd = -1;static const int default_retry_interval = 1;
static const int default_max_retries = 5;//支持短线重连的客户端代码版本
enum class Status
{NEW,        // 新建状态,就是单纯的连接CONNECTING, // 正在连接,仅仅方便查询conn状态CONNECTED,  // 连接或者重连成功DISCONNECTED, // 重连失败CLOSED        // 连接失败,经历重连,无法连接
};class ClientConnection
{
public:ClientConnection(const std::string& serverip,uint16_t port):_sockfd(defaultsockfd),_serverport(port),_serverip(serverip),_retry_interval(default_retry_interval),_max_retries(default_max_retries),_status(Status::NEW){}void Connect(){_sockfd = socket(AF_INET,SOCK_STREAM,0);if(_sockfd < 0){std::cerr << "Socket Error" << std::endl;exit(SOCKET_ERR);}InetAddr Server(_serverip,_serverport);int n = connect(_sockfd,Server.NetAddrPtr(),Server.NetAddrLen());if(n < 0){_status = Status::DISCONNECTED;Closed();return;}_status = Status::CONNECTED;std::cout << "与服务端连接成功,开始执行任务" << std::endl;}void Reconnect(){int cnt = 0;while(cnt < _max_retries){cnt++;Connect();if(_status == Status::CONNECTED){return;}std::cout << "重连失败,当前为第" << cnt << "次重连。" << std::endl;sleep(_retry_interval);}_status = Status::CLOSED;std::cout << "与服务端丢失连接,请检查本地网络连接..." << std::endl;}void Process(){//与服务端的通信程序char buffer[1024];while(true){//提示用户输入消息std::string message;std::cout << "Please enter#";std::cin >> message;ssize_t ns = send(_sockfd,message.c_str(),message.size(),0);if(ns <= 0){std::cout << "与服务端连接异常,开始进行重连..." << std::endl;_status = Status::DISCONNECTED;break;}ssize_t nr = recv(_sockfd,buffer,sizeof(buffer) - 1,0);if(nr <= 0){std::cout << "与服务端连接异常,开始进行重连..." << std::endl;_status = Status::DISCONNECTED;break;}buffer[nr] = 0;std::cout << buffer << std::endl;}}void Closed(){if(_sockfd != defaultsockfd){close(_sockfd);_status = Status::CLOSED;_sockfd = -1;}}Status StatusConnection(){return _status;}~ClientConnection() {Closed();}
private:int _sockfd;uint16_t _serverport;  // server port 端口号std::string _serverip; // server ip地址int _retry_interval;   // 重试时间间隔int _max_retries;      // 重试次数Status _status;        // 连接状态
};class TcpClient
{
public:TcpClient(const std::string& serverip,uint16_t port):_conn(serverip,port){}void Excute(){while(true){switch (_conn.StatusConnection()){case Status::NEW:_conn.Connect();break;case Status::CONNECTED:_conn.Process();break;case Status::DISCONNECTED:_conn.Reconnect();break;case Status::CLOSED:break;default:break;}if(_conn.StatusConnection() == Status::CLOSED){break;}}}~TcpClient() {}
private:ClientConnection _conn;
};//./tcpclient_con.cc serverip serverport
int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage:" << argv[0] << " serverip" << " serverport" << std::endl;return USAGE_ERR;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);std::unique_ptr<TcpClient> tcpc = std::make_unique<TcpClient>(serverip,serverport);tcpc->Excute();return 0;
}

四.应用层自定义协议与序列化 

        在之前的文章中,我们已经知道,协议就是通信双方的一种约定。但这个解释还是较为模糊的,它在我们实际的应用层上到底是什么呢?其实就是结构体。也就是说,协议在今天我们看来,就是双方约定好的结构化的数据。

4.1序列化与反序列化

        既然协议是结构化数据,那么我们可不可以在传输的时候直接传二进制结构体给对方呢。这种做法貌似行得通。但由于服务端与客户端的使用的语言可能不相同,比如客户端使用c++而服务端使用的是python,结构体的对齐规则在不同语言中又各种各样。但字符串无论什么语言其规则都是近乎一致的。所以我们在传递协议结构体时,通常是将结构体序列化为字符串,然后另一方接收到对端发送过来的字符串后对该字符串进行反序列化得到协议内容。

这里我们介绍一种开源且成熟的序列化与反序列化方案:JSONCPP

4.2JSONCPP 

         Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各种需要处理 JSON 数据的 C++ 项目中。

4.2.1JSONCPP的特性及食用方法

特性: 
  1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
  2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
  3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数字、 布尔值和 null。
  4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便开发者调试。

        当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时, 确实存在不同的做法和工具类可供选择。 以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:

安装: 
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
序列化与反序列化: 

        我们这里只简单介绍一种序列化与反序列化方案,细微部分本文不深究,感兴趣的读者可自行搜索资料进行了解: 

比如我们此时有这样一个结构体,该结构体成员如下:

private:int _x;int _y;char _oper;
};

一种序列化与反序列化方案如下:

    bool Deserialization(std::string& in){if(in.empty()) return false;Json::Value root;Json::Reader redr;bool ok = redr.parse(in,root);if(!ok){return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}std::string Serialization(){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string s = writer.write(root);return s;}
JSON::Value: 

Json::Value 是 Jsoncpp 库中的一个重要类, 用于表示和操作 JSON 数据结构。 以下是一些常用的 Json::Value 操作列表:

1.构造函数 
  • Json::Value(): 默认构造函数, 创建一个空的 Json::Value 对象。
  • Json::Value(ValueType type, bool allocated = false): 根据给定的ValueType(如 nullValue, intValue, stringValue 等) 创建一个 Json::Value 对象
2.访问元素 
  • Json::Value& operator[](const char* key): 通过键(字符串) 访问对象中的元素。 如果键不存在, 则创建一个新的元素。
  • Json::Value& operator[](const std::string& key): 同上, 但使用std::string 类型的键。
  • Json::Value& operator[](ArrayIndex index): 通过索引访问数组中的元素。 如果索引超出范围, 则创建一个新的元素。
  • Json::Value& at(const char* key): 通过键访问对象中的元素, 如果键不存在则抛出异常。
  • Json::Value& at(const std::string& key): 同上, 但使用 std::string类型的键。
3.类型检查 
  • bool isNull(): 检查值是否为 null。
  • bool isBool(): 检查值是否为布尔类型。
  • bool isInt(): 检查值是否为整数类型。
  • bool isInt64(): 检查值是否为 64 位整数类型。
  • bool isUInt(): 检查值是否为无符号整数类型。
  • bool isUInt64(): 检查值是否为 64 位无符号整数类型。
  • bool isIntegral(): 检查值是否为整数或可转换为整数的浮点数。
  • bool isDouble(): 检查值是否为双精度浮点数。
  • bool isNumeric(): 检查值是否为数字(整数或浮点数) 。
  • bool isString(): 检查值是否为字符串。
  • bool isArray(): 检查值是否为数组。
  • bool isObject(): 检查值是否为对象(即键值对的集合) 。
4.赋值与类型转换 
  • Json::Value& operator=(bool value): 将布尔值赋给 Json::Value 对象。
  • Json::Value& operator=(int value): 将整数赋给 Json::Value 对象。
  • Json::Value& operator=(unsigned int value): 将无符号整数赋给Json::Value 对象。
  • Json::Value& operator=(Int64 value): 将 64 位整数赋给 Json::Value对象。
  • Json::Value& operator=(UInt64 value): 将 64 位无符号整数赋给Json::Value 对象。
  • Json::Value& operator=(double value): 将双精度浮点数赋给Json::Value 对象。
  • Json::Value& operator=(const char* value): 将 C 字符串赋给Json::Value 对象。
  • Json::Value& operator=(const std::string& value): 将 std::string赋给 Json::Value 对象。
  • bool asBool(): 将值转换为布尔类型(如果可能) 。
  • int asInt(): 将值转换为整数类型(如果可能) 。
  • Int64 asInt64(): 将值转换为 64 位整数类型(如果可能) 。
  • unsigned int asUInt(): 将值转换为无符号整数类型(如果可能) 。
  • UInt64 asUInt64(): 将值转换为 64 位无符号整数类型(如果可能) 。
  • double asDouble(): 将值转换为双精度浮点数类型(如果可能) 。
  • std::string asString(): 将值转换为字符串类型(如果可能) 。
5.数组和对象操作 
  • size_t size(): 返回数组或对象中的元素数量。
  • bool empty(): 检查数组或对象是否为空。
  • void resize(ArrayIndex newSize): 调整数组的大小。
  • void clear(): 删除数组或对象中的所有元素。
  • void append(const Json::Value& value): 在数组末尾添加一个新元素。
  • Json::Value& operator[](const char* key, const Json::Value&defaultValue =Json::nullValue): 在对象中插入或访问一个元素, 如果键不存在则使用默认值。
  • Json::Value& operator[](const std::string& key, constJson::Value& defaultValue = Json::nullValue): 同上, 但使用 std::string类型的。

4.3进程间关系与守护进程 

        我们之前写的通信服务,基本上服务端都是启动我们自己写的程序后,会挂在前台,此时我们无法在当前窗口执行任何其他的操作。而且一旦我们关闭ssh连接,相应的服务端也会直接关闭。如果想要ssh关闭后我们的服务仍然可以在服务端运行,此时我们就需要对服务进程守护进程化。什么是守护进程化,如何使服务进程守护进程化?让我们先从进程间关系谈起。

4.3.1进程组

什么是进程组? 

         之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID)之外 还属于一个进程组。 进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中。

$ ps -eo pid,pgid,ppid,comm | grep test#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,) 作为定界符, 可以指定要输出的列
组长进程 

        每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。 我们可以通过 ps 命令看到组长进程的现象:

[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat# 输出结果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat

        从结果上看 ps 进程的 PID 和 PGID 相同, 那也就是说明 ps 进程是该进程组的组长进程, 该进程组包括 ps 和 cat 两个进程。

  • 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
  • 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。 注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。

4.3.2会话 

什么是会话?

        刚刚我们谈到了进程组的概念, 那么会话又是什么呢? 会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。 每一个会话也有一个会话 ID(SID)。


 

通常我们都是使用管道将几个进程编成一个进程组。 如上图的进程组 2 和进程组 3 可能是由下列命令形成的:

[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示将进程组放在后台执行

我们举一个例子观察一下这个现象:

# 用管道和 sleep 组成一个进程组放在后台运行
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
# 查看 ps 命令打出来的列描述信息
[node@localhost code]$ ps axj | head -n1
# 过滤 sleep 相关的进程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a 选项表示不仅列当前⽤户的进程, 也列出所有其他⽤户的进程
# x 选项表示不仅列有控制终端的进程, 也列出所有⽆控制终端的进程
# j 选项表示列出与作业控制相关的信息, 作业控制后续会讲
# grep 的-v 选项表示反向过滤, 即不过滤带有 grep 字段相关的进程# 结果如下
PPID PID PGID SID TTY TPGID STAT UID TIME
COMMAND
2806 4223 4223 2780 pts/2 4229 S 1000 0:00 sleep
100
2806 4224 4223 2780 pts/2 4229 S 1000 0:00 sleep
200
2806 4225 4223 2780 pts/2 4229 S 1000 0:00 sleep
300

从上述结果来看 3 个进程对应的 PGID 相同, 即属于同一个进程组。

如何创建会话

可以调用 setseid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。

#
include <unistd.h>
/*
*功能: 创建会话
*返回值: 创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);

该接口调用之后会发生:

  • 调用进程会变成新会话的会话首进程。 此时, 新会话中只有唯一的一个进程 ○
  • 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID○ 
  • 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端, 则调用之后会切断联系。

需要注意的是: 这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是先调用 fork 创建子进程, 父进程终止, 子进程继续执行, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误的情况。

会话ID(SID)

        上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。 注意: 会话 ID 在有些地方也被称为会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。

4.3.3守护进程化

        有了上面的认识,我们可以得出一种守护进程化的思想。也就是对于当前服务进程。我们在一开始运行时,先创建子进程,然后让当前进程退出,再使用session使当前子进程成为一个新的会话。因为我们每次ssh连接的窗口就是一个会话,让当前子进程变为一个单独的会话即可使其在ssh连接后保持不退出。

#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "Comm.hpp"using namespace LogModule;const std::string dev = "/dev/null"; // 使用/dev/null设备文件作为输入输出黑洞void Daemon(int nochdir, int noclose)
{// 1. 忽略可能影响守护进程的信号// SIGPIPE: 防止写入已关闭的管道导致进程意外终止// SIGCHLD: 防止子进程退出产生僵尸进程(设置为SIG_IGN表示由init进程回收)signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN); // 也可以使用SIG_DFL让init进程回收// 2. 第一次fork: 创建子进程并终止父进程// 目的: 1) 确保子进程不是进程组组长 2) 使子进程成为孤儿进程被init接管if (fork() > 0)exit(0); // 父进程直接退出// 3. 子进程继续执行,创建新会话并成为会话首进程// setsid()作用:// - 脱离原控制终端// - 成为新会话的首进程// - 成为新进程组的组长// - 没有控制终端(这是守护进程的关键特性)setsid(); // 4. 可选: 更改工作目录// 目的: 1) 避免占用可卸载的文件系统 2) 确保使用绝对路径时不会意外访问错误位置// 通常设置为根目录(/),这里设置为当前目录(.)if(nochdir == 0)chdir(".");// 5. 处理标准IO文件描述符// 守护进程不应从终端接收输入或向终端输出// 有两种处理方式:// 方法1: 直接关闭0,1,2文件描述符(不推荐,可能导致新打开的文件意外使用这些描述符)// 方法2: 将它们重定向到/dev/null(推荐做法)if (noclose == 0){// 打开/dev/null设备文件int fd = ::open(dev.c_str(), O_RDWR);if (fd < 0){LOG(LogLevel::FATAL) << "open " << dev << " errno";exit(OPEN_ERR);}else{// 将标准输入、输出、错误重定向到/dev/nulldup2(fd, STDIN_FILENO);   // 0dup2(fd, STDOUT_FILENO);  // 1dup2(fd, STDERR_FILENO);   // 2close(fd);  // 关闭原始文件描述符}}
}

        这里额外提一嘴dev/null与更改目录,dev/null是 Linux/Unix 系统中的一个特殊设备文件,通常被称为 "黑洞" 或 "空设备"。它的主要作用是丢弃所有写入它的数据,并且读取它时会立即返回 EOF(文件结束符)。 为了避免守护进程打印消息到前台窗口,需要将所有输出到标准错误输入输出流的信息全部丢弃。

为什么要更改目录?有一下三点原因:

1. 防止占用挂载的文件系统

        守护进程若在某个挂载的文件系统(如USB或NFS共享目录)中运行,会导致该文件系统无法卸载(umount命令报错Device or resource busy)。通过切换工作目录到根目录(chdir("/")),可解除对原目录的占用,确保文件系统能正常卸载。

2. 避免相对路径引发的权限或安全问题

        守护进程可能以高权限(如root)运行,若工作目录留在用户目录(如/home/user),使用相对路径访问文件时可能导致权限冲突或意外覆盖用户文件。更改为固定目录(如//var/log)后,强制使用绝对路径访问资源,提升安全性和可预测性。

3. 符合守护进程的"无关联"特性

        守护进程的核心设计是脱离用户环境(终端、文件系统、会话等)。chdir("/")setsid()(脱离终端)、重定向标准IO(脱离终端输入输出)共同作用,确保进程完全独立于启动环境,避免残留依赖。若需访问特定目录(如日志目录),应在切换工作目录后使用绝对路径操作文件(如/var/log/daemon.log)。

4.4自定义协议实现网络版本简单计算器

        我们这里实现的是一个支持加减乘除的简单计算器。首先我们使用守护进程化代码将当前服务进程守护进程化, 然后先写对请求结构体的处理类,该类将结果存储到应答结构体中返回。让其作为我们当前服务器的业务函数。

        接下来实现协议的主体,该协议类会接收上面的业务处理函数,并具备从tcp接收缓冲区提取完整报文的能力,由于tcp的报文数据通常是黏在一起的。我们需要使用一些方法来区分不同的完整报文同时对当前读取的报文完整性做检验。所以该协议模块需要具备对客户端和服务端双方的请求与应答进行编码解码的能力。这里我们采用简单一点的方法:分隔符+报文长度。

        此外,我们再对tcp socket编程中常用的api接口进行封装,封装内容包括:套接字创建,套接字绑定,监听,关闭,accept,接收发送消息,connect,以及关闭套接字文件。        

        最后就是将协议整体作为我们tcp服务端的功能函数,其实就是对上面echoserver部分代码功能部分替换下而已。下面是具体实现代码:

NetCal.hpp(业务处理函数类):
#pragma once#include "Protocol.hpp"
#include <iostream>class Cal
{
public:Response Execute(Request &req){Response resp(0, 0); // code: 0表示成功switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetEffectiveness(1); // 1除零错误}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetEffectiveness(2); // 2 mod 0 错误}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetEffectiveness(3); // 非法操作break;}return resp;}
};
Protocol.hpp(自定义协议类) :
#pragma once
#include "Socket.hpp"
#include <jsoncpp/json/json.h>
#include <memory>
#include <functional>using namespace socket_module;//client -> server
class Request
{
public:Request() :_x(1),_y(1),_oper('+') {}Request(int x,int y,char oper):_x(x),_y(y),_oper(oper) {}bool Deserialization(std::string& in){if(in.empty()) return false;Json::Value root;Json::Reader redr;bool ok = redr.parse(in,root);if(!ok){return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}std::string Serialization(){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string s = writer.write(root);return s;}int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }~Request() {}
private:int _x;int _y;char _oper;
};//server -> client
class Response
{
public:Response():_result(-1),_effectiveness(0) {}Response(int result,int effectiveness):_result(result),_effectiveness(effectiveness){}bool Deserialization(std::string& in){if(in.empty()) return false;Json::Value root;Json::Reader redr;bool ok = redr.parse(in,root);if(!ok){return false;}_result = root["result"].asInt();_effectiveness = root["effectiveness"].asInt();return true;}std::string Serialization(){Json::Value root;root["result"] = _result;root["effectiveness"] = _effectiveness;Json::FastWriter writer;return writer.write(root);}void SetResult(int result){_result = result;}int Result() { return _result; }int Effectiveness() { return _effectiveness; }void SetEffectiveness(int effectiveness){_effectiveness = effectiveness;}~Response() {}
private:int _result;//默认为-1int _effectiveness;//确认结果的有效性,默认为0,表示有效
};const std::string sep = "\r\n";  // 报文分隔符
using Bs_pg = std::function<Response(Request&)>;  // 业务处理函数类型// 协议处理类,负责报文编解码和通信流程控制
class Protocol {
public:Protocol() {} //对客户端的特殊构造Protocol(Bs_pg server_pro) :_server_pro(server_pro) {}  // 构造函数,传入业务处理函数// 编码:为消息添加长度报头和分隔符void Encode(std::string& send_message) {std::string len = std::to_string((int)send_message.size());  // 计算消息长度send_message = len + sep + send_message + sep;  // 格式: [长度]\r\n[消息]\r\n}// 解码:从接收缓冲区提取完整报文bool Decode(std::string& recv_message, std::string* package) {if(recv_message.empty()) return false;// 查找第一个分隔符位置int pos = recv_message.find(sep);if(pos == std::string::npos) {return false;  // 没有找到分隔符,报文不完整}// 提取消息长度std::string mess_len = recv_message.substr(0, pos);int mes_len = std::stoi(mess_len);// 计算完整报文大小: 长度字段 + 分隔符 + 消息体 + 分隔符int full_message_size = pos + sep.size() + mes_len + sep.size();// 检查缓冲区是否有足够数据if(recv_message.size() < full_message_size) {return false;  // 数据不足,等待更多数据}// 提取消息体*package = recv_message.substr(pos + sep.size(), mes_len);// 从缓冲区移除已处理的数据recv_message.erase(0, full_message_size);return true;}// 处理客户端请求的主循环void GetRequest(std::shared_ptr<Socket> &sock, InetAddr& client) {std::string buffer_queue;  // 接收缓冲区while(true) {int n = sock->Recv(&buffer_queue);  // 接收数据if(n > 0) {  // 成功接收数据std::string json_package;bool ok = Decode(buffer_queue, &json_package);  // 尝试解码if(!ok) continue;  // 报文不完整,继续接收Request req;ok = req.Deserialization(json_package);  // 反序列化请求if(!ok) continue;  // 反序列化失败,丢弃// 调用上层业务处理函数Response resp = _server_pro(req);// 序列化响应并发送std::string send_message = resp.Serialization();Encode(send_message);  // 添加协议头sock->Send(send_message);  // 发送响应}else if(n == 0) {  // 客户端关闭连接LOG(LogLevel::DEBUG) << "客户端:" << client.StringAddr() << ",退出了...";break;}else if(n < 0) {  // 接收错误LOG(LogLevel::ERROR) << "客户端:" << client.StringAddr() << ",异常了...";break;}}sock->Close();  // 关闭连接}//客户端处理服务端发送的消息std::string GetResponse(std::shared_ptr<Socket>& sock,std::string& buffer_queue){std::string res;while(true){int n = sock->Recv(&buffer_queue);if(n > 0){std::string json_package;bool ok = Decode(buffer_queue, &json_package);  // 尝试解码if(!ok) continue;  // 报文不完整,继续接收Response resp;ok = resp.Deserialization(json_package);  // 反序列化请求if(!ok) continue;  // 反序列化失败,丢弃res = std::to_string(resp.Result()) + "[" + std::to_string(resp.Effectiveness()) +"]";break;}else if(n == 0){std::cout << "服务端退出了..." << std::endl;break;}else{std::cout << "服务端异常了..." << std::endl;break;}}return res;}~Protocol() {}private:Bs_pg _server_pro;  // 业务处理函数
};
TCP套接字功能封装类:

 (

#pragma once
#include <iostream>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <memory>
#include "log.hpp"
#include "InterAddr.hpp"
#include "Comm.hpp"namespace socket_module
{using namespace LogModule;const int defaultbacklog = 15;  // 默认监听队列长度const static int defaultsocket = -1;   // 默认无效socket描述符// Socket抽象基类,定义接口规范class Socket{public:virtual ~Socket() {}virtual void SocketOrDie() = 0;                      // 创建socket(失败则退出)virtual void BindOrDie(uint16_t port) = 0;           // 绑定端口(失败则退出)virtual void ListenOrDie(int backlog) = 0;           // 开始监听(失败则退出)virtual std::shared_ptr<Socket> Accept(InetAddr* Client) = 0; // 接受连接virtual void Close() = 0;                            // 关闭socketvirtual int Recv(std::string* out) = 0;              // 接收数据virtual int Send(const std::string& message) = 0;    // 发送数据virtual int Connect(std::string& ip,int port) = 0;  //请求连接// 构建TCP socket的完整流程void BuildTcpSocket(uint16_t port, int backlog = defaultbacklog){SocketOrDie();     // 1. 创建socketBindOrDie(port);   // 2. 绑定端口ListenOrDie(backlog); // 3. 开始监听}void BuildTcpClientSocket(){SocketOrDie();}};// TCP Socket具体实现类class TcpSocket : public Socket{public:TcpSocket() :_socket(defaultsocket) {}  // 默认构造TcpSocket(int socket) :_socket(socket) {} // 通过已有socket描述符构造// 创建TCP socketvoid SocketOrDie() override{_socket = ::socket(AF_INET, SOCK_STREAM, 0);  // IPv4, TCPif(_socket < 0){LOG(LogLevel::FATAL) << "create socket error";exit(SOCKET_ERR);  // 创建失败则退出程序}LOG(LogLevel::DEBUG) << "create socket success:" << _socket;}// 绑定到指定端口void BindOrDie(uint16_t port) override{InetAddr peer(port);  // 创建地址结构int n = ::bind(_socket, peer.NetAddrPtr(), peer.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);  // 绑定失败则退出程序}LOG(LogLevel::DEBUG) << "bind success:" << _socket; }// 开始监听连接void ListenOrDie(int backlog) override{int n = listen(_socket, backlog);if(n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);  // 监听失败则退出程序}LOG(LogLevel::DEBUG) << "listen success" << _socket;}// 接受客户端连接std::shared_ptr<Socket> Accept(InetAddr* Client) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = ::accept(_socket, CONV(peer), &len);if(fd < 0){LOG(LogLevel::ERROR) << "accept error";return nullptr;  // 接受失败返回空指针}Client->SetAddr(peer);  // 通过输出参数返回客户端地址LOG(LogLevel::DEBUG) << "客户端:" << Client->StringAddr() << "上线了...";return std::make_shared<TcpSocket>(fd);  // 返回新创建的socket对象}// 接收数据int Recv(std::string* out) override{char buffer[1024];int n = ::recv(_socket, buffer, sizeof(buffer) - 1, 0);if(n >= 0){buffer[n] = 0;  // 添加字符串结束符(*out) += buffer;  // 追加到输出字符串}return n;  // 返回接收的字节数}// 发送数据int Send(const std::string& message) override{int n = ::send(_socket, message.c_str(), message.size(), 0);return n;  // 返回发送的字节数}// 关闭socketvoid Close() override{if(_socket >= 0){close(_socket);_socket = defaultsocket;  // 重置为无效值}}//客户端请求连接int Connect(std::string& ip,int port) override{InetAddr Server(ip,port);return ::connect(_socket,Server.NetAddrPtr(),Server.NetAddrLen());}~TcpSocket() {}private:int _socket;  // socket文件描述符};
}
Daemon.hpp(守护进程函数类) :
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "Comm.hpp"using namespace LogModule;const std::string dev = "/dev/null";// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{// 1. 忽略IO,子进程退出等相关的信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN); // SIG_DFL// 2. 父进程直接结束if (fork() > 0)exit(0);// 3. 只能是子进程,孤儿了,父进程就是1setsid(); // 成为一个独立的会话if(nochdir == 0) // 更改进程的工作路径???为什么??chdir(".");// 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的.//  守护进程,不从键盘输入,也不需要向显示器打印//  方法1:关闭0,1,2 -- 不推荐//  方法2:打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/nullif (noclose == 0){int fd = ::open(dev.c_str(), O_RDWR);if (fd < 0){LOG(LogLevel::FATAL) << "open " << dev << " errno";exit(OPEN_ERR);}else{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
 TcpServer.hpp与TcpServer.cc:
#include "TcpServer.hpp"
#include "NetCal.hpp"
#include "Daemon.hpp"//./tcpserver port
int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage:" << argv[0] << " port" << std::endl;exit(USAGE_ERR); }Output_To_File();std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;//daemon(0, 0);Daemon(0,0);// 1. 顶层std::unique_ptr<Cal> cal = std::make_unique<Cal>();// 2. 协议层std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{return cal->Execute(req);});// 3. 服务器层std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket> &sock, InetAddr client){protocol->GetRequest(sock, client);});tsvr->Start();return 0;
}//TcpServer.hpp
#pragma once
#include "Socket.hpp"
#include <functional>
#include <memory>using namespace socket_module;
using server_t = std::function<void(std::shared_ptr<Socket> &sock,InetAddr addr)>;class TcpServer
{
public:TcpServer(uint16_t port,server_t server):_port(port),_listensockptr(std::make_unique<TcpSocket>()),_isrunning(false),_server(server){_listensockptr->BuildTcpSocket(_port);}void Start(){_isrunning = true;while(_isrunning){InetAddr Client;auto sock = _listensockptr->Accept(&Client);if(sock == nullptr){continue;}//多进程模式实现pid_t id = fork();if(id < 0){LOG(LogLevel::FATAL) << "fork error";exit(FORK_ERR);}else if(id == 0){//子进程pid_t n = fork();if(n > 0)exit(OK);_listensockptr->Close();_server(sock,Client);sock->Close();exit(OK);}else{//父进程sock->Close();::waitpid(id,nullptr,0);}}_isrunning = false;}~TcpServer() {}
private:uint16_t _port;std::unique_ptr<Socket> _listensockptr;bool _isrunning;server_t _server;
};
TcpClient.cc(客户端代码) :
#include "Protocol.hpp"
#include <iostream>
#include "Socket.hpp"using namespace socket_module;void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}void  GetDataFromStdin(int& x,int& y,char& oper)
{while(true){std::cout << "please entet x# ";if(!(std::cin >> x)) {std::cerr << "输入错误,请输入一个整数。" << std::endl;std::cin.clear();  // 重置错误标志std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // 忽略错误输入continue;  // 跳过本次循环,重新输入}    std::cout << "please enter y# ";if(!(std::cin >> y)) {std::cerr << "输入错误,请输入一个整数。" << std::endl;std::cin.clear();  // 重置错误标志std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // 忽略错误输入continue;  // 跳过本次循环,重新输入}    std::cout << "please enter opeartor# ";std::cin >> oper;break;}
}
// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}int port = std::stoi(argv[2]);std::string ip = argv[1];std::shared_ptr<Socket> Client = std::make_shared<TcpSocket>();Client->BuildTcpClientSocket();std::unique_ptr<Protocol> Pro = std::make_unique<Protocol>();std::string resp_buffer;if(Client->Connect(ip,port) == 0){while(true){int x = 0,y = 0;char oper = '+';GetDataFromStdin(x, y, oper);//对数据进行序列化并封装长度报头Request req(x,y,oper);std::string send_message = req.Serialization();Pro->Encode(send_message);Client->Send(send_message);//获取字节流中服务端发送过来的消息std::string res = Pro->GetResponse(Client,resp_buffer);if(res != ""){std::cout << res << std::endl; std::cout << std::endl;}else break;}}else{std::cout << "connect error: " << "服务端处于离线状态,请稍后进行连接" << std::endl;}Client->Close();return 0;
}

相关文章:

  • 基于物联网的智能家居监控系统设计和实现(源码+论文+部署讲解等)
  • OpenWrt开发第8篇:树莓派开发板做无线接入点
  • 计算机网络笔记(二十一)——4.3IP层转发分组的过程
  • 小土堆pytorch--torchvision中的数据集的使用dataloader的使用
  • 在python中,为什么要引入事件循环这个概念?
  • 第二十三节:图像金字塔- 图像金字塔应用 (图像融合)
  • 封装和分用(网络原理)
  • 【常用算法:排序篇】4.高效堆排序:线性建堆法与蚂蚁问题的降维打击
  • Kafka的基本概念和Dokcer中部署Kafka
  • B 端电商数据接口开发:1688 商品详情页实时数据抓取技术解析
  • 组合模式(Composite Pattern)详解
  • Docker拉取ubuntu22.04镜像使用ROS2 humble及仿真工具可视化进行导航
  • [案例四] 智能填写属性工具(支持装配组件还有建模实体属性的批量创建、编辑)
  • NoSQL数据库技术与应用复习总结【看到最后】
  • MySQL为什么选择B+树
  • MCP:重塑AI交互的通用协议,成为智能应用的基础设施
  • JUC并发编程(上)
  • Qt—多线程基础
  • 《Redis应用实例》学习笔记,第一章:缓存文本数据
  • Python----神经网络(基于Alex Net的花卉分类项目)
  • 小米SU7 Ultra风波升级:数百名车主要求退车,车主喊话雷军“保持真诚”
  • 学者纠错遭网暴,人民锐评:“饭圈”该走出畸形的怪圈了
  • 美国政府信用卡被设1美元限额,10美元采购花一两小时填表
  • 上海建筑领域绿色发展2025年工作要点发布
  • 郑州通报“夜市摊贩收取香烟交给城管”:涉事人员停职调查
  • 习近平结束对俄罗斯国事访问并出席纪念苏联伟大卫国战争胜利80周年庆典回到北京