当前位置: 首页 > 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;
}

相关文章:

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); socket()打开一个网络通讯端口,如果…...

缓存(3):本地缓存作用 及 数据一致性 实现策略

概述 CAP 什么是CAP CAP理论&#xff0c;指的是在一个分布式系统中&#xff0c; Consistency&#xff08;一致性&#xff09;、 Availability&#xff08;可用性&#xff09;、Partition tolerance&#xff08;分区容错性&#xff09;&#xff0c;三者不可得兼。 三者关系如…...

【leetcode】《BFS扫荡术:如何用广度优搜索征服岛屿问题》

前言 &#x1f31f;&#x1f31f;本期讲解关于力扣的几篇题解的详细介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…...

vue中理解MVVM

理解 在 Vue 中&#xff0c;MVVM&#xff08;Model-View-ViewModel&#xff09; 是其核心设计思想之一&#xff0c;它帮助实现了数据驱动的视图更新和良好的代码结构分离。我们来具体解析 Vue 是如何实现 MVVM 模式的。 &#x1f310; MVVM 是什么&#xff1f; 角色含义Vue…...

[工具]B站缓存工具箱 (By 郭逍遥)

&#x1f4cc; 项目简介 B站缓存工具箱是一个多功能的B站缓存工具&#xff0c;包含视频下载、缓存重载、文件合并及系统设置四大核心功能。基于yutto开发&#xff0c;采用图形化界面操作&#xff0c;极大简化B站资源获取与管理流程。 工具可以直接将原本缓存的视频读取&#…...

Docker Compose 完全指南:从入门到生产实践

Docker Compose 完全指南&#xff1a;从入门到生产实践 1. Docker Compose 简介与核心价值 Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个 YAML 文件来配置应用的服务&#xff0c;只需简单命令就能创建和启动所有服务。 核心优势&#xff1a;…...

《Redis应用实例》学习笔记,第二章:缓存二进制数据

前言 最近在学习《Redis应用实例》&#xff0c;这本书并没有讲任何底层&#xff0c;而是聚焦实战用法&#xff0c;梳理了 32 种 Redis 的常见用法。我的笔记在 Github 上&#xff0c;用 Jupyter 记录&#xff0c;会有更好的阅读体验&#xff0c;作者的源码在这里&#xff1a;h…...

ARM GIC(七)亲和路由:GICD_IROUTER寄存器具体如何与MPIDR配合使用?

ARM GIC&#xff08;一&#xff09; GIC V3架构基础学习笔记 完善亲和路由章节。 一、MPIDR MPIDR&#xff08;Multiprocessor Affinity Register&#xff09;寄存器在ARM架构中用于标识处理器的亲和性信息&#xff0c;这对于中断处理非常重要&#xff0c;特别是在多处理器系…...

stm32之SPI

目录 1.SPI通信协议1.1 简介1.2 硬件电路1.3 移位示意图1.4 SPI时序基本单元1.5 SPI时序 2.W25Q642.1 简介2.2 硬件电路2.3 框图2.3.1 结构介绍2.3.2 混淆 2.4 Flash操作注意事项2.4.1 写操作2.4.2 读取操作 2.5 芯片手册补充2.5.1 状态寄存器2.5.2 指令集 3.软件操作W25Q644.S…...

数据库事务以及JDBC实现事务

一、数据库事务 数据库事务&#xff08;Database Transaction&#xff09;是数据库管理系统中的一个核心概念&#xff0c;它代表一组操作的集合&#xff0c;这些操作要么全部执行成功&#xff0c;要么全部不执行&#xff0c;即操作数据的最小执行单元&#xff0c;保证数据库的…...

C语言_函数调用栈的汇编分析

在 C 语言的底层实现中&#xff0c;函数调用栈是程序运行时内存管理的核心机制。它不仅负责函数间的控制转移&#xff0c;还管理局部变量、参数传递和返回值。本文将结合 C 语言代码和 x86-64 汇编指令&#xff0c;深入解析函数调用栈的工作原理。 一、函数调用栈的基本概念 …...

单片机调用printf概率性跑飞解决方法

最近移植软件到不同平台的单片机上时&#xff0c;遇到了软件概率性跑飞的问题&#xff0c;分析后原因均指向和printf相关的库函数&#xff08;包括sprintf, vsnsprinft&#xff09;&#xff0c;在任务里调用这些函数就有概率在ucos切换任务时跑飞&#xff08;中断&#xff09;。…...

无人机空中物流优化:用 Python 打造高效配送模型

友友们好! 我是Echo_Wish,我的的新专栏《Python进阶》以及《Python!实战!》正式启动啦!这是专为那些渴望提升Python技能的朋友们量身打造的专栏,无论你是已经有一定基础的开发者,还是希望深入挖掘Python潜力的爱好者,这里都将是你不可错过的宝藏。 在这个专栏中,你将会…...

【RP2350】香瓜树莓派RP2350之低功耗

本文最后修改时间&#xff1a;2025年05月10日 01:57 一、本节简介 本节以树莓派pico2开发板为例&#xff0c;举例如何写一个低功耗驱动。 二、实验平台 1、硬件平台 1&#xff09;树莓派pico2开发板 ①树莓派pico2开发板&#xff08;作为仿真器&#xff09; ②micro usb数…...

招行数字金融挑战赛数据赛道赛题一

赛题描述&#xff1a;根据提供的用户行为数据&#xff0c;选手需要分析用户行为特征与广告内容的匹配关系&#xff0c;准确预测用户对测试集广告的点击情况&#xff0c;通过AUC计算得分。 得分0.6120&#xff0c;排名60。 尝试了很多模型都没有能够提升效果&#xff0c;好奇大…...

仿真生成激光干涉包裹相位数据-用于深度学习训练!

该MATLAB/Octave代码实现了论文[1]中提出的相位提取算法,用于从灰度条纹图案中提取包裹相位图(相位值在-π到+π之间)。代码首先生成模拟的条纹图案,包含背景光、调制光和相位分布,并加入高斯噪声。随后,通过N步相移算法估计背景光和调制光,并计算包裹相位。结果显示包括…...

命令行解释器中shell、bash和zsh的区别

命令行解释器&#xff08;Command Line Interpreter&#xff09;是一个程序&#xff0c;它的主要作用是接收用户输入的命令&#xff0c;并执行相应的操作。它充当了用户与操作系统内核之间的桥梁。 一、什么是 Shell&#xff1f; Shell 是一个通用术语&#xff0c;指的是 命令…...

SQL 数据库监控:SQL语句监控工具与实践案例

SQL 数据库监控&#xff1a;SQL语句监控工具与实践案例 SQL语句监控的主要方法 SQL监控主要通过以下几种方式实现&#xff1a; 数据库内置监控功能&#xff1a;大多数数据库系统提供内置的SQL监控工具数据库性能视图/系统表&#xff1a;通过查询特定的系统视图获取SQL执行信…...

招行数字金融挑战赛数据分析赛带赛题二

赛题描述&#xff1a;根据提供的脱敏资讯新闻数据&#xff0c;选手需要对提供的训练集进行特征工程&#xff0c;构建资讯分类模型&#xff0c;对与测试集进行准确的新闻分类。 最终得分&#xff1a;0.8120。十二点关榜没看到排名&#xff0c;估算100&#xff1f; 训练集很小&am…...

llama.cpp初识

Llama.cpp&#xff1a;赋能本地大语言模型推理的核心引擎及其应用场景 引言&#xff1a;Llama.cpp 是什么&#xff1f; 大型语言模型 (LLM) 的兴起正在深刻改变人机交互和信息处理的方式。然而&#xff0c;这些强大的模型通常需要巨大的计算资源&#xff0c;使得它们在云端之…...

【EBNF】EBNF:扩展巴克斯-诺尔范式文件格式与实用写法详解

