网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)

目录

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. 网络版本计算器

例如, 我们需要实现一个服务器版的计算器,我们需要客户端把要计算的两个数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

这里通过实现一个网络版的计算器来讲解具体的用户协议定制以及序列化和反序列化的过程,其中用户向服务器发起计算请求,服务器计算完成后将结果响应给用户。协议是一种约定。看看方案:

约定方案一:
  • 客户端发送一个形如"1+1"的字符串
  • 这个字符串中有两个操作数,都是整形
  • 两个数字之间会有一个字符是运算符
  • 运算符只能是加减乘除和取模
  • 数字和运算符之间没有空格
约定方案二:
  • 定义结构体来表示我们需要交互的信息
  • 发送数据时将这个结构体按照一个规则转换成字符串
  • 接收到数据的时候再按照相同的规则把字符串转化回结构体

这个过程叫做 "序列化" 和 "反序列化

无论我们采用方案一,还是方案二,还是其他的方案,只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析,就是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   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); // 获取时间戳
    // 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); // 相当于ap=nullptr
    
    printf("%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.hpp"

class Sock
{
private:
    const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:
    Sock()
    {}
    ~Sock()
    {}

    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if (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;
        else
            return false;
    }
};

TcpServer.hpp

基于上一篇tcp_server.cc改的sock.hpp,再封装一个TcpServer.hpp:(二次封装)

#pragma once

#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>

namespace ns_tcpserver
{
    using func_t = std::function<void(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_cast<ThreadData *>(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); // 线程处理网络服务,要得到sock
                pthread_create(&tid, nullptr, ThreadRoutine, td);
            }
        }

        ~TcpServer()
        {
            if (_listensock >= 0)
                close(_listensock);
        }
    private:
        int _listensock;
        Sock _sock;
        std::vector<func_t> _func;
    };
}

第一次测试(链接)

Makefile

.PHONY:all
all:client CalServer

client:CalClient.cc
	g++ -o $@ $^ -std=c++11
CalServer:CalServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f client CalServer

CalServer.cc

#include "TcpServer.hpp"
#include <memory>

using 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_ptr<TcpServer> server(new TcpServer(atoi(argv[1]))); // 网络功能
    server->BindService(Debug); // 绑定一个服务方法,网络功能和服务进行了解耦
    server->Start();

    return 0;
}

CalClient.cc

#include <iostream>

int main(int argc, char *argv[])
{
    return 0;
}

编译运行:

成功运行,客户端什么也没做,链接一建立就自动退出了。


2.2 计算器实现

先把我们约定的协议(Protocol)封装成一个文件:

Protocol.hpp先写一个框架:

#pragma once

#include <iostream>
#include <string>
#include <cstring>

namespace 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; // 自己定义的类似错误码
        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 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 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"
        // "1234 + 5678"
        bool Deserialized(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
        }

第二次测试(序列化和反序列化)

// ./CalServer port
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->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\n123+789\r\n
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if (s == 0)
            return false;
        else
            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\n_x _op _y\r\n..." // 10\r\nabc // "_x _op _y\r\n length\r\nXXX\r\n"
    std::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;
        }
        else
            return "";
    }

    std::string Encode(std::string &s) // 添加长度信息,形成一个完整的报文
    {   // "XXXXXxX" -> "7\r\nXXXxXXX\r\n"
        std::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 <memory>

using 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; // 自己定义的类似错误码
        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)
    {
        // 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_ptr<TcpServer> 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.hpp"

using 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(操作系统),PID,PGID以及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" : op

"exitcode" : exitcode
"result" : result

就是将不同类型的变量和一个字符串绑定起来形成键值对,序列化的时候将多个字符串拼接在一起形成一个字符串。反序列化的时候再将多个字符串拆开,根据键值对的对应关系找到绑定的变量。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int 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 CalServer

client:CalClient.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp

.PHONY:clean
clean:
	rm -f client CalServer

Protocol.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#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) // 不能是sizeof

    // 1. 自主实现序列化的格式: "length\r\n_x _op _y\r\n" (约定/协议)
    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); // 返回值是序列化好的结果,直接return
