目录
TCP协议通讯流程
三次握手的过程
四次挥手的过程
TCP 和 UDP 对比
应用层
"协议"
网络版计算器
认识TCP
守护进程
铺垫讲解
守护进程讲解
setsid
总结
TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
- TCP是面向连接的通讯协议,在通讯之前,需要进行3次握手,来进行连接的建立。
- 当TCP在断开连接的时候,需要断开连接,4次挥手。
下面我们结合TCP协议的通信流程,来初步认识一下三次握手和四次挥手,以及建立连接和断开连接与各个网络接口之间的对应关系。
三次握手的过程
TCP协议通讯是cs模型的,作为双方的连接,总是需要其中有一个主动的,而在日常的使用中,也不可能出现抖音由服务器唤醒客户端运行,往往都是客户端进行主动的申请,然后通过三次握手的方式建立连接。
理解:
当TCP要进行通讯的时候,我们需要进行三次握手来进行建立好连接。
- 要能够被连接:服务器必须处于listen状态。
- 发起三次握手:是谁掉connect,谁就发起。
- 三次握手细节:用户不用关心,是操作系统自行完成的。
四次挥手的过程
双方已经建立连接成功。那么,断开连接就是双方的事情,是需要从双方两个方向上都要进行断开连接。
TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报
应用层
前面的,无论是UDP套接字还是TCP套接字,这些接口全都是传输层经过对TCP,UDP的数据发送能力的包装,以文件的形式进行呈现给我们,以此让我们可以进行应用层编程。所以前面所使用的TCP,UDP套接字开发,全部叫做应用层开发。
"协议"
协议是一种 "约定"。前面所使写的简易的模型,都是按 "字符串" 的方式来发送接收的,但是如果我们要传输一些 "结构化的数据" 呢?
网络版计算器
设计一个加减乘除都能做的网络版计算器。例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。
- 客户端发送一个形如"1+1"的字符串
- 这个字符串中有两个操作数, 都是整形
- 两个数字之间会有一个字符是运算符
- 数字和运算符之间没有空格
- …….
约定方案二:
- 定义结构体来表示我们需要交互的信息。
- 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体。
- 这个过程叫做 "序列化" 和 "反序列化"
// proto.h 定义通信的结构体
typedef struct Request {
int a; // 操作数
int b; // 操作数
int op; //操作符
} Request;
typedef struct Response {
int result; // 计算的结果
int code; // 运算有没有出错 - 没有->0,有->原因
} Response;
// client.c 客户端核心代码
Request request;
Response response;
scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));
// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response));
这两种方案,下面的方案更好,因为其并不是传过去一个结构化的数据 / 对象。虽然我们可以通过使用二进制方案的传递方式进行传递,但是不同的环境下是具有不同的处理方式的,如:大小端的不同,内存对齐的不同。就会造成二者认为的结构体的不同,所以如果使用二进制进行发送会出现各种各样的数据问题 —— 一般并不会将结构体对象直接发送向对方。
而是首先将这个结构化的数据,转成一种序列化的数据。相当于转化成为一个字符串,本质上称作为:字节流。
可以理解为,将上述的struct Request结构体中的a、b、ob字段,想办法将三个字段转换为一个大的字符串,然后以字节流的方式推送给对方,让对方进行读取并解析。
- 序列化:是将对象(结构化的数据 / 对象)的状态信息转换为可以存储或传输的形式(字节序列)的过程。
- 反序列化:是把字节序列恢复为对象(结构化的数据 / 对象)的过程。
当我们能够做到,序列化与反序列化之后,所谓的协议定制本质上就是:
- 定义我们请求以及未来的响应当中的字段都需要哪些 —— 字段本身就是协议的一部分。
实现一个,基于序列化与反序列化方式的,基于TCP套接字的,一个自定义应用层协议的一个定制工作。
我们通过自身所写的序列化与反序列化,来实现网络计算器的网络沟通实现(代码式有问题的,我们自身所写的序列化与反序列化总是会有问题的,但是可以增强理解)
log.hpp
日志。
#pragma once
#include <cstdarg>
#include <ctime>
#include <cstdio>
#include <unistd.h>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2 // 警告 -- 没出错
#define ERROR 3 // 错误 -- 不影响后续执行(一个功能因为条件等,没有执行)
#define FATAL 4 // 致命 -- 代码无法继续向后执行
const char* gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL",
};
#define LOGFILE "./threafpool.log"
// 完整的日志功能,至少:日志等级 时间 日志内容 支持用户自定义
void logMessage(int level, const char* format, ...)// level:日志等级; format, ...:用户传参、日志对应的信息等。
{
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// 向缓冲区logBuffer中打印
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// 向屏幕
printf("%s%s\n", stdBuffer, logBuffer);
}
Sock.hpp
网络套接字使用的封装。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Sock
{
private:
const static int gbacklog = 20; // 一般不能太大也不能太小
public:
Sock(){}
int Socket()
{
// 创建socket
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "create socker error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", listensock); // 验证其是3
return listensock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
// bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
//local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
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, "create server success");
}
// 一般而言
// const std::string &:输入型参数
// std::string *:输出型参数
// std::string &:输入输出型参数
int Accept(int listensock, std::string *ip, uint16_t *port) // 这样既拿出来了新获得的套接字,又将客户端的ip和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;
else return false;
}
~Sock() {}
private:
int _port;
std::string _ip;
int _listensock;
};
Protocol.hpp
自己实现的序列化与反序列化,协议约定(服务器与客户端两方必须完全遵守)。
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
// 客户端于服务器共同约定的协议(需要相互遵守)
namespace cr_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
struct Request // 定好的协议
{
// 1. 自主实现 "length\r\nx_ op_ y_\r\n"
// 2. 使用现成的方案
std::string Serialize() // 序列化
{
#ifdef MYSELF
// "1234 + 5678"
std::string str;
str = std::to_string(_x);
str += SPACE;
str += _op;
str += SPACE;
str += std::to_string(_y);
return str;
#else
std::cout << "……" << std::endl;
#endif
}
bool Deserialized(const std::string &str) // 反序列化
{
#ifdef MYSELF
// "1234 + 5678"
std::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
std::cout << "……" << std::endl;
#endif
}
Request()
{}
Request(int x, int y, char op) : _x(x), _y(y), _op(op)
{}
~Request()
{}
int _x;
int _y;
char _op; // '+' '-' '*' '/' '%'
};
struct Response
{
std::string Serialize() // 序列化
{
#ifdef MYSELF
std::string s;
s = std::to_string(_code);
s += SPACE;
s += std::to_string(_result);
return s;
#else
std::cout << "……" << std::endl;
#endif
}
bool Deserialized(const std::string &str) // 反序列化
{
#ifdef MYSELF
std::size_t pos = str.find(SPACE);
if (pos == std::string::npos)
return false;
_code = atoi(str.substr(0, pos).c_str());
_result = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
#else
std::cout << "……" << std::endl;
#endif
}
Response(){}
Response(int result, int code) : _result(result), _code(code)
{}
~Response()
{}
int _result; // 计算结果
int _code; // 计算结果的状态码(出现运算符不存在,/0,%0等)
};
// 临时方案 - 接收
std::string Recv(int sock)
{
char inbuffer[1024];
ssize_t s = recv(sock, inbuffer, sizeof(inbuffer), 0);
if(s > 0)
return inbuffer;
return " ";
}
// 临时方案 - 发送
void Send(int sock, const std::string str)
{
send(sock, str.c_str(), str.size(), 0);
}
}
CalServer.hpp
#pragma once
#include "Sock.hpp"
#include "log.hpp"
#include <functional>
#include <string>
#include <vector>
#include <pthread.h>
namespace cr_tcpserver
{
using func_t = std::function<void(int)>;
class TcpServer;
class PthreadData
{
public:
PthreadData(int sock, TcpServer* server) :_sock(sock), _server(server)
{}
~PthreadData()
{}
public:
int _sock;
TcpServer* _server;
};
class TcpServer
{
private:
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
PthreadData *td = static_cast<PthreadData *> (args);
td->_server->Excute(td->_sock);
close(td->_sock);
return nullptr;
}
public:
// 一般而言
// const std::string &:输入型参数
// std::string *:输出型参数
// std::string &:输入输出型参数
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 Srart()
{
while (true)
{
std::string clinentip;
uint16_t clinentport;
int sock = _sock.Accept(_listensock, &clinentip, &clinentport);
if (sock == -1)
continue;
logMessage(NORMAL, "create neww link success, sock: %d", sock);
pthread_t tid;
PthreadData * td = new PthreadData(sock, this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
~TcpServer()
{
if (_listensock >= 0)
close(_listensock);
}
private:
int _listensock;
Sock _sock;
std::vector<func_t> _func;
// std::unordered_map<std::string, func_t> func_; // 为每一个服务起名字
};
}
CalServer.cc
服务器。
#include "CalServer.hpp"
#include "Protocol.hpp"
#include <memory>
using namespace cr_tcpserver;
using namespace cr_protocol;
void Usage(const std::string &process)
{
std::cout << "\nUasge: " << process << " port\n"
<< std::endl;
}
// 进行计算
Response calculatorHelper(const Request &req)
{
Response resp(0, 0);
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; // 设置错误码
else
resp._result = req._x / req._y;
break;
case '%':
if (0 == req._y)
resp._code = 2; // 设置错误码
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3; // 设置错误码
break;
}
return resp;
}
void calculator(int sock)
{
while (true)
{
std::string res = Recv(sock); // 在这里我们读到了一个请求
Request req;
req.Deserialized(res); // 反序列化
Response resp = calculatorHelper(req);
std::string respString = resp.Serialize(); // 序列化
Send(sock, respString);
}
}
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->Srart();
// // 测试序列化
// Request req(123, 456, '+');
// std::string s = req.Serialize();
// std::cout << s << std::endl;
// // 测试反序列化
// 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
客户端
#include <iostream>
#include <time.h>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace cr_protocol;
void Usage(const std::string &process)
{
std::cout << "\nUasge: " << process << " serverIp serverPort\n"
<< std::endl;
}
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);
}
while(true)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req._x >> req._op >> req._y;
// 2. 序列化
std::string s = req.Serialize();
// 3. 发送给服务端
Send(sockfd, s);
// 4. 正常读取
std::string r = Recv(sockfd);
// 5. 获取答案
Response resp;
// 6. 序列化
resp.Deserialized(r);
std::cout << "code: " << resp._code << std::endl;
std::cout << "result: " << resp._result << std::endl;
sleep(1);
}
return 0;
}
总结
看起来我们像做了一个序列化和反序列化,但是其实有很多问题:
- 我们将客户端提出后,服务器并不是仅仅将分配给客户端的线程销毁,还将自身也给销毁了。
是因为此处在哭护短关闭后,读到的为空,但是我们的结构体创建是有对应的空间的,只不过没有进行初始化,所以逻辑上也可以执行,但是主要问题就是:Send上,当send的时候可能文件描述符已经关了,这个时候send就是向非法文件进行写入,于是服务器直接被终止。
此处我们可以通过信号进行验证,因为如果正在对服务器进行写入的时候,突然读取端关闭了,再写入就会出现写入异常,或者关闭之后我们才进行写入,那么就会出现问题。
在CalServer.cc中捕捉SIGPIPE信号。
这就是写服务器的时候,最最最常见的一个问题。对应的解决方法有量两种:
- 对于SIGPIPE信号进行忽略。
- 对读取的结果是否成功进行判断,只有成功并且符合要求才会进行对应的处理。
并增加调试信息,便于观察。
#问:这个代码就没有问题了吗?
不是的,这个代码还有问题。我们写的这个代码能够保证其在读取的时候,如果读取失败了,我们在代码逻辑上解决了不会向一个已经关闭的文件中写入了,但是如果是正在写入的时候将文件关了呢?
这个问题最好的解决方式就是上述的对于SIGPIPE信号进行忽略。
融会贯通的理解:
协议:我们可以发送结构化的数据,直接进行网络通讯,这样是可以的。但是这种方法的可扩展性、可维护性实在是太差。一般是在应用层将这样的结构化的数据直接转换为字符串(准确的说是字节流,说支付串便于理解),然后发送给对方,对方在基于所收到的字符串(字节流),然后对其做反序列化的转化拿到结果。
所以任何一个服务器和客户端都有其对应的发送与接收,即:需要对应约定的序列化和反序列化。
上面的代码看似是完美执行实现的,但是上述的做法是不对的,因为其缺了一个重要的规则。
#问:TCP是面向字节流的 -> recv如何保证,读到的inbuffer,是一个完整完善的请求呢?
正是因为其是TCP,不是UDP,UDP是发一个数据服务端就接收一个数据。而TCP是可能在一段时间内接收到大量的数据,很可能接收一次,其中就包含着多次的请求。如此上述我们所写的序列化和反序列化就会出现问题。
- UDP面向数据报。
- TCP面向字节流。
认识TCP
论点的提出:
论点的解释:
总结:
- IO接口:本质都是拷贝函数。
- 字节流:发送的次数和接收的次数,没有任何关系。
其很可能是在发送缓冲区中堆积数据,然后才发送到接收缓冲区中。所以我们必须需要保证读取到的是完整的数据,而单纯的recv是不能够保证的,我们需要对协议进行进一步的定制。
- 在协议当中,必须保证让别人能够知道,我们的一个完整的请求报文的长度是多少。
报文:"_x _op _y" -> "length\r\n_x _op _y\r\n"
保证读取到完整的长度: 因为我们为了更好的处理已经加上了特殊的符号"\r\n"(建议使用->可读性好),并且能够保证长度中不会出现特殊的符号"\r\n"。("\r\n"不参与具体运算,只是作为特殊的符号使用)
于是,便可以只将完整数据进行处理。(此处在前面的代码稍作小更改,就不展示了,最后结合守护进程展示)
UDP会自行的对数据进行处理,而TCP不会这样干,需要我们自行进行分包处理,这就叫做面向字节流。所以为了解决这个问题,在应用层我们就需要自己定义协议,自己解包。
守护进程
我们都通过上述的方式进行启动。
#问:这样启动如果我们将终端关掉了,那这个服务还在不在?
我们需要做一个工作,这个工作就叫做:将进程守护进程化。
铺垫讲解
全部都是在前台运行的。
- 前台进程:和终端关联的进程,前台进程。(能否正常的获取我们的输入,能否正常的将我们的输入进行处理)即:bash本身就是一个前台进程。
证明当前bash不是前台进程,而此时的前台进程是运行的server。
- 任何xshell登陆,只允许一个前台进程和多个后台进程。
- 进程除了有自己的pid,ppid,还有一个组ID。
- 在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash -> 可以用匿名管道来进行通讯。
- 而同时被创建的多个进程可以成为一个进程的概念,组长一般是第一个进程。
- 任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程,或者用户自己启动的所有用户的进程或者服务,整体都是要属于一个叫做会话的机制中的。
换而言之就是进程间的关系,无非就是PCB中多加了这些数据。
当一个用户登陆的时候:
当一个用户退出的时候:
理论上是需要将我们所创建、建立好的资源进行释放,换句话说就是当我们在登陆上系统的时候,我们启动了一个服务,比如说前台服务。然后我们退出了,当前我们所启动的服务就有可能会退出(不同的操作系统、内核版本,有不同的处理方式)。反正关掉就是不合理的,一个服务用户启动然后退出服务就没了,就是不合理。
于是便有守护进程。
比如说windows当中的有一个注销。其就是退出了,然后又弹出输入账号密码的界面,注销其实就相当于退出登陆,因为退出登陆的时候会几乎释放一下曾经我们登陆时所创建的一些相关任务,也就是电脑卡的时候,利用注销也会对应的清理的作用。
守护进程讲解
我们在一个会话中创建一个进程后,我们想让一个进程不要在对应的会话里面(不属于会话)。而是如退出登陆时,这个进程还在执行,于是我们需要将该进程拿出来,让其自成一个会话(守护进程)。
#问:如何将自己变成自成会话呢?
利用setsid函数。
setsid
#include <unistd.h>
// 让自己变为这个会话以及这个进程组的组长
pid_t setsid(void);
- 返回值:
成功,返回对应调用进程的pid,失败返回-1。
Note:
setsid要成功被调用,必须保证当前进程不是进程组的组长(员工随便走,领导不许走)。
#问:怎么保证我不是组长?
利用fork函数。
- 守护进程不能直接向显示器打印消息,一旦打印,会被暂停、终止。
#问:如何在Linux中正确的写一个让进程守护进程化的一个代码?
(通过自己写一个函数,让我们的进程调用这个函数,自动变成守护进程)
Note:
因为守护进程,是一个独立的进程,我们的终端本身是与会话相关联的。一旦打印,会被暂停 / 终止。
需要对标准输出、标准错误、标准输入的重定向。
Linux在设备当中,都存在一个文件/dev/null:
只要我们往其中所写的所有东西,默认是全部丢弃。去读取其并不会阻塞我们,并且什么都不让我们读取到。
(/dev/null文件 == 黑洞文件)
补充:
对于序列化与反序列化,使用别人成熟的方案:json。
其是一种网络通讯的格式,它可以帮助我们完成序列化与反序列化,其是一个{"key":”value"}式的结构,如果有多个就是{"key":”value","key":”value","key":”value"},并且其还支持数组,并且数组里也支持{"key":”value"}式的结构。
在C++中使用的是jsoncpp。
首先我们需要安装库:sudo yum install jsoncpp-devel。
其就会在我们特定的系统路径下安装好了,对应的库文件也是由yum自动的放好。
使用库:
Note:使用自己的库、C/C++标准库之外,那么在编译选项中就需要带上我们所使用的库。
序列化:
#include <iostream> #include <string> #include <jsoncpp/json/json.h> int main() { int a = 10; int b = 20; char c = '+'; Json::Value root; // 万能对象 // 因为Json是kv的,所以我们需要对变量取名字 root["aa"] = a; root["bb"] = b; root["op"] = c; // 序列化 Json::StyledWriter writer; // Json::FastWriter writer; /*上面二者的区别是:序列化的时候,形成的字符串格式不同*/ std::string s = writer.write(root); // 其返回值自动帮我们准换为了序列化 std::cout << s << std::endl; return 0; }
StyledWriter:
FastWriter:
堆叠使用:
#include <iostream> #include <string> #include <jsoncpp/json/json.h> int main() { int a = 10; int b = 20; char c = '+'; Json::Value root; // 万能对象 // 因为Json是kv的,所以我们需要对变量取名字 root["aa"] = a; root["bb"] = b; root["op"] = c; 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; }
这个时候我们再使用ps命令观察它。
守护进程本质就是孤儿进程的一种。
与孤儿进程的区别:
孤儿进程依旧属于某个会话,但是守护进程自成会话。
这个时候这个守护进程服务器就相当于,即在后端提供服务,我们输入的任何信息有日志,就将日志输出到文件当中,就可以定期的让程序员去查这个服务工作的怎么样了。此时如果我们将xshell关闭了,此时这个服务就在云上,在公网上对外可以提供服务了。这个时候如果想让别人用,就将对应的客户端分发给别人让别人使用了,这就是日常生活中,我们所使用服务的原理。
log.hpp
日志信息打印。
#pragma once
#include <cstdarg>
#include <ctime>
#include <cstdio>
#include <unistd.h>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2 // 警告 -- 没出错
#define ERROR 3 // 错误 -- 不影响后续执行(一个功能因为条件等,没有执行)
#define FATAL 4 // 致命 -- 代码无法继续向后执行
const char* gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL",
};
#define LOGFILE "./calculator.log"
// 完整的日志功能,至少:日志等级 时间 日志内容 支持用户自定义
void logMessage(int level, const char* format, ...)// level:日志等级; format, ...:用户传参、日志对应的信息等。
{
#ifndef DEBUG_SHOW
if(level == DEBUG) return;
#endif
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// 向缓冲区logBuffer中打印
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// 向屏幕
//printf("%s%s\n", stdBuffer, logBuffer);
// 向文件中打印
FILE* fp = fopen(LOGFILE, "a");
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
CalClient.cc
客户端。
#include <iostream>
#include <time.h>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace cr_protocol;
void Usage(const std::string &process)
{
std::cout << "\nUasge: " << process << " serverIp serverPort\n"
<< std::endl;
}
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);
}
while (true)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req._x >> req._op >> req._y;
// 2. 序列化
std::string s = req.Serialize();
std::string tmp(s);
// 3.添加长度信息,形成完整的报文
s = Encode(s);
// 4. 发送给服务端
Send(sockfd, s);
bool quit = false;
std::string buffer;
// 5. 正常读取
while (!quit)
{
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 << /*resp._x << resp._op << resp._y*/tmp << " = " << resp._result << " [success]" << std::endl;
break;
}
if (!err.empty()) std::cerr << err << std::endl;
break; // 因为此处我们实现的是一个任务
}
}
close(sockfd);
return 0;
}
Calserver.cc
#include "CalServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <signal.h>
#include <memory>
using namespace cr_tcpserver;
using namespace cr_protocol;
void Usage(const std::string &process)
{
std::cout << "\nUasge: " << process << " port\n"
<< std::endl;
}
// 进行计算
Response calculatorHelper(const Request &req)
{
Response resp(0, 0);
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; // 设置错误码
else
resp._result = req._x / req._y;
break;
case '%':
if (0 == req._y)
resp._code = 2; // 设置错误码
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3; // 设置错误码
break;
}
return resp;
}
void calculator(int sock)
{
std::string inbuffer;
while (true)
{
// 1.读取数据 - 能读多少就读多少,我们并不关心
bool res = Recv(sock, &inbuffer); // 在这里我们读到了一个请求
if (!res) break;
// 2.协议解析 - 保证得到一个完整的报文
std::string package = Decode(inbuffer);
if (package.empty()) // 只要空就返回重新读取
continue;
logMessage(NORMAL, "%s", package.c_str());
// 3.保证该报文是一个完整的报文
Request req;
// 4.反序列化,字节流 -> 结构化
req.Deserialized(package); // 反序列化
// 5.业务处理
Response resp = calculatorHelper(req);
// 6.序列化
std::string respString = resp.Serialize(); // 序列化
// 7.添加长度信息,形成完整的报文
respString = Encode(respString);
// 8.发送数据
Send(sock, respString);
}
}
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl;
exit(0);
}
int main(int argc, char *argv[])
{
// 一般经验:server在编写的时候,要有较为严谨性的判断逻辑
// 一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题!
// signal(SIGPIPE, SIG_IGN);
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
MyDaemon();
std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
server->BindService(calculator);
// 将进程守护进程化
server->Srart();
// // 测试序列化
// Request req(123, 456, '+');
// std::string s = req.Serialize();
// std::cout << s << std::endl;
// // 测试反序列化
// temp.Deserialized(s);
// std::cout << temp._x << std::endl;
// std::cout << temp._op << std::endl;
// std::cout << temp._y << std::endl;
return 0;
}
CalServer.hpp
#pragma once
#include "Sock.hpp"
#include "log.hpp"
#include <functional>
#include <string>
#include <vector>
#include <pthread.h>
namespace cr_tcpserver
{
using func_t = std::function<void(int)>;
class TcpServer;
class PthreadData
{
public:
PthreadData(int sock, TcpServer* server) :_sock(sock), _server(server)
{}
~PthreadData()
{}
public:
int _sock;
TcpServer* _server;
};
class TcpServer
{
private:
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
PthreadData *td = static_cast<PthreadData *> (args);
td->_server->Excute(td->_sock);
close(td->_sock);
return nullptr;
}
public:
// 一般而言
// const std::string &:输入型参数
// std::string *:输出型参数
// std::string &:输入输出型参数
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 Srart()
{
while (true)
{
std::string clinentip;
uint16_t clinentport;
int sock = _sock.Accept(_listensock, &clinentip, &clinentport);
if (sock == -1)
continue;
logMessage(NORMAL, "create neww link success, sock: %d", sock);
pthread_t tid;
PthreadData * td = new PthreadData(sock, this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
~TcpServer()
{
if (_listensock >= 0)
close(_listensock);
}
private:
int _listensock;
Sock _sock;
std::vector<func_t> _func;
// std::unordered_map<std::string, func_t> func_; // 为每一个服务起名字
};
}
Protocol.hpp
定制的协议。
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
// 客户端于服务器共同约定的协议(需要相互遵守)
namespace cr_protocol
{
// #define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof - 因为其会将'\0'算上。
struct Request // 定好的协议
{
// 1. 自主实现 "length\r\nx_ op_ y_\r\n"
// 2. 使用现成的方案
std::string Serialize() // 序列化
{
#ifdef MYSELF
// "1234 + 5678"
std::string str;
str = std::to_string(_x);
str += SPACE;
str += _op;
str += SPACE;
str += std::to_string(_y);
return str;
#else
// std::cout << "……" << std::endl;
Json::Value root; // Value是万能的对象(可以定义任何对象)
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool Deserialized(const std::string &str) // 反序列化
{
#ifdef MYSELF
// "1234 + 5678"
std::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
// std::cout << "……" << std::endl;
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asInt(); // char的本质是整数
return true;
#endif
}
Request()
{
}
Request(int x, int y, char op) : _x(x), _y(y), _op(op)
{
}
~Request()
{
}
int _x;
int _y;
char _op; // '+' '-' '*' '/' '%'
};
struct Response
{
std::string Serialize() // 序列化
{
#ifdef MYSELF
std::string s;
s = std::to_string(_code);
s += SPACE;
s += std::to_string(_result);
return s;
#else
// std::cout << "……" << std::endl;
Json::Value root;
root["code"] = _code;
root["result"] = _result;
Json::FastWriter writer;
return writer.write(root);
#endif
}
bool Deserialized(const std::string &str) // 反序列化
{
#ifdef MYSELF
std::size_t pos = str.find(SPACE);
if (pos == std::string::npos)
return false;
_code = atoi(str.substr(0, pos).c_str());
_result = atoi(str.substr(pos + SPACE_LEN).c_str());
return true;
#else
//std::cout << "……" << std::endl;
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
_code = root["code"].asInt();
_result = root["result"].asInt();
return true;
#endif
}
Response() {}
Response(int result, int code) : _result(result), _code(code)
{
}
~Response()
{
}
int _result; // 计算结果
int _code; // 计算结果的状态码(出现运算符不存在,/0,%0等)
};
// 临时方案 - 接收
bool Recv(int sock, std::string *out)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = '\0';
*out += buffer;
}
else if (s == 0)
{
// std::cout << "client quit" << std::endl;
return false;
}
else
{
// std::cout << "recv error" << std::endl;
return 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\nx_ op_ y_\r\n") - 解析buffer
std::string Decode(std::string &buffer)
{
// 没有"\r\n" - 不做处理
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 - 2 * SEP_LEN;
if (surplus >= size)
{
// 至少是一个合法完整的报文,就可以动手提取了
buffer.erase(0, pos + SEP_LEN);
std::string s = buffer.substr(0, size);
buffer.erase(0, size + SEP_LEN);
return s;
}
else
return "";
}
// 根据我们所定义的协议("length\r\nx_ op_ y_\r\n") - 封装buffer
std::string Encode(std::string &s)
{
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
}
Daemon.hpp
将进程变为守护进程。
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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_WRONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
文章来源:https://www.toymoban.com/news/detail-406135.html
总结
在网络计算器中,我们实现了:文章来源地址https://www.toymoban.com/news/detail-406135.html
- 序列化与反序列化
- 定义了自己的协议
- 将我们的服务守护进程化,让他变成一个网络服务。
到了这里,关于【网络】-- 应用层的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!