应用层自定义协议与序列化
- 1、协议和序列化
- 2、Jsoncpp
- 3、使用Jsoncpp实现网络版计算器
- 4、进程间关系与守护进程
- 4.1、进程组
- 4.2、会话
- 4.3、作业控制
- 4.4、实现服务守护进程化
- 5、手写序列化和反序列化
1、协议和序列化
之前写UDP和TCP的时候,本质上来说也是一种协议,比如使用UDP实现一个英汉词典的功能,我们就规定了客户端只能发送英文单词,然后服务端会根据英文单词查找中文意思返回。再比如TCP实现的远程命令执行,我们就规定了客户端必须输入Linux的指令。
在网络通信中,我们都是通过字符串来发送接受数据的,那么如果现在我们想传递一些结构化的数据呢?
比如今天我在微信群里聊天,那么我发出一条消息,我需要将头像、昵称、消息发给服务器,然后再有服务器转发给群里的其他人。那么是一条一条的发送给服务器还是整合在一起发送给服务器呢?当然是把这三条整合在一起发送给服务器最好。头像其实是字符串,因为你的头像上传到服务器了,所以就是一个路径,昵称和消息也是字符串,所以我们可以把这三个数据打包成一个大字符串发送给服务器。然后服务器再转发给其他用户,其他用户将信息提取出来。而这些信息肯定也是要在一个结构体里面保存起来的,而客户端和服务器都认识这个结构体,这就是双方约定好的协议。
那么如果是一个结构体对象数据如何传递给对方呢?
那么我们能不能直接将上图定义的data数据直接以二进制方式发送给服务器呢?可以是可以,不过可能会有问题。比如我们服务端是C/C++写的,但是客户端并不一定也是用C/C++写的,还有客户端的机器可能是32/64位的,所以会有问题。
因此我们还有下面这种更好的方式:
如图,聊天信息比如:消息、时间、昵称,这些信息是在一个结构体里面的,我们将他们提取出来转换成一个大字符串,把信息由多变一,方便网络发送,这个过程就叫做序列化。服务端在接受的时候,获取这个大字符串,然后将每个信息提取出来,把信息由一变多,方便上层处理,这个过程就叫做反序列化。
TCP全双工+面向字节流:
之前在TCP服务端HandlerRequest那里read和write是不完善的。
1、为什么TCP支持全双工?
实际上我们创建套接字sockfd后,操作系统会给我们创建两个缓冲区:发送缓冲区和接收缓冲区。而我们调用系统调用write向网络发送数据本质上是将我们的数据拷贝到发送缓冲区中,然后再由操作系统将发送缓冲区的数据发送给对方的。对方read如果没有数据会阻塞住,当数据发送到对方的接收缓冲区后,read再把数据从接收缓冲区拷贝到上层。因此write和read向网络发送或接收数据,本质上都是在拷贝数据。而当主机A将数据写入发送缓冲区然后发送给主机B的接收缓冲区的时候,主机B也可以将数据写入自己的发送缓冲区,然后发送到主机A的接收缓冲区中,因此TCP是全双工的。所以当我们将数据拷贝到发送缓冲区后,就由操作系统自主决定什么时候发送了,所以TCP叫做传输控制协议,这里的控制就是由操作系统自主来控制的。
2、TCP是面向字节流的?
由于TCP发送数据是由操作系统自己控制的,如果今天主机B迟迟没有将接收缓冲区的数据读走,上层可能还在进行数据的处理来不及读走,现在主机B的接收缓冲区可能快满了,只剩下10个字节的空间了。现在主机A要发送20字节的数据,先将这20字节数据拷贝到发送缓冲区中,然后主机A识别主机B只剩下10字节的空间了,所以就只发送过去10字节的数据。那么很不巧,这时候主机B上层将缓冲区的数据全部读走了,但是最后的数据就无法被正确解析了,因为最后只有10个字节的数据,数据是不完整的。因此TCP需要应用层自己保证报文的完整性。
TCP面向字节流,也就是说数据发送是按字节来发送的,发送方可能发送很多次,接收方可能一次就将它全部读完。就比如自来水公司给你们家通了自来水,你想用多少就用多少,你可以用瓶子接,也可以用盆接,还可以用桶接。TCP将应用层传递的数据视为无结构的字节流,不维护消息之间的边界。
而UDP是面向数据报的,所以UDP是要保证数据完整的发送给对方的。
所以,操作系统内部可能会存在大量的报文来不及处理,这么多报文操作系统要不要管理起来呢?要,如何管理?——先描述,再组织。
操作系统中描述报文的结构体就是struct sk_buff。那么网络通信需要通过文件描述符来实现,所以必定要创建struct file对象,如果是普通文件对象,struct file中的private_data就为空,如果是网络通信,那么该指针指向struct socket对象,而struct socket对象里面有个struct sock* sk,它又指向一个struct sock对象,sock里面有两个队列,一个接收队列,一个写队列,它们都是struct sk_buff_head类型的。而这两个队列结构里面又有struct sk_buff*的指针next和prev,它们指向了一个一个的报文sk_buff。
今天我们要实现一个网络版本的计算器,那么就需要制定协议。
网络功能我们就使用TCP来实现。
约定方案一:
1、客户端发送一个形如:1+2的字符串。
2、这个字符串中有两个操作数,都是整数。
3、两个整数之间有一个运算符,可以是加减乘除中的任意一个。
4、数字和运算符之间没有空格。
那么服务端接收到客户端发送过来的数据就按约定好的进行解析读取,计算后返回给客户端结果。这本质上就是在制定协议。
约定方案二:
1、定义结构体来表示我们需要交互的信息。
2、发送数据时将这个结构体按照一个规则转换成字符串,接收到数据时再按相同的规则将字符串转回结构体。这个过程就叫做序列化和反序列化。
首先我们需要给网络计算器制定协议:协议就是双方约定好的结构化的数据。
那么我们定义两个结构体Request和Response。Request里面有两个整形变量和一个字符,将来就通过x oper y来计算结果返回给客户端。Response里面有result和code,result表示计算结果,而由于除0是有问题的,所以我们通过code来表示结果是否可信,比如0表示成功,1表示除0结果不可信。那么客户端服务器都约定好,将来客户端发送数据给服务器,那么服务器也能根据数据提取出Request对象,再将计算结果写入Response对象发送给客户端,客户端接收后也能获取Response对象然后获得计算结果。这就是在制定协议。
其次我们要选择序列化的方案:
1、自己做:我们可以自己来做序列化和反序列化的方案,比如我们约定将来客户端发送过来的数据都是"xopery",但是由于TCP是面向字节流的,因此用户层需要判断报文的完整性。我们在该数据前面加上数据长度和\r\n,在该数据后面再加上\r\n。也就是下面这个样子:
发送过来的数据为11+22,长度为5,所以前面加上5\r\n,后面加上\r\n。这样将来如果发送过来的是多个报文,我只要找到\r\n,然后前面就是有效载荷的长度,我们将前面长度提取出来,根据长度获取后面的数据即可。
2、使用工具:有xml && json && protobuf。
我们可以使用工具,我们选择jsoncpp。
如图,它会将数据转换成上图这种格式,左边这种是有\n的,右边是直接使用,间隔的。序列化后转换成这种格式的字符串后发送给对方,将来对方拿着这个数据再反序列化就可以获取数据。
2、Jsoncpp
Jsoncpp是一个用于处理 JSON 数据的 C++ 库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。 Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
特性:
1.简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
2.高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
3.全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
4.错误处理:在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
首先需要安装jsoncpp:
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
安装后/usr/include目录下就有个jsoncpp文件,里面的json就包含了需要引入的头文件,将来在使用的时候需要引入如:#include <jsoncpp/json/reader.h>,因为系统默认只能找到/usr/include。
序列化:
1、使用Json::Value的toStyledString方法:
优点:将Json::Value对象直接转换为格式化的JSON字符串。
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;return 0;
}
编译后报错,需要指明链接文件:-ljsoncpp
2、使用Json::StreamWriter:
优点:提供了更多的定制选项,如缩进、换行符等。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter的工厂std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}
3、使用Json::FastWriter:
优点:比StyledWriter更快,因为它不添加额外的空格和换行符。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::FastWriter writer1;Json::StyledWriter writer2;std::string s1 = writer1.write(root);std::string s2 = writer2.write(root);std::cout << s1 << std::endl;std::cout << s2 << std::endl;return 0;
}
反序列化:使用 Json::Reader
优点:提供详细的错误信息和位置,方便调试。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root1;root1["name"] = "joe";root1["sex"] = "男";Json::StreamWriterBuilder wbuilder;std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root1, &ss);std::cout << ss.str() << std::endl;std::string json_string = ss.str();Json::Reader reader;Json::Value root2;bool parsingSuccessful = reader.parse(json_string, root2);if (!parsingSuccessful){std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return 1;}std::string name = root2["name"].asString();std::string sex = root2["sex"].asString();std::cout << "name: " << name << std::endl;std::cout << "sex: " << sex << std::endl;return 0;
}
3、使用Jsoncpp实现网络版计算器
直接将之前写的TCP远程命令执行拿过来,去掉CommandExec.hpp,还需要添加Calculator.hpp和Protocol.hpp。
下面我们先实现Protocol.hpp,如下:
#pragma once#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>bool Encode(std::string& message)
{}bool Decode(std::string& package, std::string* content)
{}class Request
{
public:Request():_x(0),_y(0),_oper(0){}Request(int x, int y, char oper):_x(x),_y(y),_oper(oper){}bool Serialize(std::string& out_string){}bool Deserialize(std::string& in_string){}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}int X() const { return _x; }int Y() const { return _y; }char Oper() const { return _oper; }
private:int _x;int _y;char _oper;
};class Response
{
public:Response():_result(0),_code(0){}Response(int result, int code):_result(result),_code(code){}bool Serialize(std::string& out_string){}bool Deserialize(std::string& in_string){}int Result() const { return _result; }int Code() const { return _code; }void SetResult(int result) { _result = result; }void SetCode(int code) { _code = code; }
private:int _result; // 结果int _code; // 出错码,0,1,2,...
};
首先实现Request和Response序列化和反序列化。同时因为服务端可能接收到多个报文,因此我们需要加入一些信息来区分。我们就按上面的方式,在有效载荷前面加上head_length\r\n,在后面加上\r\n,这样将来服务端读取数据就可以将一个一个的报文拆分出来。同时读取的数据也可能没有一个报文的长度,那就需要继续读取直到至少有一个报文的长度才能处理。
class Request
{
public:Request():_x(0),_y(0),_oper(0){}Request(int x, int y, char oper):_x(x),_y(y),_oper(oper){}bool Serialize(std::string& out_string){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StreamWriterBuilder wbuilder;std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);out_string = ss.str();return true;}bool Deserialize(std::string& in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Filed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}int X() const { return _x; }int Y() const { return _y; }char Oper() const { return _oper; }
private:int _x;int _y;char _oper;
};class Response
{
public:Response():_result(0),_code(0){}Response(int result, int code):_result(result),_code(code){}bool Serialize(std::string& out_string){Json::Value root;root["result"] = _result;root["code"] = _code;Json::StreamWriterBuilder wbuilder;std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);out_string = ss.str();return true;}bool Deserialize(std::string& in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Filed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_result = root["result"].asInt();_code = root["code"].asInt();return true;}int Result() const { return _result; }int Code() const { return _code; }void SetResult(int result) { _result = result; }void SetCode(int code) { _code = code; }
private:int _result; // 结果int _code; // 出错码,0,1,2,...
};
Request和Response的序列化和反序列化很简单,跟上面介绍Jsoncpp的用法没什么区别。唯一需要注意的是这里的_oper是char类型的,通过root获取的时候也是需要asInt()来获取的,因为并没有asChar()这个接口。
紧接着实现Encode和Decode,Encode给序列化后的字符串前面和后面加上我们需要的信息。Decode表示从读取的字符串中拆分出一个报文,Decode需要考虑几种情况。
// {json} -> len\r\n{json}\r\n
bool Encode(std::string& message)
{if (message.size() == 0) return false;std::string package = std::to_string(message.size()) + Sep + message + Sep;message = package;return true;
}// len\r\n{json}\r\n
// 123\r\n{json}\r\n -> {json}
// 123\r\n
// 123\r\n{jso
// 123\r\n{json}\r
// 123\r\n{json}\r\n123\r\n{json}\r\n123\r\n{j
bool Decode(std::string& package, std::string* content)
{auto pos = package.find(Sep);if (pos == std::string::npos) return false;std::string content_length_str = package.substr(0, pos);int content_length = std::stoi(content_length_str);int full_length = content_length_str.size() + content_length + Sep.size() * 2;if (package.size() < full_length) return false;*content = package.substr(pos + Sep.size(), content_length);// package earsepackage.erase(0, full_length);return true;
}
Decode我们需要先找\r\n,如果找不到那就直接返回了。如果找到了就把前面的长度提取出来,先判断package有没有一个报文的长度,如果没有也直接返回,如果有再处理将有效载荷提取出来作为输出型参数带出,然后返回true。并且要把package移除掉一个报文的长度。
接着实现Calculator.hpp,实现计算:
#pragma once#include <iostream>
#include "Protocol.hpp"class Calculator
{
public:Calculator() {}~Calculator() {}Response Execute(const Request &req){Response resp;int x = req.X();int y = req.Y();switch (req.Oper()){case '+':resp.SetResult(x + y);break;case '-':resp.SetResult(x + y);break;case '*':resp.SetResult(x * y);break;case '/':{if (y == 0)resp.SetCode(1); // 1表示除0错误elseresp.SetResult(x / y);}break;case '%':{if (y == 0)resp.SetCode(2); // 2表示mod 0错误elseresp.SetResult(x % y);}break;default:resp.SetCode(3); // 3表示不支持该运算break;}return resp;}
};
Calculator类中的Execute实现计算,传入一个Request对象,然后计算出结果写入Response中返回。其中出错码0,1,2,3分别表示:成功、除0错误、模0错误、该运算类型无法识别。
紧接着在TcpServer.hpp中添加回调函数:
回调函数_handler我们在构造函数中传入初始化,然后在线程执行的HandlerRequest中,我们定义了一个string的package对象,每次将读取的数据添加到package中,然后调用回调函数。回调函数的功能就是对传入的package进行解析,如果有一个报文就拿下来,然后进行反序列化计算出结果,将结果序列化成字符串再返回。如果成功了就返回结果序列化后的字符串,如果失败就返回空串。所以进一步判断,如果message为空串,说明上层可能因为某些原因比如不足一个报文长度,失败了就continue,继续读取数据添加到package后面。成功就将message发送给客户端,然后客户端读取数据后再反序列化出结果,将结果输出。
接着在TcpServer.cc中实现回调函数:
using cal_t = std::function<Response(Request)>;class Parse
{
public:Parse(cal_t cal):_cal(cal){}std::string Entry(std::string& package){// 1.判断报文的完整性std::string content;if (!Decode(package, &content))return std::string();// 2.反序列化Request req;if (!req.Deserialize(content))return std::string();// 3.计算Response resp = _cal(req);// 4.序列化std::string res;resp.Serialize(res);// 5.添加报头Encode(res);return res;}
private:cal_t _cal;
};
但是上面一次就处理一个报文,package里面可能有多个报文,我们想将所有完整的报文都处理了,所以需要加一个循环。
using cal_t = std::function<Response(Request)>;class Parse
{
public:Parse(cal_t cal):_cal(cal){}std::string Entry(std::string& package){// 1.判断报文的完整性std::string content;std::string respstr;while (Decode(package, &content)){LOG(LogLevel::DEBUG) << "content:\n" << content; // 2.反序列化Request req;if (!req.Deserialize(content))break;// 3.计算Response resp = _cal(req);// 4.序列化std::string res;resp.Serialize(res);LOG(LogLevel::DEBUG) << "序列化:\n" << res;// 5.添加报头Encode(res);LOG(LogLevel::DEBUG) << "Encode:\n" << res;respstr += res;}return respstr;}
private:cal_t _cal;
};
循环处理完所有完整的报文,将所有序列化的结果拼接到respstr中,最后将处理的多个结果返回。然后服务端发送给客户端。
接着在服务端main函数中添加:
这样我们就实现了各个模块的解耦。网络模块只负责IO,然后HandlerRequest里面_handler回调解析模块中的Entry函数进行解析,然后在Entry中回调计算模块中的Execute进行计算并返回结算结果。
最后实现客户端:
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Protocol.hpp"// ./client_tcp serverip serverport
int main(int argc, char* argv[])
{if (argc != 3){std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]); int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "create socket failed" << std::endl;return 2;} struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = ::htons(serverport);server.sin_addr.s_addr = ::inet_addr(serverip.c_str());int n = ::connect(sockfd, (const struct sockaddr*)&server, sizeof(server));if (n < 0){std::cout << "connect failed" << std::endl;return 3;}std::string message;while (1){int x, y;char op;std::cout << "input x: ";std::cin >> x;std::cout << "input y: ";std::cin >> y;std::cout << "input oper: ";std::cin >> op;Request req(x, y, op);req.Serialize(message);Encode(message);n = ::send(sockfd, message.c_str(), message.size(), 0);if (n > 0){char buffer[4096];int m = ::recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (m > 0){buffer[m] = 0;std::string package = buffer; // 暂定读取一个完整报文std::string content; Decode(package, &content);Response resp;resp.Deserialize(content);std::cout << resp.Result() << "[" << resp.Code() << "]" << std::endl;}else break;}else break;}return 0;
}
客户端也是跟服务端一样需要判断报文的完整性,不过这里我们简单暂定接收到的就是一个完整的报文。
最后编译运行程序进行测试:
之前OSI七层模型中,最上面的三层:会话层、表示层、应用层,在TCP/IP模型中被压缩成了一层:应用层?
如图:
会话层负责建立和断开通信连接,这不就是我们网络模块中的accept获取新连接,然后线程去执行HanderRequest获取数据,当客户端退出了会读到0,线程也就退出HandlerRequest了。
表示层设备固有数据和网络标准数据格式转换,这不就是我们解析模块中将定义的协议Request和Response进行序列化变成网络传输的数据,再反序列化变成结构化数据。
应用层指针特定应用的协议,这不就是我们计算模块中,将结构化数据Request中的数据进行计算,然后构建Response返回。
而下面的:物理层、数据链路层、网络层、传输层,下面四层是统一的,下面都内置在操作系统中。而上面这三层是无法内置在操作系统中的,比如表示层进行序列化和反序列化可以选择不同的工具。应用层今天我们实现的计算器,将来也可能是其他业务。因此上面这三层压缩成一层应用层,将来是要我们自己实现的。
4、进程间关系与守护进程
4.1、进程组
右边我们使用sleep命令然后通过管道起了三个进程,左边查看进程信息。我们发现确实起了三个进程,这三个进程的父进程都是2160,而进程PID为2160的进程就是bash进程。所以这三个进程相当于是兄弟关系。再看它们的PGID,都是2188,这个PGID表示的是进程组ID,这三个进程都属于一个进程组,而它们的进程组ID为第一个进程的PID,第一个进程就是这个进程组的组长。
下面我们运行一个进程,该进程循环打印一条信息:
如图,这就是我们运行的进程,PID为2318,它的父进程就是bash进程,并且这个进程也有一个PGID,而且这个PGID就是它自己。所以无论是多进程还是单进程,他们都有进程组,哪怕只有一个进程,也会自成一个进程组。
下面修改代码,创建子进程让子进程循环打印信息,父进程则是一直休眠:
如图,父进程PID为2345,子进程PID为2346,子进程的PPID就是父进程。并且我们注意到父子进程同属一个进程组,进程组ID就是父进程的ID。这个进程组的组长就是第一个进程也就是父进程。
进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程。
进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。 注意:只要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
ps -eo pid,pgid,ppid,comm | grep myprocess
-e选项表示every的意思,表示输出每一个进程信息
-o选项以逗号操作符(,)作为定界符,可以指定要输出的列
4.2、会话
当我们使用xshell连接远程服务器,Linux会给我们创建一个终端文件,并创建一个bash进程,将bash进程和终端文件相关联。然后我们输入命令就是通过网络发送到终端文件中,然后bash从终端文件中读取执行再将结果写回终端文件,终端文件通过网络再发送给客户端。那么创建终端文件、创建bash进程,我们叫做构建了一个会话。
下面先验证终端文件:
查看/dev/pts下面的文件,发现有两个终端文件0和1,接着我们向0写入就是左边这个终端文件,向1写入就是右边这个终端文件。
我们再新建一个终端,发现该目录下又多了一个文件,我们向新增加的文件中写入,果然是向新的终端文件中写入。
下面验证bash:
我们先以后台的方式运行sleep,然后再运行myprocess。接着查看这些进程信息,选项-E可以将sleep和myprocess中匹配任意一个的显示出来。
上面的三个sleep他们是一个进程组,进程组ID就是组长ID。下面的两个myprocess他们是一个进程组。并且下面两个进程是前台进程,STAT带了+表示前台进程。
并且我们发现它们的PPID都是一样的,也就是bash。更重要的是,我们发现它们的SID都是一样的。SID就是session ID,也就是会话ID,而会话ID就是bash进程的ID。所以一般形成一个会话,这个会话的第一个进程就是bash,会话ID一般就是会话中的第一个进程ID,一般是bash进程。
这两个任务,其中sleep是一个进程组,myprocess是一个进程组,但是它们都属于同一个会话。所以一个会话里面有多个进程组。
在我们登录的时候,会形成一个会话。创建一个bash,而bash进程一定是一个进程组,bash是该会话的第一个进程,所以会话ID就是bash进程ID。所以我们应该要看到bash进程的PID、PGID、SID都是一样的。
会话ID在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
4.3、作业控制
当我们运行一个程序后,我们再输入命令bash就不会执行了,这是因为默认bash是前台进程,bash可以接收我们输入的命令,然后执行后将结果返回。而当我们运行程序后,myprocess就变成了前台进程,bash就变成了后台进程。而只有前台进程可以接收键盘输入,因此我们输入的命令都给了myprocess,而myprocess并不会读取也不会执行指令。那么当我们ctrl c杀掉myprocess后,bash进程就会自动从后台切成前台进程,继续获取我们输入的命令。
同一个会话中,可以同时存在多个进程组。但是任何时刻,只允许一个前台进程(组),可以运行有多个后台进程(组)。
当我们在后面加上&,表示以后台进程的方式运行,所以这时候bash进程还是前台进程,这时候我们输入的命令都给了bash,所以bahs可以解析执行。而我们ctrl c就杀不掉刚才起的后台进程了,因为这时候ctrl c是给bash进程的了。
并且我们注意到打印了一条消息,方框中的1表示的就是作业号,然后后面的5795就是最后一个进程sleep 3000的进程ID。
通过jobs可以查看所有后台作业:
如果想把作业放到前台可以使用:fg n,其中n表示作业号。
如果想把作业放到后台,需要先将作业暂停,然后再将作业运行起来:先ctrl z暂停作业,然后bg n运行作业,n表示作业号。
4.4、实现服务守护进程化
之前我们的网络版本计算器还不是一个真正意义上的服务,因为我们启动进程后就会卡住,因为该服务是前台进程,当我们退出登录该进程也就不存在了。
基于上面的知识,我们知道当我们登录Linux会创建一个会话,会话里有终端和bash进程,当我们运行网络计算器服务时,该进程是一个进程组,但是这个进程组只有一个进程,并且这个进程组是属于该会话的,我们假定这个进程组是进程组4。而当我们xshell退出登录后,就会清理会话、bash进程,那么我们启动的服务就可能受到影响。因此,我们要让我们起的服务进程,它所在的进程组单独成一个会话。也就是将进程组4单独拎出来形成独立的会话。那么此时进程组4所在会话和之前就由包含关系变成了并列关系,这时候我们退出了登录删除之前的会话,那么就和进程组4没有关系了。
这种独立会话,内部只存在一个进程,我们把这种进程称为守护进程。
那么如何创建会话呢,需要通过系统调用setsid来实现:
可以调用setseid函数来创建一个会话,前提是调用进程不能是一个进程组的组长。
但是我们启动服务,默认就是进程组的组长,那么该怎么办呢?
我们通常先调用fork()创建子进程,这时候子进程和父进程属于同一个进程组,但是进程组组长是父进程,父进程直接退出,子进程变成孤儿进程会被1号进程领养,所以子进程就可以调用setsid函数创建一个会话。
下面实现Daemon.hpp实现服务守护进程化:
#pragma once#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const char* root = "/";
const char* dev_null = "/dev/null";void Daemon(bool ischdir, bool isclose)
{// 1.屏蔽掉特定的信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2.称为非组长if (fork() > 0) exit(0);// 3.创建新会话setsid();// 4.每个进程都有自己的CWD,是否将当前进程CWD更改为根目录/if (ischdir)chdir(root);// 5.已经成守护进程了,不需要再和用户输入输出相关联了。if (isclose){close(0);close(1);close(2);}else{int fd = open(dev_null, O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
/dev/null相当于一个黑洞文件,你将数据写入到该文件中都会被丢弃,同时你无法从该文件中读取到数据。
下面在TcpServer.cc中引入Daemon.hpp并调用Daemon函数实现服务守护进程化:
我们将日志信息刷新到磁盘上,然后不更改当前工作目录,不关闭0、1、2文件,所以这三个文件就会被重定向到/dev/null黑洞文件中。
下面启动我们的服务:
如图,此时我们的服务进程PID、PGID、SID都是一样的,它是一个独立的会话,该会话中只有一个进程组,该进程组中只有它这一个进程。这时候我们退出登录,销毁的是bash进程所在会话,就不会影响到它了。
还有另外一种方式,直接使用系统调用:daemon
如果nochdir是0,该函数改变进程的当前工作目录为根目录,否则不改变。
如果noclose是0,该函数重定向标准输入、标准输出和标准错误到/dev/null,否则保留原有文件描述符。
5、手写序列化和反序列化
对于Request:我们将其序列化为:"x oper y"的格式,以空格间隔。反序列化就是从字符串中提取出x、oper、y。
对于Response:我们序列化为:"result code"的格式,以空格间隔。
#pragma once#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>#define SELF 1const std::string ProtSep = " ";
const std::string Sep = "\r\n";// {json} -> len\r\n{json}\r\n
bool Encode(std::string& message)
{if (message.size() == 0) return false;std::string package = std::to_string(message.size()) + Sep + message + Sep;message = package;return true;
}// len\r\n{json}\r\n
// 123\r\n{json}\r\n -> {json}
// 123\r\n
// 123\r\n{jso
// 123\r\n{json}\r
// 123\r\n{json}\r\n123\r\n{json}\r\n123\r\n{j
bool Decode(std::string& package, std::string* content)
{auto pos = package.find(Sep);if (pos == std::string::npos) return false;std::string content_length_str = package.substr(0, pos);int content_length = std::stoi(content_length_str);int full_length = content_length_str.size() + content_length + Sep.size() * 2;if (package.size() < full_length) return false;*content = package.substr(pos + Sep.size(), content_length);// package earsepackage.erase(0, full_length);return true;
}class Request
{
public:Request():_x(0),_y(0),_oper(0){}Request(int x, int y, char oper):_x(x),_y(y),_oper(oper){}bool Serialize(std::string& out_string){
#ifdef SELF// ->"x oper y" 以空格间隔std::string x_string = std::to_string(_x);std::string y_string = std::to_string(_y);out_string = x_string + ProtSep + _oper + ProtSep + y_string;return true;
#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StreamWriterBuilder wbuilder;std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);out_string = ss.str();return true;
#endif}bool Deserialize(std::string& in_string){
#ifdef SELF// "x oper y" ->提取出x、opear、yauto left = in_string.find(ProtSep);if (left == std::string::npos) return false;auto right = in_string.rfind(ProtSep);if (right == std::string::npos) return false;if (left + ProtSep.size() + 1 != right) return false;std::string x_string = in_string.substr(0, left);if (x_string.empty()) return false;std::string y_string = in_string.substr(right + ProtSep.size());if (y_string.empty()) return false;_x = std::stoi(x_string);_y = std::stoi(y_string);_oper = in_string[right-1];return true;
#elseJson::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Filed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;
#endif}void Print(){std::cout << _x << std::endl;std::cout << _y << std::endl;std::cout << _oper << std::endl;}int X() const { return _x; }int Y() const { return _y; }char Oper() const { return _oper; }
private:int _x;int _y;char _oper;
};class Response
{
public:Response():_result(0),_code(0){}Response(int result, int code):_result(result),_code(code){}bool Serialize(std::string& out_string){
#ifdef SELF// 结构化数据 -> "result code"std::string result_string = std::to_string(_result);std::string code_string = std::to_string(_code);out_string = result_string + ProtSep + code_string;return true;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::StreamWriterBuilder wbuilder;std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);out_string = ss.str();return true;
#endif}bool Deserialize(std::string& in_string){
#ifdef SELF// "result code" -> 结构化数据auto pos = in_string.find(ProtSep);if (pos == std::string::npos) return false;std::string result_string = in_string.substr(0, pos);if (result_string.empty()) return false;std::string code_string = in_string.substr(pos + ProtSep.size());if (code_string.empty()) return false;_result = std::stoi(result_string);_code = std::stoi(code_string);return true;
#elseJson::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Filed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_result = root["result"].asInt();_code = root["code"].asInt();return true;
#endif}int Result() const { return _result; }int Code() const { return _code; }void SetResult(int result) { _result = result; }void SetCode(int code) { _code = code; }
private:int _result; // 结果int _code; // 出错码,0,1,2,...
};