文章目录
- 1. 协议
- 2. 网络版计算器简易实现代码链接
- 3. 网络版计算器
- 2-1. 约定的协议方案有两种
- 2-3. 协议代码框架
- 1. 自定义的协议方案
- 2. json(库里的完整协议方案)
- 2-4. send和recv单独使用不安全
- 2-5. 剩余代码写法讲解参考如下:
- 2-6. 代码运行结果示意图:
- 4. 守护进程
- 4-1. 守护进程概念
- 4-2. 守护代码方法
- 4-3. 守护进程后的相关示意图:
1. 协议
- 注意:我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
- 协议是一种 “约定”
socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么做呢?
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体。
2. 网络版计算器简易实现代码链接
- 完整代码我放到git仓库了哈~
- 链接:
https://gitee.com/ding-xushengyun/linux__cpp/tree/master/NetCal
3. 网络版计算器
2-1. 约定的协议方案有两种
约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 +
- 数字和运算符之间没有空格。
约定方案二(我们的代码实现采用方案二):
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”
注意:无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的. 这种约定, 就是应用层协议。
- 当然我们自己简单实现的协议比较简陋(不能应用于任何场景);库里的json更强大。
2-3. 协议代码框架
结果和计算过程都有序列和反序列化
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
using std::cout;
using std::endl;
using std::string;
namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LINE strlen(SPACE)
#define SEP "\r\n"
#define SEP_LINE strlen(SEP)
class Request // 计算数据
{
public:
// "_x _op _y"
string Serialize() // 序列化
{
#ifdef MYSELF
// 自己模拟的协议
#else
// json
#endif
}
// "111 + 222"
bool Deserialized(const string &str) // 反序列化
{
#ifdef MYSELF
// 自己模拟的协议
#else
// json
return true;
#endif
}
Request() {}
Request(int x, int y, int op) : _x(x), _y(y), _op(op)
{
}
public:
int _x;
int _y;
char _op; // '+' '-' '*' '/' '%'
};
class Response // 结果
{
public:
// _code _result
string Serialize() // 序列化
{
#ifdef MYSELF
// 自己模拟的协议
#else
// json
#endif
}
// 1 333
bool Deserialized(const string &str) // 反序列化
{
#ifdef MYSELF
// 自己模拟的协议
return true;
#else
// json
#endif
}
Response() {}
Response(int result, int code) : _result(result), _code(code)
{
}
public:
int _result; // 计算结果
int _code; // 计算结果的状态码
};
// 下面是接受和发送数据函数(recv, send)
bool Recv(int sock, string *out) // 接受信息/数据
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);
if (s > 0)
{
buffer[s] = 0;
*out += buffer;
}
else if (s == 0)
{
// cout << "我也要退出了" << endl;
return false;
}
else
{
// cout << "接受失败" << endl;
return false;
}
return true;
}
void Send(int sock, string &str) // 发送信息/数据
{
ssize_t s = send(sock, str.c_str(), str.size(), 0);
if (s < 0)
{
cout << "发送消息失败" << endl;
}
}
1. 自定义的协议方案
客户发过来的数据例如:2+1
序列化过程:("_x _op _y") 就是把值(类的成员函数)拿出来变成字符串
_x :左值;_op:运算符;_y:右值(例如:2 + 1)
"2+1" -> "2 + 1" // 字符个数从3变为5,我们以空格作为分割符。
string Serialize() // 序列化
{
string str;
str = std::to_string(_x);
str += SPACE;
str += _op;
str += SPACE;
str += std::to_string(_y);
return str;
}
反序列化过程:就是把字符串解析出来存起来(类的成员函数)
以空格作为分割符;解析出来客户发的数据。
bool Deserialized(const string &str) // 反序列化
{
size_t left = str.find(' ');
if (left == string::npos)
return false;
size_t right = str.rfind(' ');
if (right == string::npos)
return false;
_x = stoi(str.substr(0, left));
_y = stoi(str.substr(right + SPACE_LINE));
if (left + SPACE_LINE > str.size())
return false;
else
_op = str[left + SPACE_LINE];
return true;
}
结果序列化和反序列化(例如:3)
序列化过程:("_x _op _y") 就是把值(类的成员函数)拿出来变成字符串
code表示结果状态值,result:表示结果 (例如:0 3)
"03" -> "0 3" // 字符个数从2变为3,我们以空格作为分割符。
string Serialize() // 序列化
{
string str;
str = std::to_string(_code);
str += SPACE;
str += std::to_string(_result);
return str;
}
反序列化过程:就是把字符串解析出来存起来(类的成员函数)
以空格作为分割符;解析出来客户发的数据。
bool Deserialized(const string &str) // 反序列化
{
size_t left = str.find(' ');
if (left == string::npos)
return false;
_code = stoi(str.substr(0, left));
_result = stoi(str.substr(left + SPACE_LINE));
return true;
}
2. json(库里的完整协议方案)
下载json库指令; 我们下载后这个库就在系统路径下(我们使用这个库记住,编译的适合链接就可以了跟线程库一样。)
例如:
sudo yum install -y jsoncpp-devel
- json库使用方法固定用法哈~
客户发过来的数据例如:2+1
序列化过程:("_x _op _y") 就是把值(类的成员函数)拿出来变成字符串
_x :左值;_op:运算符;_y:右值(例如:2 + 1)
string Serialize() // 序列化
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter write;
return write.write(root);
}
反序列化过程:就是把字符串解析出来存起来(类的成员函数)
bool Deserialized(const string &str) // 反序列化
{
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asInt();
return true;
}
结果序列化和反序列化(例如:3)
序列化过程:("_x _op _y") 就是把值(类的成员函数)拿出来变成字符串
code表示结果状态值,result:表示结果 (例如:0 3)
string Serialize() // 序列化
{
Json::Value root;
root["code"] = _code;
root["result"] = _result;
Json::FastWriter write;
return write.write(root);
}
反序列化过程:就是把字符串解析出来存起来(类的成员函数)
bool Deserialized(const string &str) // 反序列化
{
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_code = root["code"].asInt();
_result = root["result"].asInt();
return true;
}
2-4. send和recv单独使用不安全
多路转接的时候,出现的发送的问题,我们暂时不考虑。
-
这里的“不安全” 是指send数据发了好多数据,而recv读取数据不完整那就造成解析失败进而结果不正确。
-
send/write:你是把数据发送到网络甚至是对方的主机中错误的!(你只是把数据发的缓存中,数据通过io拷贝出来滴!)
那么我们怎么保证自己读到的数据完整?通过加一些东西作为分割。
我们通过添加字符串长度和加"\r\n"(传统做法)
-
例如:length\r\nxxxxx\r\n
- 我们可以通过length来确定数据的准确新(xxxxx)
-
a. 解析先查找\r\n确定length长度,通过长度来确定这次读到数据是否>=一个完整的数据报。
-
b. 添加就是先加长度和分割符最后末尾加上分割符(“\r\n”)
// length\r\nxxxxx\r\n
string Decode(string &buffer) // 解析
{
int pos = buffer.find(SEP);
if (string::npos == pos) // 没找到
{
return "";
}
int size = stoi(buffer.substr(0, pos));
int length = buffer.size() - SEP_LINE * 2 - pos;
if (size < length) // 读取到的数据不完整
{
return "";
}
// 读取到一个完成数据报
buffer.erase(0, pos + SEP_LINE); // 去掉length\r\n
string s = buffer.substr(0, size); // 取出
buffer.erase(0, size + SEP_LINE); // 去掉xxxxx\r\n
// 解析数据完成
return s;
}
// xxxxx
void Encode(string &s) // 添加
{
string tmp = std::to_string(s.size()); // length
tmp += SEP; // length\r\n
tmp += s; // length\r\nxxxxx
tmp += SEP; // length\r\nxxxxx\r\n
swap(s, tmp);
}
}
2-5. 剩余代码写法讲解参考如下:
剩余写法详情参考这篇文章中的TCP网络简单实现:
链接:https://blog.csdn.net/Dingyuan0/article/details/129074597?spm=1001.2014.3001.5501
2-6. 代码运行结果示意图:
我们把结果输入到日志中,便于查看。
- 自定义协议运行结果:
- json库协议运行结果:
注意:我们把字符(运算符)转换为整数了。
4. 守护进程
用户退出后,服务器还在运行;如果我们的服务器也退出就不合理;就产生了守护进程。
4-1. 守护进程概念
- 1.前台进程是和终端关联的进程。
- 2.任何xshell登陆,只允许一个前台进程和多个后台进程。(例如:刚登录时bash是前台进程,我们运行我们的服务器后;bash不在是前台进程,我们的服务器是前台进程。)
- 3.进程除了有自己的pid,ppid,还有一个组ID。
- 4.在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash ->可以用匿名管道来进行通信。
- 5.而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程(组长不能成为守护进程)
- 6.任何一次登陆,登陆的用户,需要有多个进程(组)来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
- 7.如何将自己变成自成会话(就是守护进程)呢? 调用setsid()函数
- 8.setsid要成功被调用,必须保证当前进程不是进程组的组长;怎么保证呐?fork()一下让它子进程成为守护进程。
- 9.守护进程不能直接向显示器打印消息一旦打印,会被暂停、终止。
- 10.fork()之后此时守护进程也可称为特殊的孤儿进程(因为它的父母是一号进程。)
用户登录时系统创建会话,相关示意图:
4-2. 守护代码方法
代码写法如下。
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void MyDaemon()
{
// 1. 忽略信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2 . 不成为组长
if (fork() > 0)
exit(0);
// 3. 调用setsid
setsid();
// 3. cin cout ceer 重定向,守护进程不直接向显示器中打印信息
int devnull=open("/dev/null", O_RDONLY | O_WRONLY);
if(devnull>0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
4-3. 守护进程后的相关示意图:
无论我们怎样退出xshell重新登录,我们的服务器一直在运行。