EBNF&#xff1a;扩展巴克斯-诺尔范式文件格式与实用写法详解 一、什么是 EBNF&#xff1f; Extended Backus-Naur Form (EBNF)是一种形式化的语法&#xff0c;用于指定编程语言或其他形式化语言的结构。它是Backus-Naur形式&#xff08;BNF&#xff09;的扩展&#xff0c;最初…...

Go语言运算符详解

文章目录 1. 算术运算符2. 关系运算符3. 逻辑运算符4. 位运算符5. 赋值运算符6. 其他运算符运算符优先级注意事项 Go语言提供了与其他语言类似的运算符&#xff0c;包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等。这些运算符即可满足基本的运算需求。 1. 算…...

MySQL用户管理

目录 一、用户用户信息创建用户删除用户从远端登录修改用户密码 二、数据库的权限给用户授权回收权限 与Linux操作系统类似&#xff0c;MySQL中也有超级用户和普通用户之分。&#xff0c;如果一个用户只需要访问MySQL中的某一个数据库&#xff0c;甚至数据库中的某一个表&#…...

视频编解码学习三之显示器续

一、现在主流的显示器是LCD显示器吗&#xff1f; 是的&#xff0c;现在主流的显示器仍然是 LCD&#xff08;液晶显示器&#xff0c;Liquid Crystal Display&#xff09;&#xff0c;但它已经细分为多种技术类型&#xff0c;并和其他显示技术&#xff08;如OLED&#xff09;形成…...

VSCode1.101.0便携版|中英文|编辑器|安装教程

软件介绍 Visual Studio Code是微软推出的一个强大的代码编辑器&#xff0c;功能强大&#xff0c;操作简单便捷&#xff0c;还有着良好的用户界面&#xff0c;设计得很人性化&#xff0c;旨在为所有开发者提供一款专注于代码本身的免费的编辑器。 软件安装 1、 下载安装包…...

Scala 中累加器的创建与使用格式详解

1. 内置累加器的创建与使用格式 1.1 创建内置累加器 // 通过 SparkContext 创建 val acc sc.longAccumulator("累加器名称") // Long 类型&#xff08;默认初始值 0&#xff09; val accDouble sc.doubleAccumulator("累加器名称") // Double 类型&a…...

【DNDC模型】双碳目标下DNDC模型建模方法及在土壤碳储量、温室气体排放、农田减排、土地变化、气候变化中的应用

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。国家领导人在多次重要会议上讲到&#xff0c;要把“双碳”纳入经济社会发展和生态文明建设整体布局。同时&#xff0c;提到要把减污降碳协同增效作为促…...

深入剖析缓存与数据库一致性:Java技术视角下的解决方案与实践

一、缓存与数据库一致性问题根源 读写分离的架构矛盾 缓存作为数据库的“副本”&#xff0c;天然存在数据同步延迟。 高频读写场景下&#xff0c;缓存与数据库的更新顺序、失败重试等操作易引发不一致。 经典问题场景 场景1&#xff1a;先更新数据库&#xff0c;再删除缓存。…...

Anaconda环境中conda与pip命令的区别

文章目录 conda与pip的基本区别在Anaconda环境中的实际差异安装包环境管理依赖解决示例最佳实践建议 常见问题解答 conda与pip的基本区别 包来源与生态系统 conda&#xff1a;从Anaconda默认仓库或conda-forge等渠道获取包 不仅管理Python包&#xff0c;还能管理非Python依赖&…...

使用FastAPI和React以及MongoDB构建全栈Web应用05 FastAPI快速入门

一、FastAPI概述 1.1 什么是FastAPI FastAPI is a modern, high-performance Python web framework designed for building APIs. It’s rapidly gaining popularity due to its ease of use, speed, and powerful features. Built on top of Starlette, FastAPI leverages a…...

每日c/c++题 备战蓝桥杯(P1002 [NOIP 2002 普及组] 过河卒)

