目录
- TCP协议通信流程
- TCP三次握手
- 数据传输过程
- 四次挥手过程
- TCP 和 UDP 对比
- 认识协议
- 协议的概念
- 结构化数据的传输
- 序列化和反序列化
- 网络版计算器
- 服务端代码
- 面向字节流
- 协议定制
- 客户端代码编写
- 代码测试
- 守护进程
- 守护进程创建
- 关于协议制定中使用现成方法实现
TCP协议通信流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
TCP三次握手
服务器初始化
- 调用socket,创建文件描述符;
- 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt,并阻塞, 等待客户端连接过来。
建立连接的过程
- 调用socket,创建文件描述符;
- 调用connect,向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答;(第一次)
- 服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接";(第二次)
- 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段;(第三次)
在此我们需要注意的是accept并不参与三次握手的过程,因为三次握手本身就是底层TCP所做的工作。accept要做的只是将底层已经建立好的连接拿到用户层,如果底层没有建立好的连接,那么accept函数就会阻塞住直到有建立好的连接。
数据传输过程
- 建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同一条连接中,同一时刻, 通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
- 服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回,发送下一条请求,如此循环下去。
四次挥手过程
当双方通信结束之后,需要通过四次挥手的方案使双方断开连接,当客户端调用close关闭连接后,服务器最终也会关闭对应的连接。而其中一次close就对应两次挥手,因此一对close最终对应的就是四次挥手。
- 如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次);
- read返回之后,服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN;(第三次)
- 客户端收到FIN,再返回一个ACK给服务器;(第四次)
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
为什么要断开连接?
建立连接本质上就是要保证双方的通信,建立连接以后,我们就会传输数据,如果建立连接以后不断开,就会造成系统资源越来越少。
操作系统同样会对这些连接进行管理,在服务端会对这些连接产生的数据结构进行管理,随着连接的增加,维护此数据结构的时间和空间成本也随之增加,所以双方通信以后就应该断开连接。
TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报
认识协议
协议的概念
协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
结构化数据的传输
通信双方在进行网络通信时:
如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。
如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。
将结构化的数据组合成一个字符串
- 客户端发送一个形如“1+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 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *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);
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 <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#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;
public:
Sock()
{
}
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(ERROR, "create socket error:%d:%s", errno, strerror(errno));
exit(0);
}
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(ERROR, "bind error:%d:%s", errno, strerror(errno));
exit(1);
}
}
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(ERROR, "listen error:%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init server success...");
}
int Accept(int listensock, uint16_t *port, std::string *ip)
{
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);
exit(3);
}
if (port)
*port = htons(src.sin_port);
if (ip)
*ip = inet_ntoa(src.sin_addr);
return servicesock;
}
bool Connect(int sock, const uint16_t &server_port, const std::string &server_ip)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
socklen_t len = 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, len) == 0)
return true;
else
return false;
}
~Sock()
{
}
};
服务端代码
我们所需要做的就是先初始化服务器,然后就是启动服务器了,启动服务器后就是不断调用accept函数,从监听套接字当中获取新链接,每当获取一个链接之后就创建一个新线程,该线程为服务端提供服务。
TcpServer.hpp
#pragma once
#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>
namespace ns_tcpserver
{
using func_t = std::function<void(int)>;
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_);
// delete td;
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 BindServer(func_t func)
{
func_.push_back(func);
}
void Excute(int sock)
{
for (auto &f : func_)
{
f(sock);
}
}
void start()
{
for (;;)
{
std::string clientip;
uint16_t clientport;
int sock = sock_.Accept(listensock_, &clientport, &clientip);
if (sock == -1)
continue;
logMessage(NORMAL, "create new link success, sock: %d", sock);
pthread_t tid;
ThreadData *td = new ThreadData(sock, this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
~TcpServer()
{
if (listensock_ >= 0)
close(listensock_);
}
private:
int listensock_;
Sock sock_;
std::vector<func_t> func_;
};
}
CalServer.cc
#include <iostream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include "TcpServer.hpp"
#include "Protocol.hpp"
using namespace ns_tcpserver;
using namespace ns_protocol;
static void usage(const std::string &proc)
{
std::cout << "\nusage:" << proc << "port\n"
<< std::endl;
}
static 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 ret = Recv(sock, &inbuffer);
if (!ret)
break;
// std::cout << "begin: inbuffer: " << inbuffer << std::endl;
// 2. 协议解析,获取完整报文
std::string message = Decode(inbuffer);
if (message.empty())
continue;
// std::cout << "end: inbuffer: " << inbuffer << std::endl;
// std::cout << "packge: " << message << std::endl;
logMessage(NORMAL, "%s", message.c_str());
// 3. 保证该报文是一个完整报文
Request req;
// 4. 反序列化 字节流->结构化
req.Deseserialize(message);
// 5. 业务逻辑
Response resp = calculatorHelper(req);
// 6. 序列化
std::string respString = resp.Serialize();
// 7. 添加长度信息,形成一个完整报文
// std::cout << "respString: " << respString << std::endl;
respString = Encode(respString);
// std::cout << "encode: respString: " << respString << std::endl;
Send(sock, respString);
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
signal(SIGPIPE, SIG_IGN);
std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
server->BindServer(calculator);
server->start();
return 0;
}
上面服务端代码包含了许多细节类的问题,因为我们需要进行结构化数据的传输,所以我们的服务端接收数据的过程是将序列化数据反序列化,因为我们服务端是要进行计算的,我们的网络计算器底层是以TCP来实现的,就存在字节流->结构化的转换问题,就像我们在读取数据的过程中,此时单纯的recv已经不能满足我们的需求,所以我们在接下来定制协议的过程,就需要考虑数据完整性的问题。
Server在编写的时候,要有较为严谨性的判断逻辑,一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题。
服务端创建新线程时,需要将调用accept获取到套接字作为参数传递给该线程,为了避免该套接字被下一次获取到的套接字覆盖,最好在堆区开辟空间存储该文件描述符的值。
面向字节流
当创建一个TCP协议时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区。
就像我们的网络计算器一样,服务端客户端都会存在一个发送缓冲区和一个接收缓冲区:
- 调用write函数就可以将数据写入缓冲区,写入以后write函数就可以返回了,此时发送缓冲区中数据是由TCP进行发送的,至于他什么时候发?发多少?出错了怎么办?这都是由我们TCP协议决定的;
- 如果发送的字节数太长,TCP会将其拆成多个数据包进行发生,如果发送的字节数太长太短,TCP会将其留在发送缓冲区中,等待长度合适后在进行发送;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,可以通过调用read函数来读取接收缓冲区当中的数据。而调用read函数读取接收缓冲区中的数据时,也可以按任意字节数进行读取;
- 由于缓冲区的存在,TCP程序的读和写不需要一一匹配。
实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
协议定制
要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。
- 请求结构体中需要包括两个操作数,以及对应需要进行的操作,而且支持序列化与反序列化操作。
- 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的,而且支持序列化与反序列化操作。
- 必须保证读取的数据具有完整性,必须是两个操作数以及一个操作符,这样才可以计算成功,同样,发送数据也是一样。
协议的制定可以分为自主实现和使用现成的方案,为了更好的理解协议,我们下面使用自主实现的方法,在文中最后面我们会介绍使用现成的方法:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
// 两种方法
// 1. 自主实现
// 2. 使用现成的方案
// 请求
class Request
{
public:
std::string Serialize()
{
#ifdef MYSELF
std::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_"
// "123 + 456"
bool Deseserialize(const std::string &str)
{
#ifdef MYSELF
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
#endif
}
public:
Request()
{
}
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
~Request()
{
}
public:
int x_;
int y_;
char op_;
};
// 响应
class Response
{
public:
Response()
{
}
Response(int code, int result)
: code_(code), result_(result)
{
}
~Response()
{
}
std::string Serialize()
{
#ifdef MYSELF
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
#else
#endif
}
//"code_ result_"
//" 0 100"
bool Deseserialize(const std::string &s)
{
#ifdef MYSELF
std::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
#endif
}
public:
int result_; // 计算结果
int code_; // 计算结果状态码
};
// 必须保证收到的是一个完整的的需求
// TCP是面向字节流的
bool Recv(int sock, std::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)
{
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;
}
}
std::string Decode(std::string &buffer)
{
// "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
// "x_ op_ y_\r\n length\r\nXXX\r\n"
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 "";
}
}
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;
}
}
规定状态字段对应的含义:
- 状态字段为0,表示计算成功。
- 状态字段为1,表示出现除0错误。
- 状态字段为2,表示出现模0错误。
- 状态字段为3,表示非法计算。
客户端代码编写
同样,客户端也需要进行初始化,初始化完成以后就调用connect进行连接服务端,连接完成以后此时发送请求,这里可以让用户输入两个操作数和一个操作符构建一个计算请求,然后将该请求发送给服务端。而当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。
同样,我们服务端进行操作时也会存在遵守我们的协议规定操作。
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace ns_protocol;
static void usage(const std::string &proc)
{
std::cout << "\nusage" << proc << "port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(0);
}
uint16_t server_port = atoi(argv[2]);
std::string server_ip = argv[1];
Sock sock;
int sockfd = sock.Socket();
if (!sock.Connect(sockfd, server_port, server_ip))
{
std::cerr << "connect error" << std::endl;
exit(1);
}
std::string buffer;
bool quit = false;
while (true)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter# ";
std::cin >> req.x_ >> req.op_ >> req.y_;
// 2.序列化
std::string s = req.Serialize();
// 3. 添加报头长度
s = Encode(s);
// 4. 发送给服务端
Send(sockfd, s);
// 5. 正常读取
while (true)
{
bool ret = Recv(sockfd, &buffer);
if (!ret)
{
quit = true;
break;
}
std::string message = Decode(buffer);
if (message.empty())
continue;
Response resp;
resp.Deseserialize(message);
std::string err;
switch (resp.code_)
{
case 1:
err = "除0错误";
break;
case 2:
err = "模0错误";
break;
case 3:
err = "非法输入";
break;
default:
std::cout << "计算结果" << " = " << resp.result_ << std::endl;
break;
}
if (!err.empty())
std::cout << err << std::endl;
break;
}
}
close(sockfd);
return 0;
}
代码测试
运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。而如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。
守护进程
我们会发现,每次我们需要运行时都需要输入./server 8080
命令才可以,对于对于一个服务器来说,他是一直运行的,就像我们在命令行输入各种命令一样,他是立马执行的,就不需要运行服务器。
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,比如作业规划进程crond等。
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着,这种进程有一个名称叫守护进程(Daemon)。
我们上面的网络计算器也可以创建一个守护进程,来保证他周期性的运行。
我们首先需要理解进程组与前台后台进程的概念:
进程组
每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。
通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。
需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
我们可以看见下面三个sleep进程都有一个相同的PPID = 14647,他们是一个进程组,而这个进程组的组长就是我们的sleep 1000
,因为他的PGID与PID相等。
前台进程
直接运行某一可执行程序,例如./可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如S+。
守护进程创建
我们的守护进程都是以前台进程的方式存在的,任何一台xsell中都只存在一个前台进程和多个后台进程,我们需要知道的就是进程组长并不能成为守护进程,所以我们需要通过创建子进程的方法来进行守护进程的创建。
我们在此就原生创建一个守护进程,步骤如下:
- 忽略信号,
SIGPIPE
,SIGCHLD
; - 不要让自己成为组长;
- 调用
setsid
接口; - 标准输入,标准输出,标准错误的重定向到
/dev/null
。,守护进程不能直接向显示器打印消息。
#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);
}
}
此时将我们的网络计算器就可以添加守护进程,我们只需运行一次,只要客户端一启动,他就可以控制终端并且周期性地执行计算任务。
最后如果我们如果想关闭这个守护进程,我们就可以使用kill
命令来杀死它。
关于协议制定中使用现成方法实现
我们进行自主实现是为了更好的理解我们的协议制定,接下来我们就可以使用现成的方法来实现,我们只需引入我们的Json库就可以,首先我们来看一段代码来理解一下:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
int a = 10;
int b = 20;
char c = '+';
Json::Value root;
root["aa"] = a;
root["bb"] = b;
root["cc"] = c;
//Json::StyledWriter writer;
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
运行代码就可以发现,我们Json其实就属于一个(key,value)模型。
此时协议中就可以更改代码为:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#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)
// 两种方法
// 1. 自主实现
// 2. 使用现成的方案
// 请求
class Request
{
public:
std::string Serialize()
{
#ifdef MYSELF
std::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);
#endif
}
// "x_ op_ y_"
// "123 + 456"
bool Deseserialize(const std::string &str)
{
#ifdef MYSELF
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
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
public:
Request()
{
}
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
~Request()
{
}
public:
int x_;
int y_;
char op_;
};
// 响应
class Response
{
public:
Response()
{
}
Response(int code, int result, int x, int y, char op)
: code_(code), result_(result), x_(x), y_(y), op_(op)
{
}
~Response()
{
}
std::string Serialize()
{
#ifdef MYSELF
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
#else
Json::Value root;
root["code"] = code_;
root["result"] = result_;
root["xx"] = x_;
root["yy"] = y_;
root["zz"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
//"code_ result_"
//" 0 100"
bool Deseserialize(const std::string &s)
{
#ifdef MYSELF
std::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;
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:
int result_; // 计算结果
int code_; // 计算结果状态码
int x_;
int y_;
char op_;
};
// 必须保证收到的是一个完整的的需求
// TCP是面向字节流的
bool Recv(int sock, std::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)
{
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;
}
}
std::string Decode(std::string &buffer)
{
// "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
// "x_ op_ y_\r\n length\r\nXXX\r\n"
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 "";
}
}
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;
}
}
运行代码,我们同样可以看到同样计算器正常运行,此时我们的制定协议采用的是现成方法: