作者:@阿亮joy.
专栏:《学会Linux》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
网络通信中的协议是指在网络中进行数据传输时遵循的一些规则和标准,用于确保不同设备之间的通信能够顺利进行。协议的本质是软件,它最终是需要通过计算机语言(编码)的方式来表现出来,协议如何编写取决于我们的应用场景。
通行双方在进行网络通信时:
序列化和反序列化是计算机中常用的概念,用于在不同系统或网络之间传输数据或存储数据时进行格式转换。
序列化是指将对象或数据结构转换成字节流的过程,以便于在网络或存储设备上进行传输或存储。在序列化的过程中,会将对象或数据结构的属性或元素逐个转换成二进制格式,并将这些二进制数据组成一个连续的字节流,以便于传输或存储。
反序列化是指将序列化后的字节流转换成对象或数据结构的过程,以便于在程序中进行操作。在反序列化的过程中,会将字节流逐个读取,并将其转换成相应的对象属性或数据结构元素,以便于程序对其进行操作。
在网络通信中,客户端向服务器发送请求时,需要将请求对象序列化成字节流进行传输;服务器收到请求后,需要将接收到的字节流反序列化成请求对象进行处理。
注:序列化和反序列化可以让上层业务和网络传输进行一定程度的解耦。
网络版计算器要实现的功能:我们需要客户端把数据和操作符发给服务器,然后由服务器进行计算,最后再把结果返回给客户端。为了实现这样的网络版计算器,我们就需要进行协议定制。
#pragma once
#include
#include
#include
#include
#include
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
#define SIZE 1024
#define MYSELF
class Request
{
public:
Request() {}
Request(int x, int y, char op)
: _x(x)
, _y(y)
, _op(op)
{}
~Request() {}
std::string Serialize()
{
#ifdef MYSELF
std::string str = std::to_string(_x);
str += SPACE;
str += _op;
str += SPACE;
str += std::to_string(_y);
return str;
#else
#endif
}
bool Deserialize(std::string& str)
{
#ifdef MYSELF
size_t left = str.find(SPACE);
if(left == std::string::npos)
return false;
size_t right = str.rfind(SPACE);
if(right == std::string::npos)
return false;
if(left + SPACE_LEN >= str.size())
return false;
_x = atoi(str.substr(0, left).c_str());
_y = atoi(str.substr(right + SPACE_LEN).c_str());
_op = str[left + SPACE_LEN];
#else
#endif
}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response() {}
Response(int code, int ret, int x, int y, char op)
: _code(code)
, _ret(ret)
, _x(x)
, _y(y)
, _op(op)
{}
~Response() {}
std::string Serialize()
{
#ifdef MYSELF
std::string str = std::to_string(_code);
str += SPACE;
str += std::to_string(_ret);
return str;
#else
#endif
}
bool Deserialize(const std::string& str)
{
#ifdef MYSELF
size_t pos = str.find(SPACE);
if(pos == std::string::npos)
return false;
_code = atoi(str.substr(0, pos).c_str());
_ret = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
#else
#endif
}
public:
int _code;
int _ret;
int _x;
int _y;
char _op;
};
bool Recv(int sock, std::string* out)
{
char buffer[SIZE];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if(s > 0)
{
buffer[s] = '\0';
*out += buffer;
return true;
}
else if(s == 0)
{
std::cout << "Client Quit!" << std::endl;
return false;
}
else
{
std::cout << "Recv Error!" << std::endl;
return false;
}
}
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;
}
// 去除报头
std::string Decode(std::string& buffer)
{
size_t pos = buffer.find(SEP);
if(pos == std::string::npos)
return "";
int size = atoi(buffer.substr(0, pos).c_str());
int leftSize = buffer.size() - pos - 2 * SEP_LEN;
if(leftSize >= size)
{
// 至少有一个完整的报文
buffer.erase(0, pos + SEP_LEN);
std::string s = buffer.substr(0, size);
buffer.erase(0, size + SEP_LEN);
return s;
}
else // 没有完整的报文,不进行解析
return "";
}
// 添加报头
// 有效载荷长度\r\n有效载荷\r\n
std::string Encode(std::string& s)
{
std::string newPackage = std::to_string(s.size());
newPackage += SEP;
newPackage += s;
newPackage += SEP;
return newPackage;
}
功能说明:
\r\n
有效载荷\r\n
。对端收到报文后,需要先去除报头 Decode,然后才能进行反序列化。日志是计算机系统中的一种记录信息的机制,可以用来追踪系统运行的情况和出现问题时进行分析和调试。所以我们编写的网络版计算器也引入了之前写的日志组件。
#pragma once
#include
#include
#include
#include
#include
// 日志等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOGFILE "./Calculate.log"
const char* levelMap[] =
{
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level, const char* format, ...)
{
// 只有定义了DEBUG_SHOW,才会打印debug信息
// 利用命令行来定义即可,如-D DEBUG_SHOW
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; // 标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", levelMap[level], timestamp);
char logBuffer[1024]; // 自定义部分
va_list args; // va_list就是char*的别名
va_start(args, format); // va_start是宏函数,让args指向参数列表的第一个位置
// vprintf(format, args); // 以format形式向显示器上打印参数列表
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); // va_end将args弄成nullptr
FILE *fp = fopen(LOGFILE, "a");
// printf("%s%s\n", stdBuffer, logBuffer);
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
class Sock
{
private:
const static int backlog = 20;
public:
Sock() {}
// 返回值是创建的套接字
int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
logMessage(FATAL, "Create Socket Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "Create Socket Success! Socket:%d", sock);
return sock;
}
// 绑定端口号
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);
local.sin_addr.s_addr = inet_addr(ip.c_str());
if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "Bind Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(3);
}
}
// 将套接字设置为监听套接字
void Listen(int listenSock)
{
if (listen(listenSock, backlog) < 0)
{
logMessage(FATAL, "Listen Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "Init Server Success!");
}
// 接收链接,返回值是为该连接服务的套接字
// ip和port是输出型参数,返回客户端的ip和port
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(FATAL, "Accept Error! Errno:%d Strerror:%s", errno, strerror(errno));
return -1;
}
if (ip)
*ip = inet_ntoa(src.sin_addr);
if (port)
*port = ntohs(src.sin_port);
return serviceSock;
}
// 发起连接
bool Connet(int sock, const std::string &serverIP, const int16_t &serverPort)
{
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_pton(AF_INET, serverIP.c_str(), &server.sin_addr);
if (connect(sock, (struct sockaddr *)&server, sizeof server) == 0)
return true;
else
return false;
}
~Sock() {}
};
TcpServer.hpp
#pragma once
#include "Sock.hpp"
#include
#include
#include
#include
using func_t = std::function<void(int)>;
class TcpServer; // 类型声明
class ThreadData
{
public:
ThreadData(int sock, TcpServer* ptr)
: _sock(sock)
, _ptr(ptr)
{}
~ThreadData() {}
public:
int _sock;
TcpServer* _ptr;
};
class TcpServer
{
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);
}
~TcpServer()
{
if(_listenSock >= 0) close(_listenSock);
}
void BindService(func_t func)
{
_func.push_back(func);
}
void Start()
{
while(true)
{
std::string clientIP;
uint16_t clientPort;
int sock = _sock.Accept(_listenSock, &clientIP, &clientPort);
if(sock == -1) continue; // 获取连接失败
logMessage(NORMAL, "Create A New Link! Socket:%d", sock);
pthread_t tid;
ThreadData* td = new ThreadData(sock, this);
// 创建线程完成用户的请求
pthread_create(&tid, nullptr, ThreadRoutine, (void*)td);
}
}
void ExcuteService(int sock)
{
for(auto& f : _func)
{
f(sock); // 执行服务端绑定的每一个服务
}
}
private:
static void* ThreadRoutine(void* args)
{
pthread_detach(pthread_self()); // 线程分离
ThreadData* td = static_cast<ThreadData*>(args);
td->_ptr->ExcuteService(td->_sock);
close(td->_sock); // 服务完成后关闭文件描述符
delete td;
return nullptr;
}
private:
Sock _sock;
int _listenSock;
std::vector<func_t> _func;
// std::unordered_map _func;
};
CalServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include
#include
static void Usage(const std::string& proc)
{
std::cout << "\nUsage: " << proc << " Port" << std::endl;
}
static Response CalculatorHelper(const Request &req)
{
Response resp(0, 0, req._x, req._y, req._op);
switch (req._op)
{
case '+':
resp._ret = req._x + req._y;
break;
case '-':
resp._ret = req._x - req._y;
break;
case '*':
resp._ret = req._x * req._y;
break;
case '/':
if (req._y == 0)
resp._code = 1;
else
resp._ret = req._x / req._y;
break;
case '%':
if (req._y == 0)
resp._code = 2;
else
resp._ret = req._x % req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
void Calculator(int sock)
{
std::string inbuffer;
while(true)
{
bool ret = Recv(sock, &inbuffer);
if(!ret) break;
// 读取成功
std::string package = Decode(inbuffer);
if(package.empty()) continue;
// 保证该报文是一个完整的报文
logMessage(NORMAL, "%s", package.c_str());
Request req;
// 反序列化:字节流 -> 结构化
req.Deserialize(package);
// 业务逻辑
Response resp = CalculatorHelper(req);
// 序列化
std::string respStr = resp.Serialize();
// 添加报头,形成一个完整的报文
respStr = Encode(respStr);
Send(sock, respStr); // 将处理结果返回给客户端
}
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
signal(SIGPIPE, SIG_IGN);
std::unique_ptr<TcpServer> ptr(new TcpServer(atoi(argv[1])));
ptr->BindService(Calculator); // 绑定服务
ptr->Start(); // 开始服务
return 0;
}
为什么服务端通常需要忽略 SIGPIPE 和 SIGCHLD 信号呢?
#include "Protocol.hpp"
#include "Sock.hpp"
#include
#include
static void Usage(const std::string& proc)
{
std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string ServerIP = argv[1];
uint16_t ServerPort = atoi(argv[2]);
Sock sock;
int sockfd = sock.Socket(); // 创建套接字
// 发起连接请求
if(!sock.Connet(sockfd, ServerIP, ServerPort))
{
std::cerr << "Connet Error!" << std::endl;
exit(2);
}
bool quit = false; // false表示不退出循环
std::string buffer;
srand((unsigned int)time(nullptr));
const char* op = "+-*/%";
while(!quit)
{
// 获取需求(此部分可以设置为手动输入)
Request req;
req._x = rand() % 100;
req._y = rand() % 100;
req._op = op[rand() % 5];
// 序列化
std::string str = req.Serialize();
std::string tmp = str;
// 添加报头
str = Encode(str);
// 向服务器发起请求
Send(sockfd, str);
// 接收服务器的应答
while(true)
{
bool ret = Recv(sockfd, &buffer);
// 服务器关闭连接或Recv异常
if(!ret)
{
quit = true;
break;
}
std::string package = Decode(buffer);
if(package.empty()) continue;
// 接收到一个完整的报文
Response resp;
resp.Deserialize(package);
std::string err;
switch(resp._code)
{
case 1:
err = "除0错误";
break;
case 2:
err = "模0错误";
break;
case 3:
err = "非法操作";
break;
default:
std::cout << "[Calculate Success] " << tmp << " = " << resp._ret << std::endl;
// std::cout << "[Calculate Success] " << resp._ret << std::endl;
// std::cout << "[Calculate Success] " << resp._x << " " << resp._op << " " << resp._y << " = " << resp._ret << std::endl;
break;
}
if(!err.empty()) std::cerr << err << std::endl;
sleep(1);
break;
}
}
close(sockfd);
return 0;
}
什么是前台进程?什么是后台进程?
在 Linux 系统中,一个进程可以在前台运行或者在后台运行。
前台进程是指用户当前正在与之交互的进程,通常在终端(Terminal)上显示进程的输出信息,同时接收用户输入的命令。当一个进程在前台运行时,它会阻塞终端(bash)的输入,直到该进程退出或者被暂停。前台进程可以使用 Ctrl+Z 将前台进程暂停,可以使用 Ctrl + C 将前台进程终止。
后台进程是指在后台运行的进程,不与终端交互,通常不会在终端上输出信息。后台进程可以继续运行,即使用户退出了终端。可以通过在命令行末尾添加“&”符号将进程放到后台运行。
任何一个 Xshell 登录,只允许一个前台进程和多个后台进程。
一个进程除了有 PID、PPID(父进程 ID),还有一个组 ID(PGID)。每个进程都属于一个进程组。进程组是一组具有相同进程组 ID(PGID)的进程的集合,同时被创建的多个进程可以成为一个进程组,第一个进程的进程 ID 成为进程组 ID。一个进程可以将它的子进程加入到同一个进程组中,从而使得这些进程可以共享同一个终端。
在 Linux 系统中,同一个父进程下的多个子进程称为兄弟进程。那么上面的 sleep 1000、sleep 2000 和 sleep 3000 就是兄弟进程,它们的父进程就是 bash。
什么是会话?
在 Linux 中,会话(session)是指从用户登录开始,到用户退出结束这段时间内的整个过程。通常情况下,一个会话包含多个进程,这些进程可以是由当前会话的 bash 启动的,也可以是由其他进程启动的。当用户退出登录时,会话会被终止。具体来说,这意味着所有与该会话相关的进程都将被终止,包括终端和 bash 进程。当用户使用 exit 命令或输入 Ctrl+D 组合键退出 bash 时,会话会被终止。在会话终止之前,系统会执行一些清理工作,例如向所有已连接的进程发送 SIGHUP 信号,以通知它们会话已经终止。注:bash 是自成一个进程组的!
终端和 bash 的关系
什么是守护进程?
守护进程(Daemon)是一种在后台运行的进程,通常在启动系统时自动启动,一直运行直到系统关闭。守护进程通常不会与用户交互,也不会直接响应用户请求,而是通过监听网络端口或定期执行任务等方式,提供某种服务或功能。守护进程常常用于网络服务、系统监控、定时任务等方面。
在 Linux 系统中,守护进程通常通过 fork 函数创建子进程的方式启动,并且需要调用 setsid 函数创建新会话(Session)和进程组(Process Group),以便于与终端(Terminal)分离,避免受到用户登录或注销的影响。此外,守护进程还需要关闭不需要的文件描述符(File Descriptor)、改变工作目录(Working Directory)等操作,以提高系统的安全性和稳定性。
注:setsid 要调用成功,必须保证当前进程不是进程组的组长。守护进程是自成一个会话的!
#pragma once
#include
#include
#include
#include
#include
#include
void MyDaemon()
{
// 1.忽略信号:SIGPIPE,SIGCHLD
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2.不要让自己成为进程组组长
if(fork() > 0) exit(0);
// 3.调用setsid
setsid();
// 4.标准输入、输出、错误的重定向
int devnull = open("/dev/null", O_RDONLY | O_RDONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
守护进程通常不应该向显示器输出信息,因为守护进程运行在后台,没有终端(Terminal)或标准输入输出(stdin/stdout/stderr)设备,也没有交互界面。如果守护进程尝试向显示器输出信息,可能会导致进程暂停或终止。
/dev/null 文件的介绍
/dev/null 是 Linux 操作系统中的一种特殊文件,它通常用于丢弃不需要的输出或输入数据。在 Linux 操作系统中,一切皆文件,/dev/null 也被看做是一个文件,但是它并不会存储数据,而是会将一切写入它的操作视为成功,并不做任何操作。
守护进程和孤儿进程的区别
守护进程是孤儿进程的一种,它们的父进程都是 1 号进程,最主要的区别就是守护进程自成一个会话,而孤儿进程是属于某个会话的。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于数据的序列化和跨语言数据交换。JSON 采用键值对的方式来组织数据,数据格式为键值对之间用逗号分隔,键和值之间用冒号分隔,整个数据由一对大括号包含。
例如,以下是一个 JSON 格式的数据:
{
"name": "Alice",
"age": 20,
"gender": "female",
"interests": ["reading", "music", "travel"]
}
这个数据包含了一个人的姓名、年龄、性别和兴趣爱好。其中,键名是字符串类型,键值可以是字符串、数字、布尔值、数组或对象。
JSON的优点是:
JSON 库的安装
sudo yum install -y jsoncpp-devel
JSON 库的简单使用
#include
#include
#include
int main()
{
int a = 10;
int b = 20;
char op = '+';
Json::Value root;
root["aa"] = a;
root["bb"] = b;
root["op"] = op;
Json::Value sub;
sub["other"] = 200;
sub["other1"] = "hello";
root["sub"] = sub;
Json::StyledWriter writer;
// Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
注意:编译时需要加上 -ljosncpp,否则无法找到库。
#pragma once
#include
#include
#include
#include
#include
#include
class Request
{
public:
Request() {}
Request(int x, int y, char op)
: _x(x)
, _y(y)
, _op(op)
{}
~Request() {}
std::string Serialize()
{
#ifndef MYSELF
std::string 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["op"] = _op;
root["y"] = _y;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool Deserialize(std::string& str)
{
#ifndef MYSELF
size_t left = str.find(SPACE);
if(left == std::string::npos)
return false;
size_t right = str.rfind(SPACE);
if(right == std::string::npos)
return false;
if(left + SPACE_LEN >= str.size())
return false;
_x = atoi(str.substr(0, left).c_str());
_y = atoi(str.substr(right + SPACE_LEN).c_str());
_op = str[left + SPACE_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_x = root["x"].asInt();
_op = root["op"].asInt();
_y = root["y"].asInt();
return true;
#endif
}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response() {}
Response(int code, int ret, int x, int y, char op)
: _code(code)
, _ret(ret)
, _x(x)
, _y(y)
, _op(op)
{}
~Response() {}
std::string Serialize()
{
#ifndef MYSELF
std::string str = std::to_string(_code);
str += SPACE;
str += std::to_string(_ret);
return str;
#else
Json::Value root;
root["code"] = _code;
root["ret"] = _ret;
root["x"] = _x;
root["op"] = _op;
root["y"] = _y;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool Deserialize(const std::string& str)
{
#ifndef MYSELF
size_t pos = str.find(SPACE);
if(pos == std::string::npos)
return false;
_code = atoi(str.substr(0, pos).c_str());
_ret = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
#else
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_code = root["code"].asInt();
_ret = root["ret"].asInt();
_x = root["x"].asInt();
_op = root["op"].asInt();
_y = root["y"].asInt();
return true;
#endif
}
public:
int _code;
int _ret;
int _x;
int _y;
char _op;
};
本篇博客主要讲解了协议的概念、序列化和反序列化、守护进程以及网络版计算器的编写等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!❣️