洛谷P1002 [NOIP 2002 普及组] 过河卒 题解 题目描述 过河卒是一道经典的动态规划题目。题目大意是&#xff1a;一个卒子从棋盘左上角(0,0)出发&#xff0c;要走到右下角(n,m)&#xff0c;棋盘上有一个马在(x,y)位置&#xff0c;卒子不能经过马所在位置及其周围8个位置。求卒…...

kubectl系列(十二):查询pod的resource 配置

在 Kubernetes 中&#xff0c;可以通过 kubectl 命令快速查询 Pod 的资源请求&#xff08;requests&#xff09;和限制&#xff08;limits&#xff09;配置。以下是多种方法实现这一目标&#xff1a; 1. 查看 Pod 的资源请求和限制&#xff08;基础版&#xff09; 使用 kubec…...

前端面试2

1. 面试准备 1. 建立自己的知识体系 思维导图ProcessOn框架Vue elementUI自查 https://zh.javascript.info/ 借鉴 https://juejin.cn/post/6844904103504527374http://conardli.top/blog/article/https://github.com/mqyqingfeng/Bloghttp://47.98.159.95/my_blog/#html 2.技能…...

使用 Java 反射动态加载和操作类

Java 的反射机制(Reflection)是 Java 语言的一大特色,它允许程序在运行时检查、加载和操作类、方法、字段等元信息。通过 java.lang.Class 和 java.lang.reflect 包,开发者可以动态加载类、创建实例、调用方法,甚至在运行时构造新类。反射是 Java 灵活性的核心,广泛应用于…...

基于Dockers的Bitwarden的私有本地部署

基于Dockers的Bitwarden的私有本地部署 文章目录 基于Dockers的Bitwarden的私有本地部署 本文首发地址 https://h89.cn/archives/355.html bitwarden 默认连接的是国外服务器 https://bitwarden.com/ &#xff0c;连接不是很稳定&#xff0c;也没有安全感&#xff0c;所以我选择…...

spark-Schema 定义字段强类型和弱类型

在数据处理和存储中&#xff0c;Schema&#xff08;模式&#xff09;定义了数据的结构和字段属性&#xff0c;其中字段的强类型和弱类型是重要的概念&#xff0c;直接影响数据的验证、存储和处理方式。以下是详细解释&#xff1a; 1. 强类型&#xff08;Strongly Typed&#x…...

【第35节 数据库设计】

本章目录: 一、节概述二、知识详解1. 数据库设计的基本步骤2. 用户需求分析3. 概念结构设计&#xff08;E-R建模&#xff09;4. 逻辑结构设计5. 物理结构设计6. 数据库实施7. 数据库运行维护8. 商业智能&#xff08;BI&#xff09;与数据仓库数据仓库的特点&#xff1a; 9. OLT…...

C++基本知识 —— 缺省参数·函数重载·引用

C基本知识 —— 缺省参数函数重载引用 1. 缺省参数2. 函数重载3. 引用3.1 引用的基础知识3.2 引用的作用3.3 const 引用3.4 指针与引用的关系 1. 缺省参数 什么是缺省参数&#xff1f;缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数的时候&#xff0c;如…...

大数据基础——Ubuntu 安装

文章目录 Ubuntu 安装一、配置电脑二、安装系统 Ubuntu 安装 一、配置电脑 1、进入VMware 2、选择配置类型 3、选择硬件兼容性版本 4、当前虚拟机的操作系统 选择“稍后安装操作系统”&#xff08;修改&#xff09; 5、选择虚拟机将来需要安装的系统 选中“Linux”和选择…...

英伟达微调qwen2.5-32B模型,开源推理模型:OpenCodeReasoning-Nemotron-32B

一、模型概述 OpenCodeReasoning-Nemotron-32B 是一个大型语言模型&#xff0c;基于 Qwen2.5-32B-Instruct 开发&#xff0c;专为代码生成推理任务进行了后续训练&#xff0c;支持 32,768 个标记的上下文长度&#xff0c;适用于商业和非商业用途。 二、性能表现 在 LiveCode…...

苍穹外卖-创建阿里云oss工具包