#endif
        }

        // "_x _op _y"
        // "1234 + 5678"
        bool Deserialized(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; // 继续定义万能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 _result"
        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; // 和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 0"
        bool Deserialized(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; // 和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\n123+789\r\n
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if (s == 0)
            return false;
        else
            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\n_x _op _y\r\n..." // 10\r\nabc // "_x _op _y\r\n length\r\nXXX\r\n"
    std::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;
        }
        else
            return "";
    }

    std::string Encode(std::string &s) // 添加长度信息,形成一个完整的报文
    {   // "XXXXXxX" -> "7\r\nXXXxXXX\r\n"
        std::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协议(方法+报头+状态码)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/175859.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

黑苹果新手指导:名词解释常用软件常见问题说明

黑苹果新手指导&#xff1a;名词解释&常用软件&常见问题说明 写在前面名词解释系统篇引导篇工具篇 常见问题安装篇如何安装黑苹果&#xff1f;安装过程中卡在一排号怎么办&#xff1f;AMD处理器可以安装黑苹果 macOS吗&#xff1f;我的笔记本电脑为什么不能驱动独立显卡…

【腾讯云云上实验室-向量数据库】腾讯云开创新时代,发布全新向量数据库Tencent Cloud VectorDB

前言 随着人工智能、数据挖掘等技术的飞速发展&#xff0c;海量数据的存储和分析越来越成为重要的研究方向。在海量数据中找到具有相似性或相关性的数据对于实现精准推荐、搜索等应用至关重要。传统关系型数据库存在一些缺陷&#xff0c;例如存储效率低、查询耗时长等问题&…

腾讯云服务器99元一年是真的吗?假的!

腾讯云服务器99元一年是真的吗&#xff1f;假的&#xff0c;不用99元&#xff0c;只要88元即可购买一台2核2G3M带宽的轻量应用服务器&#xff0c;99元太多了&#xff0c;88元就够了&#xff0c;腾讯云百科活动 txybk.com/go/txy 活动打开如下图&#xff1a; 腾讯云轻量服务器 腾…

AIGC变革BI行业,永洪发布vividime全球化品牌

大数据产业创新服务媒体 ——聚焦数据 改变商业 国内BI商业智能市场&#xff0c;一直有着“内永洪&#xff0c;外Tableau”的说法。成立于2012年的永洪科技经过十多年的发展&#xff0c;早已崛起为国内大数据行业的一支劲旅。 ChatGPT火爆出圈之后&#xff0c;AIGC快速渗透&am…

MATLAB - text的两种使用方法

text小技巧 1. 常规使用&#xff08;Method 1&#xff09;2. 在显示画面的相对位置&#xff08;Method 2&#xff09;3. 举个例子 1. 常规使用&#xff08;Method 1&#xff09; text(x,y,txt)2. 在显示画面的相对位置&#xff08;Method 2&#xff09; text(string,‘ABC’,…

优思学院|质量管理怎样才能做好?

质量管理怎样才能做好&#xff1f;这是一个好问题&#xff0c;很多人第一时间会想到建立一个稳定的质量管理体系&#xff0c;例如ISO9001&#xff0c;又或者善用QC七大手法等等&#xff0c;虽然以上这些方法都是实用和正确的&#xff0c;绝大多数企业通常最忽略的&#xff0c;其…

Vatee万腾数字化力量的奇迹:vatee数字化解决方案的独特之选

在数字化时代的潮流中&#xff0c;Vatee万腾以其引人注目的数字化力量&#xff0c;创造了令人瞩目的奇迹。其数字化解决方案作为独特之选&#xff0c;不仅在技术上取得显著突破&#xff0c;更在为企业和个人提供创新性解决方案方面展现了卓越之处。 Vatee万腾的数字化力量体现在…

优化3种教学方法

在教育领域&#xff0c;教学方法对于学生的学习成果和兴趣至关重要。 第一种是项目式学习。这种方法鼓励学生通过完成实际的项目来获取知识&#xff0c;而不仅仅是在课堂上听讲。学生需要在实际操作中解决问题&#xff0c;这能培养他们的创新思维和实践能力。项目式学习还能提高…

【从入门到起飞】JavaSE—多线程(2)(lock锁,死锁,等待唤醒机制)

文章目录 &#x1f33a;lock锁⭐获得锁⭐释放锁✨注意&#x1f3f3;️‍&#x1f308;代码实现&#x1f388;细节 &#x1f33a;死锁⭐解决方法 &#x1f384;等待唤醒机制⭐代码实现&#x1f388;注意 &#x1f6f8;使用阻塞队列实现等待唤醒机制 &#x1f354;线程的六种状态…

STL的认知

STL vector 头文件<vector> 初始化,定义,定义长度&#xff0c;定义长度并且赋值&#xff0c;从数组中获取数据返回元素个数size()判断是否为空empty()返回第一个元素front()返回最后一个数back()删除最后一个数pop_back()插入push_back(x)清空clear()begin()end()使用s…

C#中的var究竟是强类型还是弱类型?

前言 在C#中&#xff0c;var关键字是用来声明变量类型的&#xff0c;它是C# 3.0推出的新特征&#xff0c;它允许编译器根据初始化表达式推断变量类型&#xff0c;有点跟javascript类似&#xff0c;而javascript中的var是弱类型。它让C#变量声明更加简洁&#xff0c;但也导致了…

优化 Python requests 库文档

在Python的requests库的文档中&#xff0c;缺少了一个指向意大利语翻译的链接。 1&#xff1a;定位文档源代码 首先&#xff0c;我们需要找到Python requests库的文档源代码。 2&#xff1a;克隆仓库并编辑文档** 一旦我们找到了仓库&#xff0c;我们可以将其克隆到本地。然…

重生奇迹mu格斗怎么加点

1.力量加点 力量是格斗家的主要属性之一&#xff0c;它可以增加你的攻击力和物理伤害。因此&#xff0c;对于格斗家来说&#xff0c;力量加点是非常重要的。建议在前期将大部分的加点放在力量上&#xff0c;这样可以让你更快地杀死怪物&#xff0c;提高升级速度。 2.敏捷加点…

下载安装升讯威在线客服系统时提示风险的解决办法

客服系统的服务端程序、客服端程序、配套的配置工具涉及磁盘文件读写、端口监听&#xff0c;特别是经过混淆加密后&#xff0c;可能被部分浏览器或部分杀毒软件提示风险。请忽略并放心使用&#xff0c;如果开发软件是为了植入木马&#xff0c;这个代价可太大了&#xff0c;不如…

npm install安装报错

npm WARN notsup Not compatible with your version of node/npm: v-click-outside-x3.7.1 npm ERR! Error while executing: npm ERR! /usr/bin/git ls-remote -h -t ssh://gitgithub.com/itargaryen/simple-hotkeys.git 解决办法1&#xff1a;&#xff08;没有解决我的问题…

“高校评分”走红网络,虎扑:若造谣抹黑,学校可联系平台处理

哎呀&#xff0c;最近虎扑APP的全国高校评分可是火遍了网络啊&#xff01;那些机智的评语&#xff0c;哦哟&#xff0c;都成了新的“网络爆款梗”&#xff01;有毕业生说嘛&#xff0c;这评分都是看学生自己的经历和感受&#xff0c;有好评当然就有差评啦。但关键是&#xff0c…

【腾讯云云上实验室-向量数据库】腾讯云VectorDB:深度学习场景下的新一代数据存储方案

引言 ​  在深度学习领域的实践中&#xff0c;一般会涉及到向量化处理的数据&#xff0c;如图像、文本、音频等&#xff0c;这些数据的存储和检索对于许多深度学习任务至关重要。传统的关系型数据库和NoSQL数据库在存储和检索这类大规模向量数据时&#xff0c;通常不能满足高…

[数据结构]—栈和队列

&#x1f493;作者简介&#x1f389;&#xff1a;在校大二迷茫大学生 &#x1f496;个人主页&#x1f389;&#xff1a;小李很执着 &#x1f497;系列专栏&#x1f389;&#xff1a;数据结构 每日分享✨&#xff1a;到头来&#xff0c;有意义的并不是结果&#xff0c;而是我们度…

论文笔记:Localizing Cell Towers fromCrowdsourced Measurements (intro 部分)

2015 1 Intro 1.1 motivation opensignal.com 、cellmapper.net 和 opencellid.org 都是提供天线&#xff08;antenna&#xff09;位置的网站 他们提供的天线位置相当准确&#xff0c;但至少在大多数情况下不完全正确这个目标难以实现的原因是蜂窝网络供应商没有义务提供有…

https想访问本地部署的http://localhost接口

情况说明&#xff1a; 网址是https的&#xff0c;想访问java本地启的一个程序接口http://localhost:8089 解决办法 java程序加上
最新文章