目录
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"的字符串
- 这个字符串中有两个操作数,都是整形
- 两个数字之间会有一个字符是运算符
- 运算符只能是加减乘除和取模
- 数字和运算符之间没有空格
约定方案二:
- 定义结构体来表示我们需要交互的信息
- 发送数据时将这个结构体按照一个规则转换成字符串
- 接收到数据的时候再按照相同的规则把字符串转化回结构体
这个过程叫做 "序列化" 和 "反序列化
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(×tamp); // 转化麻烦就不写了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协议(方法+报头+状态码)。