电商类网站,wordpress导航栏设置,浏览网站时弹出的广告是谁给做的,工程建设管理网站源码目录
1. 重新理解协议
2. 网络版本计算器
2.1 前期封装
Log.hpp
sock.hpp
TcpServer.hpp
第一次测试(链接)
2.2 计算器实现
第二次测试(序列化和反序列化)
第三次测试(客户端字节流)
CalServer.cc
CalClient.cc
3. 守护进程
3.1 守护进程和前后台进程
3.1 变成…目录
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. 网络版本计算器
例如, 我们需要实现一个服务器版的计算器我们需要客户端把要计算的两个数发过去然后由服务器进行计算最后再把结果返回给客户端。
这里通过实现一个网络版的计算器来讲解具体的用户协议定制以及序列化和反序列化的过程其中用户向服务器发起计算请求服务器计算完成后将结果响应给用户。协议是一种约定。看看方案 约定方案一 客户端发送一个形如11的字符串这个字符串中有两个操作数都是整形两个数字之间会有一个字符是运算符运算符只能是加减乘除和取模数字和运算符之间没有空格 约定方案二 定义结构体来表示我们需要交互的信息发送数据时将这个结构体按照一个规则转换成字符串接收到数据的时候再按照相同的规则把字符串转化回结构体 这个过程叫做 序列化 和 反序列化 无论我们采用方案一还是方案二还是其他的方案只要保证 一端发送时构造的数据在另一端能够正确的进行解析就是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); // 相当于apnullptrprintf(%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.hppclass 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.hnamespace ns_tcpserver
{using func_t std::functionvoid(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_castThreadData *(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::vectorfunc_t _func;};
} 第一次测试(链接) Makefile .PHONY:all
all:client CalServerclient:CalClient.ccg -o $ $^ -stdc11
CalServer:CalServer.ccg -o $ $^ -stdc11 -lpthread.PHONY:clean
clean:rm -f client CalServer CalServer.cc #include TcpServer.hpp
#include memoryusing 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_ptrTcpServer server(new TcpServer(atoi(argv[1]))); // 网络功能server-BindService(Debug); // 绑定一个服务方法网络功能和服务进行了解耦server-Start();return 0;
} CalClient.cc #include iostreamint main(int argc, char *argv[])
{return 0;
}
编译运行 成功运行客户端什么也没做链接一建立就自动退出了。 2.2 计算器实现
先把我们约定的协议Protocol封装成一个文件
Protocol.hpp先写一个框架
#pragma once#include iostream
#include string
#include cstringnamespace 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 5678bool 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_ptrTcpServer 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\n123789\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\nstd::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\nstd::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 memoryusing 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_ptrTcpServer 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.hppusing 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(操作系统)PIDPGID以及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 : opexitcode : exitcode
result : result就是将不同类型的变量和一个字符串绑定起来形成键值对序列化的时候将多个字符串拼接在一起形成一个字符串。反序列化的时候再将多个字符串拆开根据键值对的对应关系找到绑定的变量。
#include iostream
#include string
#include jsoncpp/json/json.hint 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 $ $^ -stdc11 -ljsoncpp
CalServer:CalServer.ccg -o $ $^ -stdc11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f client CalServer
Protocol.hpp
#pragma once#include iostream
#include string
#include cstring
#include jsoncpp/json/json.hnamespace 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 5678bool 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 _resultstd::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 0bool 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\n123789\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\nstd::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\nstd::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协议(方法报头状态码)。