网络和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.rhkb.cn/news/202628.html

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

相关文章

zlmediakit实现rtsp流服务器

本次实现是将内存中的H264数据经过zlmediakit实现为rtsp流。 我是用的是CAPI的方式&#xff0c;将zlmediakit作为一个sdk嵌入到自己的程序中而不是作为一个独立的进进程服务。 1.编译完成zkmedialit后会得到bin include lib三个文件夹如图 其中bin中的MediaServer是作为独立的…

二蛋赠书八期:《Java物联网、人工智能和区块链编程实战》

前言 大家好&#xff01;我是二蛋&#xff0c;一个热爱技术、乐于分享的工程师。在过去的几年里&#xff0c;我一直通过各种渠道与大家分享技术知识和经验。我深知&#xff0c;每一位技术人员都对自己的技能提升和职业发展有着热切的期待。因此&#xff0c;我非常感激大家一直…

常见树种(贵州省):015榧树、秋枫、滇合欢、锥栗、红豆树、刺槐、余甘子、黑荆、槐树、黄檀

摘要&#xff1a;本专栏树种介绍图片来源于PPBC中国植物图像库&#xff08;下附网址&#xff09;&#xff0c;本文整理仅做交流学习使用&#xff0c;同时便于查找&#xff0c;如有侵权请联系删除。 图片网址&#xff1a;PPBC中国植物图像库——最大的植物分类图片库 一、榧树 …

cocos2dx ​​Animate3D(二)

Twirl 扭曲旋转特效 // 持续时间(时间过后不会回到原来的样子) // 整个屏幕被分成几行几列 // 扭曲中心位置 // 扭曲的数量 // 振幅 static Twirl* create(float duration, const Size& gridSize, const Vec2& position, unsigned int twirls, float amplitude)…

​LeetCode解法汇总2304. 网格中的最小路径代价

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个下…

Tars-GO 开发

默认环境是安装好的 创建服务: tarsgo make App Server Servant GoModuleName Tars 实例的名称&#xff0c;有三个层级&#xff0c;分别是 App&#xff08;应用&#xff09;、Server&#xff08;服务&#xff09;、Servant&#xff08;服务者&#xff0c;有时也称 Object&am…

构造命题公式的真值表

构造命题公式的真值表 1&#xff1a;实验类型&#xff1a;验证性2&#xff1a;实验目的&#xff1a;3&#xff1a;逻辑联结词的定义方法4&#xff1a;命题公式的表示方法5&#xff1a;【实验内容】 1&#xff1a;实验类型&#xff1a;验证性 2&#xff1a;实验目的&#xff1a…

Python满天星

系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.csdn.net/article/details/1295031234漂浮爱心https://want…

Linux 家目录和根目录

摘要&#xff1a; 在 Linux 操作系统中&#xff0c;家目录和根目录是两个非常重要的概念。它们是 Linux 文件系统中的两个关键节点&#xff0c;为用户和系统进程提供存储、管理和访问文件和目录的接口。本文旨在深入探讨和理解这两个目录的结构、功能和使用方式&#xff0c;同时…

C#,《小白学程序》第六课:队列(Queue)其二,队列的应用,编写《实时叫号系统》

医院里面常见的《叫号系统》怎么实现的&#xff1f; 1 文本格式 /// <summary> /// 下面定义一个新的队列&#xff0c;用于演示《实时叫号系统》 /// </summary> Queue<Classmate> q2 new Queue<Classmate>(); /// <summary> /// 《小白学程序…

<C++> 继承

目录 前言 一、继承概念 1. 继承概念 2. 继承定义格式 3. 继承关系和访问限定符 4. 继承基类成员访问方式的变化 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 五、继承与友元 六、继承与静态成员 七、菱形继承及菱形虚拟继承 1. 菱形继承 2. 虚…

什么是高防IP?有什么优势?怎么选择高防IP?

在当今的互联网环境中&#xff0c;分布式拒绝服务&#xff08;DDoS&#xff09;攻击已经成为一种常见的安全威胁。这种攻击通过向目标服务器发送大量的无效流量&#xff0c;使其无法处理正常的请求&#xff0c;从而达到迫使服务中断的目的。作为一个用户&#xff0c;你是否曾遇…

【Web】PhpBypassTrick相关例题wp

目录 ①[NSSCTF 2022 Spring Recruit]babyphp ②[鹤城杯 2021]Middle magic ③[WUSTCTF 2020]朴实无华 ④[SWPUCTF 2022 新生赛]funny_php 明天中期考&#xff0c;先整理些小知识点冷静一下 ①[NSSCTF 2022 Spring Recruit]babyphp payload: a[]1&b1[]1&b2[]2&…

Go 语言中的 Switch 语句详解

switch语句 使用switch语句来选择要执行的多个代码块中的一个。 在Go中的switch语句类似于C、C、Java、JavaScript和PHP中的switch语句。不同之处在于它只执行匹配的case&#xff0c;因此不需要使用break语句。 单一case的switch语法 switch 表达式 { case x:// 代码块 cas…

【深度学习】如何选择神经网络的超参数

1. 神经网络的超参数分类 神经网路中的超参数主要包括: 1. 学习率 η 2. 正则化参数 λ 3. 神经网络的层数 L 4. 每一个隐层中神经元的个数 j 5. 学习的回合数Epoch 6. 小批量数据 minibatch 的大小 7. 输出神经元的编码方式 8. 代价函数的选择 9. 权重初始化的方法 …

2023.11.24 海豚调度,postgres库使用

目录 海豚调度架构dolphinscheduler DAG(Directed Acyclic Graph)&#xff0c; 个人自用启动服务 DS的架构(海豚调度) 海豚调度架构dolphinscheduler 注:需要先开启zookeeper服务,才能进行以下操作 通过UI进行工作流的配置操作, 配置完成后, 将其提交执行, 此时执行请求会被…

【数据分享】我国12.5米分辨率的DEM地形数据(免费获取/地理坐标系)

DEM地形数据是我们在各种研究和设计中经常使用的数据&#xff01;之前我们分享过500米分辨率的DEM地形数据、90米分辨率的DEM地形数据、30米分辨率的DEM地形数据&#xff08;均可查看之前的文章获悉详情&#xff09;。 本次我们为大家带来的是分辨率为12.5m的DEM地形数据&#…

【反射】简述反射的构造方法,成员变量成员方法

&#x1f38a;专栏【JavaSE】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 &#x1f970;欢迎并且感谢大家指出我的问题 文章目录 &#x1f384;什么是反射&#x1f384;获取class对象的三种方式⭐代码实现 &#x1f3…

【黑马甄选离线数仓day04_维度域开发】

1. 维度主题表数据导出 1.1 PostgreSQL介绍 PostgreSQL 是一个功能强大的开源对象关系数据库系统&#xff0c;它使用和扩展了 SQL 语言&#xff0c;并结合了许多安全存储和扩展最复杂数据工作负载的功能。 官方网址&#xff1a;PostgreSQL: The worlds most advanced open s…

Java Web——XML

1. XML概述 XML是EXtensible Markup Language的缩写&#xff0c;翻译过来就是可扩展标记语言。XML是一种用于存储和传输数据的语言&#xff0c;它使用标签来标记数据&#xff0c;以便于计算机处理和我们人来阅读。 “可扩展”三个字表明XML可以根据需要进行扩展和定制。这意味…