添加配置信息&#xff1a; sky:alioss:endpoint: ***access-key-id: ***access-key-secret: ***bucket-name: *** 把配置的内容转换成对象&#xff1a; Component ConfigurationProperties(prefix "sky.alioss") Data public class AliOssProperties {private St…...

代码随想录训练营第二十一天 |589.N叉数的前序遍历 590.N叉树的后序遍历

589.N叉数的前序遍历&#xff1a; 状态&#xff1a;已做出 思路&#xff1a; N叉树的前序遍历和二叉树很像&#xff0c;我这里使用栈来实现。首先把根结点入栈&#xff0c;然后删除栈顶节点后把栈顶节点的所有子树都插入到栈&#xff0c;这里需要注意的是插入的方式是从最后一…...

鸿蒙跨平台开发教程之Uniapp布局基础

前两天的文章内容对uniapp开发鸿蒙应用做了一些详细的介绍&#xff0c;包括配置开发环境和项目结构目录解读&#xff0c;今天我们正式开始写代码。 入门新的开发语言往往从Hello World开始&#xff0c;Uniapp的初始化项目中已经写好了一个简单的demo&#xff0c;这里就不再赘述…...

面试中常问的设计模式及其简洁定义

&#x1f3af; 一、面试中常问的设计模式及其简洁定义 模式名常被问到解释&#xff08;简洁&#xff09;单例模式✅ 高频保证一个类只有一个实例&#xff0c;并提供全局访问点。工厂模式✅ 高频创建对象的接口由子类决定&#xff0c;屏蔽了对象创建逻辑。抽象工厂模式✅提供多…...

关于 js:6. 网络与加密模块

一、AJAX AJAX&#xff08;Asynchronous JavaScript And XML&#xff09; 异步 JavaScript 与 XML&#xff08;现在多为 JSON&#xff09; 它允许网页在不重新加载整个页面的情况下&#xff0c;从服务器请求数据并更新页面内容。 主要用途&#xff1a; 提交表单时无需刷新页…...

量化交易系统开发经验分享--回测框架调研

一、前言 这段时间在集中做一个量化交易系统的开发任务&#xff0c;目前系统的MVP已经完成开发&#xff0c;后续会整理一些经验与成果和大家交流。刚好有一个前期做策略回测这块的调研&#xff0c;下面把调研的成果做一个整理总结先给大家分享一下&#xff0c;请批评指正。 在介…...

[学习]RTKLib详解:convkml.c、convrnx.c与geoid.c

本文是 RTKLlib详解 系列文章的一篇&#xff0c;目前该系列文章还在持续总结写作中&#xff0c;以发表的如下&#xff0c;有兴趣的可以翻阅。 [学习] RTKlib详解&#xff1a;功能、工具与源码结构解析 [学习]RTKLib详解&#xff1a;pntpos.c与postpos.c [学习]RTKLib详解&…...

【ajax基础】

提示&#xff1a;文章为 学习过程中的记录实践笔记。有问题欢迎指正。 文章目录 前言一、实现步骤二、完整示例三、封装总结 前言 AJAX 不是编程语言&#xff0c;是一种从网页访问web服务器的技术。 可以实现不刷新页面更新网页 在页面加载后从服务器请求/获取数据 在后台向服…...

Nodejs核心机制

文章目录 前言 前言 结合 Node.js 的核心机制进行说明&#xff1a; 解释事件循环的各个阶段。 答案 Node.js 事件循环分为 6 个阶段&#xff0c;按顺序执行&#xff1a; Timers&#xff1a;执行 setTimeout 和 setInterval 的回调。 Pending I/O Callbacks&#xff1a;处理系…...

Kubernetes 集群部署应用

部署 Nginx 应用 命令行的方式 1. 创建 deployment 控制器的 pod # --imagenginx&#xff1a;这个会从 docker.io 中拉取&#xff0c;这个网站拉不下来 # kubectl create deployment mynginx --imagenginx# 使用国内镜像源拉取 kubectl create deployment mynginx --imaged…...