网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)

目录

1. 重新理解协议

2. 网络版本计算器

2.1 前期封装

Log.hpp

sock.hpp

TcpServer.hpp

第一次测试(链接)

2.2 计算器实现

第二次测试(序列化和反序列化)

第三次测试(客户端+字节流)

CalServer.cc

CalClient.cc

3. 守护进程

3.1  守护进程和前后台进程

3.1 变成守护进程

4. Json序列化和反序列化

4.1 Json使用演示

4.2 Json改进计算器

Protocol.hpp

5. 本篇完。


1. 重新理解协议

再看这张图:(TCP/IP四层(五层)模型中,5,6,7三层都被看作了应用层)

通过前面学习知道,协议就是一种“约定”,在前面的TCP/UDP网络通信的代码中,读写数据的时候都是按照"字符串"的形式来发送和接收的,如果要传送一些结构化的数据怎么办呢?

拿经常使用的微信聊天来举例,聊天窗口中的信息包括头像(url),时间,昵称,消息等等,暂且将这几个信息看成是多个字符串,将这多个字符串形成一个结构化的数据:

struct/class message
{string url;string time;string nickname;string msg;
};

在聊天的过程中,通过网络发送的数据就成了上面代码所示的结构化数据,而不再是一个字符串那么简单。

如上图所示,用户A发送的消息虽然只有msg,但是经过用户层(微信软件)处理后,又增加了头像,时间,昵称等信息,形成一个结构化的数据struct/class message。

这个结构化的数据再发送到网络中,但是在发送之前,必须将结构化的数据序列化,然后才能通过socket发送到网络中。

序列化:就是将任意类型的数据或者数据结构转换成一个字符串。
如上图中的message结构体,序列化后就将所有成员合并成了一个字符串。

网络再将序列化后的数据发送给用户B,用户B接收到的报文必然是一个字符串。

然后用户B的应用层(微信软件)将接收到的报文进行反序列化,还原到原理的结构化数据message的样子,再将结构化数据中不同信息的字符串显式出来。

反序列化:就是将一个字符串中不同信息类型的字串提取出来,并且还原到结构化类型的数据。

业务结构数据在发送到网络中的时候,先序列化再发送。收到的一定是序列化后的字节流,要先进行反序列化,然后才能使用。

这里说的是TCP网络通信方式,它是面向字节流的,如果是UDP的就无需进行序列化以及反序列化,因为它是面向数据报的,无论是发送的还是接收到的,都是一个一个的数据。

在微信聊天的过程中,用户A发送message是一个结构化的数据,用户B接收到的message也是一个结构化的数据,而且它两的message中的成员变量都一样,如上图蓝色框中所示。

此时这个message就是用户A和用户B之间制定的协议。用户A的message是按照什么顺序组成的,用户B就必须按照什么顺序去使用它的message。

在这里协议不再停留在感性认识的“约定”上,而且具体到了结构化数据message中。


2. 网络版本计算器

例如, 我们需要实现一个服务器版的计算器,我们需要客户端把要计算的两个数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

这里通过实现一个网络版的计算器来讲解具体的用户协议定制以及序列化和反序列化的过程,其中用户向服务器发起计算请求,服务器计算完成后将结果响应给用户。协议是一种约定。看看方案:

约定方案一:
  • 客户端发送一个形如"1+1"的字符串
  • 这个字符串中有两个操作数,都是整形
  • 两个数字之间会有一个字符是运算符
  • 运算符只能是加减乘除和取模
  • 数字和运算符之间没有空格
约定方案二:
  • 定义结构体来表示我们需要交互的信息
  • 发送数据时将这个结构体按照一个规则转换成字符串
  • 接收到数据的时候再按照相同的规则把字符串转化回结构体

这个过程叫做 "序列化" 和 "反序列化

无论我们采用方案一,还是方案二,还是其他的方案,只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析,就是OK的,这种约定,就是应用层协议。
这里用第二种方案实现下网络版本的计算器。

2.1 前期封装

参考上面微信聊天的过程,我们知道了,网络通信过程中,服务器要做的事情是:接收数据报->反序列化->进行计算->把结果序列化->发送响应到网络中。

今天的重点不在网络通信的建立连接,而是协议定制以及序列化和反序列化,所以直接使用上篇文章中已经能建立好连接的服务器代码:

Log.hpp

把以前的日志拷过来:
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)  // 可变参数
{
#ifndef DEBUG_SHOWif(level== DEBUG) {return;}
#endifchar stdBuffer[1024]; // 标准日志部分time_t timestamp = time(nullptr); // 获取时间戳// struct tm *localtime = localtime(&timestamp); // 转化麻烦就不写了snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; // 自定义日志部分va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args); // 相当于ap=nullptrprintf("%s%s\n", stdBuffer, logBuffer);// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);// fclose(fp);
}

sock.hpp

把tcp_server.cc的关于套接字的部分封装成sock.hpp:

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:Sock(){}~Sock(){}int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAMif (listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr);if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 一般情况下:// const std::string &: 输入型参数// std::string *: 输出型参数// std::string &: 输入输出型参数int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if (port)*port = ntohs(src.sin_port);if (ip)*ip = inet_ntoa(src.sin_addr);return servicesock;}bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == 0)return true;elsereturn false;}
};

TcpServer.hpp

基于上一篇tcp_server.cc改的sock.hpp,再封装一个TcpServer.hpp:(二次封装)

#pragma once#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>namespace ns_tcpserver
{using func_t = std::function<void(int)>; // 回调,让tcp完成的方法class TcpServer; // 声明一下class ThreadData // 线程数据,当结构体使用{public:ThreadData(int sock, TcpServer *server):_sock(sock), _server(server){}~ThreadData() {}public:int _sock;TcpServer *_server;};class TcpServer{private:static void *ThreadRoutine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args); // 得到线程数据后强转td->_server->Excute(td->_sock); // 线程内部调用要执行的方法close(td->_sock);return nullptr;}public:TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0") // 构造函数初始化{_listensock = _sock.Socket();_sock.Bind(_listensock, port, ip);_sock.Listen(_listensock);}void BindService(func_t func)  // 绑定一个服务方法{ _func.push_back(func);}void Excute(int sock) // 执行被绑定的方法{for(auto &f : _func) // 遍历所有方法让线程去执行{f(sock);}}void Start(){while(true) // 不断获取新链接{std::string clientip;uint16_t clientport;int sock = _sock.Accept(_listensock, &clientip, &clientport);if (sock == -1)continue;logMessage(NORMAL, "create new link success, sock: %d", sock);pthread_t tid; // 多线程式的服务ThreadData *td = new ThreadData(sock, this); // 线程处理网络服务,要得到sockpthread_create(&tid, nullptr, ThreadRoutine, td);}}~TcpServer(){if (_listensock >= 0)close(_listensock);}private:int _listensock;Sock _sock;std::vector<func_t> _func;};
}

第一次测试(链接)

Makefile

.PHONY:all
all:client CalServerclient:CalClient.ccg++ -o $@ $^ -std=c++11
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -f client CalServer

CalServer.cc

#include "TcpServer.hpp"
#include <memory>using namespace ns_tcpserver;static void Usage(const std::string &process) // 使用手册
{std::cout << "\nUsage: " << process << " port\n" << std::endl;
}void Debug(int sock) // 测试服务
{std::cout << "我是一个测试服务, 得到的sock是: " << sock << std::endl;
}// ./CalServer port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1]))); // 网络功能server->BindService(Debug); // 绑定一个服务方法,网络功能和服务进行了解耦server->Start();return 0;
}

CalClient.cc

#include <iostream>int main(int argc, char *argv[])
{return 0;
}

编译运行:

成功运行,客户端什么也没做,链接一建立就自动退出了。


2.2 计算器实现

先把我们约定的协议(Protocol)封装成一个文件:

Protocol.hpp先写一个框架:

#pragma once#include <iostream>
#include <string>
#include <cstring>namespace ns_protocol
{class Request // 请求, 现在即要运算的式子{public:std::string Serialize() // 序列化{}bool Deserialized(const std::string &str) // 反序列化{}public:Request(){}Request(int x, int y, char op) : _x(x), _y(y), _op(op){}~Request() {}public: // 如果私有就要写get函数了,下面也不私有了// 约定int _x;int _y;char _op; // '+' '-' '*' '/' '%'};class Response // 应答, 现在即要运算的式子+结果{public:std::string Serialize() // 序列化{}std::string Deserialized() // 反序列化{}public:Response(){}Response(int result, int code, int x, int y, char op) : result_(result), code_(code), _x(x), _y(y), _op(op){}~Response() {}public:// 约定int result_; // 计算结果int code_;   // 计算结果的状态码int _x;int _y;char _op;};bool Recv(int sock, std::string *out) // 读取数据{}void Send(int sock, const std::string str) // 发送数据{}std::string Decode(std::string &buffer) // 协议解析,保证得到一个完整的报文{}std::string Encode(std::string &s) // 添加长度信息,形成一个完整的报文{}
}

下面把上面的测试服务函数改成计算器服务函数,在CalServer.cc写一个calculator函数:

static Response calculatorHelper(const Request &req) // 计算器助手,把结构化的请求转为结构化的响应
{Response resp(0, 0, req._x, req._y, req._op);switch (req._op){case '+':resp.result_ = req._x + req._y;break;case '-':resp.result_ = req._x - req._y;break;case '*':resp.result_ = req._x * req._y;break;case '/':if (0 == req._y)resp.code_ = 1; // 自己定义的类似错误码elseresp.result_ = req._x / req._y;break;case '%':if (0 == req._y)resp.code_ = 2;elseresp.result_ = req._x % req._y;break;default:resp.code_ = 3;break;}return resp;
}void calculator(int sock) // 网络计算器
{while (true){std::string str = Recv(sock); // 在这里我们读到了一个请求Request req;req.Deserialized(str); // 反序列化, 字节流 -> 结构化Response resp = calculatorHelper(req); // 计算,得到计算结果std::string respString = resp.Serialize(); // 对计算结果进行序列化Send(sock, respString);}
}

读取和发送:(暂时不考虑这么多,可以想想还要考虑什么)

    std::string Recv(int sock) // 读取数据{char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer), 0);if (s > 0)return buffer;return "";}void Send(int sock, const std::string str) // 发送数据{int n = send(sock, str.c_str(), str.size(), 0);if (n < 0)std::cout << "send error" << std::endl;}

Request的序列化和反序列化:

#define MYSELF 1#define SPACE " " // 多少个空格或者其它符号
#define SPACE_LEN strlen(SPACE)// 1. 自主实现序列化的格式: "length\r\n_x _op _y\r\n" (约定/协议)class Request // 请求, 现在即要运算的式子{public:std::string Serialize() // 序列化{
#ifdef MYSELFstd::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else
// 另一种序列化反序列化方案
#endif}// "_x _op _y"// "1234 + 5678"bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t left = str.find(SPACE); // 找空格if (left == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;_x = atoi(str.substr(0, left).c_str()); // 截取子串,前闭后开_y = atoi(str.substr(right + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size())return false;else_op = str[left + SPACE_LEN];return true;
#else
// 另一种序列化反序列化方案
#endif}

第二次测试(序列化和反序列化)

// ./CalServer port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}// // 第一次测试// std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1]))); // 网络功能// server->BindService(calculator); // 绑定一个服务方法,网络功能和服务进行了解耦// server->Start();// 第二次测试Request req(1234, 5678, '+');std::string s = req.Serialize(); // 序列化std::cout << s << std::endl;Request temp;temp.Deserialized(s); // 反序列化std::cout << temp._x << std::endl;std::cout << temp._op << std::endl;std::cout << temp._y << std::endl;return 0;
}

编译运行:

成功完成了运算式的序列化和反序列化


第三次测试(客户端+字节流)

上面的代码,有没有可能你正在向服务器写入时,别人直接把你的链接给关了,这是有可能的(你正在说话,别人直接走了),此时操作系统就不让你写了,直接把进程关掉了,这是经常要考虑的问题。(常见的解决方法就是对信号进行忽略,或者对读取进行相关的判断)

还有读取请求的时候怎么保证读到的是一个完整的请求呢,如果是半个或者两个半之类的呢,三个四个连在一起又怎么处理呢,所以下面就要对上面的代码进行改进。

UDP是面向数据报的,TCP面向字节流的。在TCP怎么保证读到一个完整的报文呢?

这里我们用在报文前面加报文长度和符号的方法。前面定义宏:

#define SEP "\r\n" // 分隔符
#define SEP_LEN strlen(SEP) // 不能是sizeof

改一下Reve,加两个函数:

    bool Recv(int sock, std::string *out) // 读取数据, 返回一个完整的报文{// UDP是面向数据报, TCP 面向字节流的:char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); // 9\r\n123+789\r\nif (s > 0){buffer[s] = 0;*out += buffer;}else if (s == 0)return false;elsereturn false;return true;}void Send(int sock, const std::string str) // 发送数据{int n = send(sock, str.c_str(), str.size(), 0);if (n < 0)std::cout << "send error" << std::endl;}//读取到的各种情况: "length\r\n_x _op _y\r\n..." // 10\r\nabc // "_x _op _y\r\n length\r\nXXX\r\n"std::string Decode(std::string &buffer) // 协议解析,保证得到一个完整的报文{std::size_t pos = buffer.find(SEP); // 找分隔符if(pos == std::string::npos) return "";int size = atoi(buffer.substr(0, pos).c_str());int surplus = buffer.size() - pos - SEP_LEN * 2; // 读取到的有效长度(剩余)if(surplus >= size) // 至少具有一个合法完整的报文, 可以提取了{buffer.erase(0, pos + SEP_LEN);std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);return s;}elsereturn "";}std::string Encode(std::string &s) // 添加长度信息,形成一个完整的报文{   // "XXXXXxX" -> "7\r\nXXXxXXX\r\n"std::string new_package = std::to_string(s.size());new_package += SEP;new_package += s;new_package += SEP;return new_package;}

此时CalServer.cc就变成这样:

CalServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <memory>using namespace ns_tcpserver;
using namespace ns_protocol;static void Usage(const std::string &process) // 使用手册
{std::cout << "\nUsage: " << process << " port\n" << std::endl;
}// void Debug(int sock) // 测试服务
// {
//     std::cout << "我是一个测试服务, 得到的sock是: " << sock << std::endl;
// }static Response calculatorHelper(const Request &req) // 计算器助手,把结构化的请求转为结构化的响应
{Response resp(0, 0, req._x, req._y, req._op);switch (req._op){case '+':resp._result = req._x + req._y;break;case '-':resp._result = req._x - req._y;break;case '*':resp._result = req._x * req._y;break;case '/':if (0 == req._y)resp._code = 1; // 自己定义的类似错误码elseresp._result = req._x / req._y;break;case '%':if (0 == req._y)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}void calculator(int sock) // 网络计算器
{std::string inbuffer;while (true){// std::string str = Recv(sock); // 在这里我们读到了一个请求// req.Deserialized(str); // 反序列化, 字节流 -> 结构化// Response resp = calculatorHelper(req); // 计算,得到计算结果// std::string respString = resp.Serialize(); // 对计算结果进行序列化// Send(sock, respString);bool res = Recv(sock, &inbuffer); // 1. 读到了一个请求if(!res) // 读取失败break;std::string package = Decode(inbuffer); //  2. 协议解析,保证得到一个完整的报文if (package.empty())continue;logMessage(NORMAL, "%s", package.c_str());Request req; // 3. 保证该报文是一个完整的报文req.Deserialized(package); // 4. 反序列化,字节流 -> 结构化Response resp = calculatorHelper(req); // // 5. 业务逻辑(把结构化的请求转为结构化的响应),计算,得到计算结果std::string respString = resp.Serialize(); // 6. 对计算结果进行序列化respString = Encode(respString); // 7. 添加长度信息,形成一个完整的报文Send(sock, respString); // 8. send这里暂时先这样写,多路转接的时候,再谈发送的问题}
}void handler(int signo)
{std::cout << "get a signo: " << signo << std::endl;exit(0);
}// ./CalServer port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}MyDaemon();// 第一次测试+第三次测试std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1]))); // 网络功能server->BindService(calculator); // 绑定一个服务方法,网络功能和服务进行了解耦server->Start();// // 第二次测试// Request req(1234, 5678, '+');// std::string s = req.Serialize(); // 序列化// std::cout << s << std::endl;// Request temp;// temp.Deserialized(s); // 反序列化// std::cout << temp._x << std::endl;// std::cout << temp._op << std::endl;// std::cout << temp._y << std::endl;return 0;
}

直接放CalClient.cc:

CalClient.cc
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"using namespace ns_protocol;static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " serverIp serverPort\n" << std::endl;
}// ./client server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);Sock sock;int sockfd = sock.Socket();if (!sock.Connect(sockfd, server_ip, server_port)){std::cerr << "Connect error" << std::endl;exit(2);}bool quit = false;std::string buffer;while (!quit){Request req; // 1. 获取需求,可以不用cin,用getline等优化std::cout << "Please Enter # ";std::cin >> req._x >> req._op >> req._y;std::string s = req.Serialize(); // 2. 序列化std::string tmp = s;s = Encode(s); // 3. 添加长度报头Send(sockfd, s); // 4. 发送给服务端while (true) // 5. 正常读取{bool res = Recv(sockfd, &buffer);if (!res){quit = true;break;}std::string package = Decode(buffer);if (package.empty()) // 至少读到一个完整报文才往后走continue;Response resp;resp.Deserialized(package);std::string err;switch (resp._code){case 1:err = "除0错误";break;case 2:err = "模0错误";break;case 3:err = "非法操作";break;default:std::cout << tmp << " = " << resp._result << " [success]" << std::endl;break;}if(!err.empty()) std::cerr << err << std::endl;// sleep(1);break;}}close(sockfd);return 0;
}

编译运行:


3. 守护进程

3.1  守护进程和前后台进程

重新运行上面的服务端,再复制会话输入netstat -lntp

可以看到,IP地址为0.0.0.0,端口号为7070,进程名为CalServer的进程是存在的。

直接关掉左边的Xshell会话窗口,不退出进程,再输入netstat -lntp

此时再查看名为CalServer的进程,已经看不到了,说明它已经退出了,但是我们明明没有让它退出啊,只是关掉了Xshell的窗口而已。

每一个Xshell窗口都会在服务器上创建一个会话,准确的说会运行一个名字为bash的进程。

每一个会话中最多只有一个前台任务,可以有多个后台任务(包括0个)。

当Xshell的窗口关闭后,服务器上对应的会话就会结束,bash进程就退出了,bash维护的所有进程都会退出。所以关掉Xshell窗口后CalServer进程就会退出。

这样就存在一个问题,提供网络服务的服务器难道运行了CalServer就不能干别的了吗?肯定不是。要想关掉Xshell后CalServer不退出,只能让CalServer自成一个会话。

自成一个会话的进程就被叫做守护进程,也叫做精灵进程

前后台进程组:

sleep 10000 | sleep 20000 | sleep 30000是通过管道一起创建的1个进程,这些进程组成一个进程组,也被叫做一个作业。后面又加了&表示这个作业是后台进程。

(使用指令jobs可以查看当前机器上的作业)

前面的数组是进程组的编号,如上图所示的【1】【2】【3】【4】。

通过指令fg+进程组编号,可以将后台进程变成前台进程,如上图所示,此时Xshell窗口就阻塞住了,在做延时,我们无法输入其他东西。

将该进程组暂停后,继续使用jobs可以看到,进程组1后面的&没有了,表示这是一个前台进程,只是暂停了而已。

使用指令bg+进程组编号,可以将进程组设置为后台进程,如上图所示,此时进程组1后面的&又有了,并且进程运行了起来,也不再阻塞了,可以在窗口中继续输入指令了。

输入命令行脚本:

ps ajx | head -n1 && ps ajx | grep sleep

以看到,这么多个sleep进程的pid值都不同,因为它们是独立的进程。

PGID表示进程组的ID,其中PID和PGID值相同的进程是这个进程组的组长

看到PGID,每个框中有3个相同的PGID,所以此时就有3组进程,和前面使用管道创建的进程组结果一样。

但是所有进程的PPID都是10452,这个进程就是bash,所以说,bash就是当前会话中所有进程的父进程。 还有一个SID,表示会话ID,所有进程的SID都相同,因为它们同属于一个会话。

PPID和SID之所以相同,是因为会话的本质就是bash。


3.1 变成守护进程

要想让会话关闭以后进程还在运行,就需要让这个进程自成一个会话,也就是成为守护进程

系统调用setsid的作用就是将调用该函数的进程变成守护进程,也就是创建一个新的会话,这个会话中只有当前进程。man 2 setsid:

看到一大堆英语里的第一句话:创建一个新会话,但该进程不能是进程组的组长

调用系统调用setsid的进程在调用之前不能是进程组的组长,否则无法创建新的会话,也就无法成为守护进程。

不能打印到显示器了,把Log.hpp改成打印到文件的:

改一下LOGFILE:

在服务端一开始就调用:

编译运行:

在运行服务端程序后,服务器进程初始化,然后变成守护进程并且开始运行(这一点我们看不到)。当前会话并没有阻塞,仍然可以数据其他指令。

查看当前服务器上的进程时,可以看到守护进程CalServer的存在,并且它的PPID是1(操作系统),PID,PGID以及SID三者都是10856。

此时关掉左边的Xshell再输入上面的指令:

你整个机子退出了,守护进程还是1在那,平时我们用的APP就是这个原理。

守护进程自成会话,自成进程组,和终端设备无关。

可以用kill 终止守护进制:

值得一提的是有一个系统调用daemon可以让一个进程变成守护进程,man daemon:

但是它并不太好用,实际应用中都通过setsid自己实现daemon的,就像我们上面写的一样。


4. Json序列化和反序列化

前面敲了一遍如何进行序列化以及反序列化,目的是为了能够更好的感受到序列化和反序列化也是协议的一部分,以及协议被制订的过程。

虽然序列化和反序列化可以自己实现,但是非常麻烦,有一些现成的工具可以直接进行序列化和反序列化,如:

  • json——使用简单。
  • protobuf——比较复杂,局域网或者本地网络通信使用较多。
  • xml——其他编程语言使用(如Java等)。

这里只介绍json的使用,同时这也是使用最广泛的,有兴趣的小伙伴可以去了解下protobuf。

  • 对于序列化和反序列化,有现成的解决方案,绝对不要自己去写。
  • 序列化和反序列化不等于协议,协议仍然可以自己制定。

在使用json之前,需要先在Linux机器上安装json工具,使用yum去安装:

切换到root,输入:

json安装后,它的头文件json.h所在路径为/usr/include/jsoncpp/json/,由于编译器自动查找头文件只到usr/include,所以在使用json时,包含头文件的形式为jsoncpp/json/json.h。

json是一个动态库,它所在路径为/lib64/,完整的名字为libjsoncpp.sp,在使用的时候,编译器会自动到/lib64路径下查找所用的库,所以这里不用包含库路径,但是需要指定库名,也就是掐头去尾后的结果jonscpp。


4.1 Json使用演示

这里新建一个TestJson目录在里面写个test.cc代码演示一下json的使用,

Json数据的格式是采用键值对的形式,如:

"first" : x
"second" : y
"oper" : op"exitcode" : exitcode
"result" : result

就是将不同类型的变量和一个字符串绑定起来形成键值对,序列化的时候将多个字符串拼接在一起形成一个字符串。反序列化的时候再将多个字符串拆开,根据键值对的对应关系找到绑定的变量。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>int main()
{int a = 7;int b = 10;char c = '+';Json::Value root; // 定义一个万能对象root["aa"] = a; // 把abc三个对象分别放入Json的万能对象root["bb"] = b;root["op"] = c;Json::StyledWriter writer;std::string s = writer.write(root); // 把万能对象传给write,自动返回序列化的结果std::cout << s << std::endl;
}

编译运行需要带-ljsoncpp:

看得出来格式不是很和预料的一样,常用的还是FastWriter:

重新编译运行;

区别就只是形成序列化的格式不同。

值得一提的是Json里面是可以"套娃的":

重新编译运行;

这里就演示了序列化的过程,反序列就直接在下面计算器的代码里演示了。

这里贴一下下面计算器代码Request里的序列化和反序列话:


4.2 Json改进计算器

在运行之前试试我们之前写的序列化和反序列化和日志写入文件的样子:

左边关掉再运行下client:

此时VSCode里看看log文件:

现在动手改我们的Protocol.hpp,把序列化和反序列化改成json的:

在Json使用演示最后贴了两张图,这里直接放完整代码了:

Makefile:

.PHONY:all
all:client CalServerclient:CalClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f client CalServer

Protocol.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>namespace ns_protocol
{
// #define MYSELF 1#define SPACE " " // 多少个空格或者其它符号
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n" // 分隔符
#define SEP_LEN strlen(SEP) // 不能是sizeof// 1. 自主实现序列化的格式: "length\r\n_x _op _y\r\n" (约定/协议)class Request // 请求, 现在即要运算的式子{public:std::string Serialize() // 序列化{
#ifdef MYSELFstd::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else
// 另一种序列化反序列化方案Json::Value root; // 万能对象root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::FastWriter writer;return writer.write(root); // 返回值是序列化好的结果,直接return
#endif}// "_x _op _y"// "1234 + 5678"bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t left = str.find(SPACE); // 找空格if (left == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;_x = atoi(str.substr(0, left).c_str()); // 截取子串,前闭后开_y = atoi(str.substr(right + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size())return false;else_op = str[left + SPACE_LEN];return true;
#else
// 另一种序列化反序列化方案Json::Value root; // 继续定义万能Value对象Json::Reader reader; // 定义Reader对象reader.parse(str, root); // 调用parse,传入序列化好的字符串str和万能对象_x = root["x"].asInt(); // 拿到key值"x"对应的val,asInt是当做整数的意思_y = root["y"].asInt();_op = root["op"].asInt(); // char类型的本质也是整数return true;
#endif}public:Request(){}Request(int x, int y, char op) : _x(x), _y(y), _op(op){}~Request() {}public: // 如果私有就要写get函数了,下面也不私有了// 约定int _x;int _y;char _op; // '+' '-' '*' '/' '%'};class Response // 应答, 现在即要运算的式子+结果{public:// "_code _result"std::string Serialize() // 序列化{
#ifdef MYSELFstd::string s;s = std::to_string(_code);s += SPACE;s += std::to_string(_result);return s;
#else
// 另一种序列化反序列化方案Json::Value root; // 和Request的步骤一样root["code"] = _code;root["result"] = _result;root["xx"] = _x;root["yy"] = _y;root["zz"] = _op;Json::FastWriter writer;return writer.write(root);
#endif}// "6912 0"bool Deserialized(const std::string &s) // 反序列化{
#ifdef MYSELFstd::size_t pos = s.find(SPACE);if (pos == std::string::npos)return false;_code = atoi(s.substr(0, pos).c_str());_result = atoi(s.substr(pos + SPACE_LEN).c_str());return true;
#else
// 另一种序列化反序列化方案Json::Value root; // 和Request的步骤一样Json::Reader reader;reader.parse(s, root);_code = root["code"].asInt();_result = root["result"].asInt();_x =  root["xx"].asInt();_y =  root["yy"].asInt();_op =  root["zz"].asInt();return true;
#endif}public:Response(){}Response(int result, int code, int x, int y, char op) : _result(result), _code(code), _x(x), _y(y), _op(op){}~Response() {}public:// 约定int _result; // 计算结果int _code;   // 计算结果的状态码int _x;int _y;char _op;};bool Recv(int sock, std::string *out) // 读取数据, 返回一个完整的报文{// UDP是面向数据报, TCP 面向字节流的:char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); // 9\r\n123+789\r\nif (s > 0){buffer[s] = 0;*out += buffer;}else if (s == 0)return false;elsereturn false;return true;}void Send(int sock, const std::string str) // 发送数据{int n = send(sock, str.c_str(), str.size(), 0);if (n < 0)std::cout << "send error" << std::endl;}//读取到的各种情况: "length\r\n_x _op _y\r\n..." // 10\r\nabc // "_x _op _y\r\n length\r\nXXX\r\n"std::string Decode(std::string &buffer) // 协议解析,保证得到一个完整的报文{std::size_t pos = buffer.find(SEP); // 找分隔符if(pos == std::string::npos) return "";int size = atoi(buffer.substr(0, pos).c_str());int surplus = buffer.size() - pos - SEP_LEN * 2; // 读取到的有效长度(剩余)if(surplus >= size) // 至少具有一个合法完整的报文, 可以提取了{buffer.erase(0, pos + SEP_LEN);std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);return s;}elsereturn "";}std::string Encode(std::string &s) // 添加长度信息,形成一个完整的报文{   // "XXXXXxX" -> "7\r\nXXXxXXX\r\n"std::string new_package = std::to_string(s.size());new_package += SEP;new_package += s;new_package += SEP;return new_package;}
}

编译运行:(注意把#define MYSELF 1注释掉)

过了一段时间回来还可以看到我们上面的守护进程还在运行,然后kill掉重新链接一下,此时的日志就是这样的:

可以看出和自己写的序列化和反序列化方案还是有很大的区别的。


5. 本篇完。

此篇的重点内容就是手写了具体的协议,对协议的认识更加深刻。之后无论是序列化还是协议都直接用现成的就好,但是要知道现成的干了什么事情。

下一篇开始http协议的学习,再就是https协议。

下一篇:网络和Linux网络_5(应用层)HTTP协议(方法+报头+状态码)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/158740.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数组扩展方法(一)

Array.prototype.forEach MDN解释forEach()方法是对数组的每个元素执行一个给定的函数&#xff0c;换句话来说就是在调用forEach()方法的时候&#xff0c;需要传入一个回调函数callback&#xff0c;循环每个数组内部元素时都会执行一次传入的回调函数callback forEach()方法的…

AUTOSAR实战篇:基于ETAS工具链的信息安全协议栈集成指南

AUTOSAR实战: 基于ETAS工具链的信息安全协议栈集成指南 前言 小T出品,必是精品! 手把手带你集成信息安全协议栈,你值得拥有! 正文 随着汽车信息安全的不断发展与完善,其在汽车电子领域如智能驾驶(ADAS),智能座舱等方向上不断被重视起来,越来越多的Tier1,主机厂都在全面…

LeetCode算法心得——爬楼梯(记忆化搜索+dp)

大家好&#xff0c;我是晴天学长&#xff0c;第二个记忆化搜索练习&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1&#xff09;爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或…

Redis主从复制,哨兵和Cluster集群

主从复制&#xff1a; 主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份&#xff08;和同步&#xff09;&#xff0c;以及对于读操作的负载均衡和简单的故障恢复。 缺陷&#xff1a;故障恢复无法自动化…

C# ReadOnlyRef Out

C# ReadOnly ReadOnly先看两种情况1.值类型2.引用类型 结论 Ref Out ReadOnly官方文档 ReadOnly 先看两种情况 1.值类型 当数据是值类型时&#xff0c;标记为Readonly时&#xff0c;如果再次设置值&#xff0c;会提示报错&#xff0c;无法分配到只读字段 public class A {pri…

基于Springboot的美容院管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的美容院管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&a…

Redis(事务和持久化)(很重要!)

事务的定义&#xff1a; Redis中的事务是指一组命令的集合&#xff0c;这些命令可以在一个原子操作中执行。在Redis中&#xff0c;可以使用MULTI命令开始一个事务&#xff0c;然后使用EXEC命令来执行事务中的所有命令&#xff0c;或者使用DISCARD命令来取消事务。事务可以确保…

爬取春秋航空航班信息

一、使用fiddler爬取小程序春秋航空航班信息 使用Fiddler爬取春秋航空微信小程序&#xff08;手机上由于网络问题&#xff0c;无法进入&#xff0c;使用电脑版&#xff09; 搜索航班信息 搜索记录 使用Fiddler查找url(没有得到有效url) 继续查找&#xff0c;发现航班信息列…

数据结构:二叉树(初阶)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下二叉树方面的相关知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 …

振南技术干货集:制冷设备大型IoT监测项目研发纪实(3)

注解目录 1.制冷设备的监测迫在眉睫 1.1 冷食的利润贡献 1.2 冷设监测系统的困难 &#xff08;制冷设备对于便利店为何如何重要&#xff1f;了解一下你所不知道的便利店和新零售行业。关 于电力线载波通信的论战。&#xff09; 2、电路设计 2.1 防护电路 2.1.1 强电防护…

LeetCode:2304. 网格中的最小路径代价(C++)

目录 2304. 网格中的最小路径代价 题目描述&#xff1a; 实现代码&#xff1a; dp&#xff08;dp有很多相似的经典题目&#xff0c;比较简单&#xff0c;不再给出解析&#xff09; 2304. 网格中的最小路径代价 题目描述&#xff1a; 给你一个下标从 0 开始的整数矩阵 grid …

Redis篇---第十四篇

系列文章目录 文章目录 系列文章目录前言一、为什么Redis的操作是原子性的,怎么保证原子性的?二、了解Redis的事务吗?四、Redis 的数据类型及使用场景前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男…

前端js调取摄像头并实现拍照功能

前言 最近接到的一个需求十分有意思&#xff0c;设计整体实现了前端仿 微信扫一扫 的功能。整理了一下思路&#xff0c;做一个分享。 tips: 如果想要实现完整扫一扫的功能&#xff0c;你需要掌握一些前置知识&#xff0c;这次我们先讲如何实现拍照并且保存的功能。 一. windo…

什么是凸函数

假设函数是定义在某个向量空间的凸子集上的实值函数&#xff0c;并且&#xff0c;如果对于中的任何两个向量和&#xff0c;都满足&#xff1a; 则称为上的凸函数

动态规划求 x 轴上相距最远的两个相邻点 java 代码实现

如图为某一状态下 x 轴上的情况&#xff0c;此时 E、F相距最远&#xff0c;现在加入一个点H&#xff0c;如果H位于点A的左边的话&#xff0c;只需要比较 A、H 的距离 和 E、F 的距离&#xff1b;如果点H位于点G的右边&#xff0c;则值需要比较 G、H 的距离 和 E、F 的距离&…

前端实现表格生成序号001、002、003自增

我们最终想要实现的效果如图&#xff0c;从后端获取数据之后&#xff0c;不使用data中的id&#xff0c;而是使用自己生成的按照顺序自增的序号id。 script <template><el-table :data"sticker" border style"width: 100%" id"stickerList&q…

[Python人工智能] 四十.命名实体识别 (1)基于BiLSTM-CRF的威胁情报实体识别万字详解

从本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。前一篇文章普及VS Code配置Keras深度学习环境,并对比常用的深度学习框架,最后普及手写数字识别案例。这篇文章将讲解如何实现威胁情报实体识别,利用BiLSTM-CRF算法实现对ATT&CK相关的技战术实体…

navicat --CSV导出数据乱码情况(三种情况解决方式)

CSV导出数据乱码情况分析及处理 在navicat 中有很多导出方式&#xff0c;大家都知道csv导出要比xlse要快很多&#xff0c;但是在使用csv导出时要防止乱码情况&#xff0c; 下面我列出三种处理方式&#xff08;如有其他方式大家可以帮忙补充一下&#xff09;&#xff1a; 文章目…

基于springboot实现班级综合测评管理系统项目【项目源码+论文说明】

基于springboot实现班级综合测评管理系统演示 摘要 随着互联网技术的高速发展&#xff0c;人们生活的各方面都受到互联网技术的影响。现在人们可以通过互联网技术就能实现不出家门就可以通过网络进行系统管理&#xff0c;交易等&#xff0c;而且过程简单、快捷。同样的&#x…

redis的高可用之持久化

1、redis的高可用考虑指标 &#xff08;1&#xff09;正常服务 &#xff08;2&#xff09;数据容量的扩展 &#xff08;3&#xff09;数据的安全性 2、redis实现高可用的四种方式 &#xff08;1&#xff09;持久化 &#xff08;2&#xff09;主从复制 &#xff08;3&…