【计算机网络_应用层】协议定制&序列化反序列化

1. TCP协议的通信流程

在之前的代码中,相信大家对TCP的通信过程的代码已经有了一定了了解。在很早之前就了解到过一些网络通信的相关描述,比如TCP的三次握手和四次挥手。那么什么是三次握手和四次挥手呢?

在介绍之前我们首先看一个图,通过这个图来了解,接下来我们讲解这张图:

ca04d7ca00e56d5855fd5d0bc694bc6d

在最开始的时候客户端和服务器都是处于关闭状态的。

1. 开始前的准备

  1. 服务端和客户端在任意时刻在应用层调用socket函数分配一个文件描述符
  2. 服务端显示bind指定端口和任意IP地址
  3. 服务端调用listen使对应的文件描述符成为一个监听描述符
  4. 服务端调用accept阻塞等待客户端的连接(至此,服务端在通信钱的准备已经完成

2. 三次握手

  1. 客户端调用connect函数向服务器发起连接请求,然后阻塞自己等待完成

  2. 服务端收到客户端的连接请求之后由OS完成连接然后accept调用完成

    这里connect是三次握手的开始,accept调用完成时三次握手一定已经结束了,三次握手是OS内部自己完成的在TCP层我们感知不到

3. 四次挥手

四次挥手的工作都是由双方的OS完成,而我们决定什么时候挥手,一旦调用系统调用close,应用层就不用管了

2. 应用层协议定制

我们在第一次谈到协议的时候就说协议其实就是一种约定。在此之前,我们也写过一些UDP和TCP的通信代码,使用过一些socket API,我们可以发现socket API在发送数据的时候都是按照“字符串”的形式来发送和接收的,那如果我们要传输一些结构化的数据该怎么办呢?

比如在发送一条QQ消息的时候,需要带上发消息的人的昵称、QQ号、消息本身等等,这些消息必须要一次性绑定的发送,那么我们在发送的时候就需要把这些内容打包成一个“字符串”来发送

为什么不直接发送一个结构体对象?

网络通信涉及到不同的机器,可能出现大小段问题和内存对齐问题等等,所以不能直接发送结构体

这个打包成一个字符串的过程就是序列化,将收到的一个字符串转化为多个信息的过程就是反序列化

那么最终我们发送的消息就可以看作是一个完整的Content,但是TCP通信是面向字节流的,所以在通信的过程中,我们也没有办法知道一次发送过来的数据里面有几个完整的Content,这就需要在应用层定制一些“协议”来保证能区分每个数据包,一般来说我们有以下几种方法

1. 确保每个数据包是定长的; 2. 用特殊符号来表示结尾; 3. 自描述

注意:这里序列化反序列化和协议定制是两码事。序列化反序列化的作用是将要发送的信息变成一整条消息;协议定制的作用是保证每次读取一整个数据包,这个数据包里面会包含包头和有效载荷,这个有效载荷就是我们所说的“一整条消息”

3. 通过“网络计算器”的实现来实现应用层协议定制和序列化

3.1 protocol

设计思想:实现两个类:request用于存储对应的运算请求,存放算式,包括两个操作数和一个操作符。response表示对应请求的响应,也就是运算的结果状态和运算结果。最终经过系列化和反序列化之后形成一个字符串形式的有效载荷,我们在这个有效载荷前面加上报头信息,这里我们**约定:报头的内容是一个字符串格式的数据,存放的是有效载荷的长度,有效载荷和报头之间存在一个分隔符**

这里的约定就是我们的协议

既然有了应用层的通信协议,那么我们就要实现对应的为有效载荷添加报头和去除报头

std::string enLength(const std::string &text) // 在text上加报头
{
    // "content_len"\r\t"text"\r\t
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    return send_string;
}
bool deLength(const std::string &package, std::string *text) // 从package上去报头
{
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

3.2 序列化和反序列化

3.2.1 手写序列化和反序列化

按照我们的约定,我们希望发送的结构化的数据就是Request和Response,里面有一些特定的字段

enum // 协议定义的相关错误枚举
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};
class Request // 客户端请求数据
{
public:
    int x;
    int y;
    char op;
};
class Response // 服务器响应数据
{
public:
    int exitcode;
    int result;
};

那么对于结构化的数据,我们要首先将其序列化,才能够作为有效载荷去添加报头,然后发送。接收到发送的数据去除报头之后的有效载荷,同样需要进行反序列化才能拿到结构化的数据,进行操作

#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符长度
#define LINE_SEP "\r\n"               // 行分隔符(分隔报头和有效载荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符长度
// class Request // 客户端请求数据
bool serialize(std::string *out) // 序列化 -> "x op y"
{
    std::string x_string = std::to_string(x);
    std::string y_string = std::to_string(y);

    *out = x_string;
    *out += SEP;
    *out += op;
    *out += SEP;
    *out += y_string;
    return true;
}
// "x op y"
bool deserialize(std::string &in) // 反序列化
{
    auto left = in.find(SEP);
    auto right = in.rfind(SEP);
    if (left == std::string::npos || right == std::string::npos)
        return false; // 出现了不合法的待反序列化数据
    if (left == right)
        return false; // 出现了不合法的待反序列化数据
    if (right - SEP_LEN - left != 1)
        return false; // op的长度不为1

    std::string left_str = in.substr(0, left);
    std::string right_str = in.substr(right + SEP_LEN);
    if (left_str.empty() || right_str.empty())
        return false;

    x = std::stoi(left_str);
    y = std::stoi(right_str);
    op = in[left + SEP_LEN];
    return true;
}
// class Response // 服务器响应数据
bool serialize(std::string *out) // 序列化
{
    // "exitcode result"
    *out = "";
    std::string ec_string = std::to_string(exitcode);
    std::string res_string = std::to_string(result);

    *out += ec_string;
    *out += SEP;
    *out += res_string;
    return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{
    auto pos = in.find(SEP);
    if (pos == std::string::npos)
        return false;
    std::string ec_string = in.substr(0, pos);
    std::string res_string = in.substr(pos + SEP_LEN);
    if (ec_string.empty() || res_string.empty())
        return false;
    exitcode = std::stoi(ec_string);
    result = std::stoi(res_string);
    return true;
}

3.2.2 使用Json库

我们会发现手写序列化好麻烦 ,那么实际上有人已经帮我们做过这件事情了,提供了一些可以使用的组件,我们只需要按照规则使用即可。常用的序列化和反序列化工具有1. Json; 2. protobuf; 3. xml。这里我们为了使用的方便,采用Json来写。(protobuf在之后的博文会更新使用方式)

// class Request // 客户端请求数据
bool serialize(std::string *out) // 序列化
{
    Json::Value root; // Json::Value 是一个KV结构。首先定义出这个结构
    root["first"] = x; // 按照KV结构的模式,为每个字段添加一个Key,给这个字段赋值
    root["second"] = y;
    root["oper"] = op;

    Json::FastWriter writer; // FastWriter是一个序列化的类,里面提供了write方法,这个方法可以将Value的对象转成std::string
    *out = writer.write(root); // 转换后的字符串就是序列化后的结果
    return true;
}
bool deserialize(std::string &in) // 反序列化
{
    Json::Value root; // 序列化后的结果需要被存放
    Json::Reader reader; // Reader类是用作读取的,里面提供了parse(解析)方法,可以将对应的序列化结果string转化成Value对象
    reader.parse(in, root);

    x = root["first"].asInt();// 按照KV结构的模式将存放的内容提取出来,提取出来的结果的类型是Json内部的,要使用的时候需要指定类型
    y = root["second"].asInt();
    op = root["oper"].asInt();
    return true;
}

// class Response // 服务器响应数据
bool serialize(std::string *out) // 序列化
{
    Json::Value root;
    root["first"] = exitcode;
    root["second"] = result;
    Json::FastWriter writer;
    *out = writer.write(root);
    return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{
    Json::Value root;
    Json::Reader reader;
    reader.parse(in, root);
    exitcode = root["first"].asInt();
    result = root["second"].asInt();
    return true;
}

Json库不是标准库的内容,所以在使用之前需要安装,在cent OS下的安装命令

sudo yum install -y jsoncpp-devel # 安装json

安装之后编译我们的代码会报错么?当然会!因为我们没有链接

cc=g++

.PHONY:all
all:Server Client

Server:calServer.cc
	$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11 # 这里加上-ljsoncpp

Client:calClient.cc
	$(cc) -o $@ $^ -ljsoncpp -std=c++11 # 这里加上-ljsoncpp

.PHONY:clean
clean:
	rm -f Server Client

3.3 数据包读取

首先明确一点:TCP协议是面向字节流的,不能确定是否当前收到的就是一个完整的报文,所以需要进行判断与读取

这里我们采用的方法是:如果读取到一个完整的报文就进行后续处理,如果没有读取到一个完整的报文,那就继续读取,直到遇到完整报文再处理

/**
 * sock:读取对应套接字的报文
 * inbuffer:接收缓冲区,这里存放接收到的所有数据
 * req_text:输出型参数,如果读到完整报文就将报文内容存放到req_text中
 * 返回值:读取成功返回true,失败返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
        if (n > 0)
        {
            buffer[n] = 0;      // 当前本次接收的数据
            inbuffer += buffer; // 放在inbuffer后面,处理整个inbuffer
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue; // 还没有接收完一个完整的报头
            // 走到当前位置确定能接收到一个完整的报头
            std::string text_len_string = inbuffer.substr(0, pos);                // 报头拿完了,报头就是这个有效载荷的长度
            int text_len = std::stoi(text_len_string);                            // 有效载荷的长度
            int total_len = text_len + 2 * LINE_SEP_LEN + text_len_string.size(); // 报文总长度

            if (inbuffer.size() < total_len)
            {
                // 收到的信息不是一个完整的报文
                continue;
            }
            // 到这里就拿到了一个完整的报文
            *req_text = inbuffer.substr(0, total_len);
            inbuffer.erase(0, total_len); // 在缓冲区中删除拿到的报文
            return true;
        }
        else
            return false;
    }
}

3.4 服务端设计

按照我们在上一篇博文的多进程版本设计,这里服务端将会让一个孙子进程来执行相关的操作,其中孙子进程需要执行的任务分为5个步骤:

1. 读取报文,读取到一个完整报文之后去掉报头; 2. 将有效载荷反序列化; 3. 进行业务处理(回调); 4. 将响应序列化; 5. 将徐姐话的响应数据构建成一个符合协议的报文发送回去

void handleEntery(int sock, func_t func) // 服务端调用
{
    std::string inbuffer;// 接收缓冲区
    while(true)
    {
        // 1. 读取数据
        std::string req_text, req_str;
        // 1.1 读到一个完整的请求(带报头)req_text = "content_len"\r\t"x op y"\r\t
        if(!recvPackage(sock, inbuffer, &req_text)) return;
        // 1.2 将req_text解析成req_str(不带报头)"x op y"
        if(!deLength(req_text, &req_str)) return;

        // 2. 数据反序列化
        Request req;
        if(!req.deserialize(req_str)) return;

        // 3. 业务处理
        Response resp;
        func(req, resp);

        // 4. 数据序列化
        std::string send_str;
        if(!resp.serialize(&send_str)) return;

        // 5. 发送响应数据
        // 5.1 构建一个完整的报文
        std::string resp_str = enLength(send_str);
        // 5.2 发送
        send(sock, resp_str.c_str(), resp_str.size(), 0);
    }
}

对应需要执行的内容我们就在业务逻辑层来处理

bool cal(const Request &req, Response &resp)
{
    // 此时结构化的数据就在req中,可以直接使用
    resp.exitcode = OK;
    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 (req.y == 0)
            resp.exitcode = DIV_ZERO;
        else
            resp.result = req.x / req.y;
    }
    break;
    case '%':
    {
        if (req.y == 0)
            resp.exitcode = MOD_ZERO;
        else
            resp.result = req.x % req.y;
    }
    break;
    default:
        resp.exitcode = OP_ERROR;
        break;
    }
}

3.5 最后的源代码和运行结果

/*calServer.hpp*/
#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>

#include <string>
#include <functional>

#include "log.hpp"
#include "protocol.hpp"

namespace Server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    typedef std::function<bool(const Request &req, Response &resp)> func_t;

    void handleEntery(int sock, func_t func) // 服务端调用
    {
        std::string inbuffer;// 接收缓冲区
        while(true)
        {
            // 1. 读取数据
            std::string req_text, req_str;
            // 1.1 读到一个完整的请求(带报头)req_text = "content_len"\r\t"x op y"\r\t
            if(!recvPackage(sock, inbuffer, &req_text)) return;
            // 1.2 将req_text解析成req_str(不带报头)"x op y"
            if(!deLength(req_text, &req_str)) return;

            // 2. 数据反序列化
            Request req;
            if(!req.deserialize(req_str)) return;

            // 3. 业务处理
            Response resp;
            func(req, resp);

            // 4. 数据序列化
            std::string send_str;
            if(!resp.serialize(&send_str)) return;

            // 5. 发送响应数据
            // 5.1 构建一个完整的报文
            std::string resp_str = enLength(send_str);
            // 5.2 发送
            send(sock, resp_str.c_str(), resp_str.size(), 0);
        }
    }
    class tcpServer;
    class ThreadData // 封装线程数据,用于传递给父进程
    {
    public:
        ThreadData(tcpServer *self, int sock) : _self(self), _sock(sock) {}

    public:
        tcpServer *_self;
        int _sock;
    };

    class tcpServer
    {
    public:
        tcpServer(uint16_t &port) : _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock == -1)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success:%d", _listensock);
            // 2.bind自己的网络信息
            sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            int n = bind(_listensock, (struct sockaddr *)&local, sizeof local);
            if (n == -1)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");
            // 3. 设置socket为监听状态
            if (listen(_listensock, gbacklog) != 0) // listen 函数
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }

        void start(func_t func)
        {
            while (true)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof peer;
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    logMessage(ERROR, "accept error, next");
                    continue;
                }

                // version 2:多进程版本
                pid_t id = fork();
                if (id == 0)
                {
                    close(_listensock); // 子进程不会使用监听socket,但是创建子进程的时候写时拷贝会拷贝,这里先关掉
                    // 子进程再创建子进程
                    if (fork() > 0)
                        exit(0); // 父进程退出
                    // 走到当前位置的就是子进程
                    handleEntery(sock, func); // 使用
                    close(sock);     // 关闭对应的通信socket(这里也可以不关闭,因为此进程在下个语句就会退出)
                    exit(0);         // 孙子进程退出
                }
                // 走到这里的是监听进程(爷爷进程)
                pid_t n = waitpid(id, nullptr, 0);
                if (n > 0)
                {
                    logMessage(NORMAL, "wait success pid:%d", n);
                }
                close(sock);

            }
        }
        ~tcpServer() {}

    private:
        uint16_t _port;
        int _listensock;
    };

} // namespace Server
/*calServer.cc*/
#include <iostream>
#include <memory>

#include "calServer.hpp"
#include "protocol.hpp"

using namespace Server;

static void Usage(const char *proc)
{
    std::cout << "\n\tUsage:" << proc << " local_port\n";
}

bool cal(const Request &req, Response &resp)
{
    // 此时结构化的数据就在req中,可以直接使用
    resp.exitcode = OK;
    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 (req.y == 0)
            resp.exitcode = DIV_ZERO;
        else
            resp.result = req.x / req.y;
    }
    break;
    case '%':
    {
        if (req.y == 0)
            resp.exitcode = MOD_ZERO;
        else
            resp.result = req.x % req.y;
    }
    break;
    default:
        resp.exitcode = OP_ERROR;
        break;
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<tcpServer> tsvr(new tcpServer(port));
    tsvr->initServer();
    tsvr->start(cal);
    return 0;
}
/*protocol.hpp*/
#pragma once

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

#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符长度
#define LINE_SEP "\r\n"               // 行分隔符(分隔报头和有效载荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符长度

enum // 协议定义的相关错误枚举
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

std::string enLength(const std::string &text) // 在text上加报头
{
    // "content_len"\r\t"text"\r\t
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    return send_string;
}
bool deLength(const std::string &package, std::string *text) // 从package上去报头
{
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request // 客户端请求数据
{
public:
    Request() {}
    Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) {}
    bool serialize(std::string *out) // 序列化 -> "x op y"
    {
#ifdef MYSELF
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
#else
        Json::Value root; // Json::Value 是一个KV结构。首先定义出这个结构
        root["first"] = x; // 按照KV结构的模式,为每个字段添加一个Key,给这个字段赋值
        root["second"] = y;
        root["oper"] = op;

        Json::FastWriter writer; // FastWriter是一个序列化的类,里面提供了write方法,这个方法可以将Value的对象转成std::string
        *out = writer.write(root); // 转换后的字符串就是序列化后的结果
#endif
        return true;
    }
    // "x op y"
    bool deserialize(std::string &in) // 反序列化
    {
#ifdef MYSELF
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false; // 出现了不合法的待反序列化数据
        if (left == right)
            return false; // 出现了不合法的待反序列化数据
        if (right - SEP_LEN - left != 1)
            return false; // op的长度不为1

        std::string left_str = in.substr(0, left);
        std::string right_str = in.substr(right + SEP_LEN);
        if (left_str.empty() || right_str.empty())
            return false;

        x = std::stoi(left_str);
        y = std::stoi(right_str);
        op = in[left + SEP_LEN];
#else
        Json::Value root; // 序列化后的结果需要被存放
        Json::Reader reader; // Reader类是用作读取的,里面提供了parse(解析)方法,可以将对应的序列化结果string转化成Value对象
        reader.parse(in, root);

        x = root["first"].asInt();// 按照KV结构的模式将存放的内容提取出来,提取出来的结果的类型是Json内部的,要使用的时候需要指定类型
        y = root["second"].asInt();
        op = root["oper"].asInt();
#endif
        return true;
    }

public:
    int x;
    int y;
    char op;
};

class Response // 服务器响应数据
{
public:
    bool serialize(std::string *out) // 序列化
    {
#ifdef MYSELF
        // "exitcode result"
        *out = "";
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        *out += ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["first"] = exitcode;
        root["second"] = result;
        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }
    bool deserialize(std::string &in) // 反序列化 "exitcode result"
    {
#ifdef MYSELF
        auto pos = in.find(SEP);
        if (pos == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, pos);
        std::string res_string = in.substr(pos + SEP_LEN);
        if (ec_string.empty() || res_string.empty())
            return false;
        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);
        exitcode = root["first"].asInt();
        result = root["second"].asInt();
#endif
        return true;
    }

public:
    int exitcode;
    int result;
};

/**
 * sock:读取对应套接字的报文
 * inbuffer:接收缓冲区,这里存放接收到的所有数据
 * req_text:输出型参数,如果读到完整报文就将报文内容存放到req_text中
 * 返回值:读取成功返回true,失败返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
        if (n > 0)
        {
            buffer[n] = 0;      // 当前本次接收的数据
            inbuffer += buffer; // 放在inbuffer后面,处理整个inbuffer
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue; // 还没有接收完一个完整的报头
            // 走到当前位置确定能接收到一个完整的报头
            std::string text_len_string = inbuffer.substr(0, pos);                // 报头拿完了,报头就是这个有效载荷的长度
            int text_len = std::stoi(text_len_string);                            // 有效载荷的长度
            int total_len = text_len + 2 * LINE_SEP_LEN + text_len_string.size(); // 报文总长度

            if (inbuffer.size() < total_len)
            {
                // 收到的信息不是一个完整的报文
                continue;
            }
            // 到这里就拿到了一个完整的报文
            *req_text = inbuffer.substr(0, total_len);
            inbuffer.erase(0, total_len); // 在缓冲区中删除拿到的报文
            return true;
        }
        else
            return false;
    }
}
/*calClient.hpp*/
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <string>

#include "log.hpp"
#include "protocol.hpp"

namespace Client
{
    class tcpClient
    {
    public:
        tcpClient(uint16_t &port, std::string &IP) : _serverPort(port), _serverIP(IP), _sockfd(-1) {}

        void initClient()
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd == -1)
            {
                std::cerr << "create socket error" << std::endl;
                exit(2);
            }
        }

        void run()
        {
            struct sockaddr_in server;
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverPort);
            server.sin_addr.s_addr = inet_addr(_serverIP.c_str());

            if (connect(_sockfd, (struct sockaddr *)&server, sizeof server) != 0)
            {
                // 链接失败
                std::cerr << "socket connect error" << std::endl;
            }
            else
            {
                std::string line;
                std::string inbuffer;
                while (true)
                {
                    std::cout << "mycal>>> ";
                    std::getline(std::cin, line);

                    Request req = ParseLine(line);

                    std::string content;
                    req.serialize(&content); // 序列化结果存放的content中

                    std::string send_string = enLength(content); // 添加报头
                    send(_sockfd, send_string.c_str(), send_string.size(), 0);

                    std::string package, text;
                    if (!recvPackage(_sockfd, inbuffer, &package))
                        continue;
                    if (!deLength(package, &text))
                        continue;
                    // text中的结果就是 "exitcode result"
                    Response resp;
                    resp.deserialize(text); // 反序列化

                    std::cout << "exitCode: " << resp.exitcode << std::endl;
                    std::cout << "result: " << resp.result << std::endl;
                }
            }
        }

        Request ParseLine(const std::string &line)
        {
            int status = 0; // 0 操作符之前 1 操作符 2 操作符之后
            int i = 0, size = line.size();
            char op;
            std::string left, right;
            while (i < size)
            {
                switch (status)
                {
                case 0:
                    if(!isdigit(line[i]))
                    {
                        // 遇到字符
                        op = line[i];
                        status = 1;
                    }
                    else left.push_back(line[i++]);
                    break;
                case 1:
                    i++;
                    status = 2;
                    break;
                case 2:
                    right.push_back(line[i++]);
                    break;
                }
            }
            return Request(std::stoi(left), std::stoi(right), op);
        }

        ~tcpClient()
        {
            if (_sockfd >= 0)
                close(_sockfd); // 使用完关闭,防止文件描述符泄露(当然这里也可以不写,当进程结束之后一切资源都将被回收)
        }

    private:
        uint16_t _serverPort;
        std::string _serverIP;
        int _sockfd;
    };

} // namespace Client
/*calClient.cc*/
#include <memory>
#include <string>

#include "calClient.hpp"
using namespace Client;

static void Usage(const char *proc)
{
    std::cout << "\n\tUsage:" << proc << " server_ip server_port\n";
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string IP = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<tcpClient> tclt(new tcpClient(port, IP));
    tclt->initClient();
    tclt->run();

    return 0;
}
/*log.hpp*/
#include <unistd.h>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

// 这里是日志等级对应的宏
#define DEBUG (1 << 0)
#define NORMAL (1 << 1)
#define WARNING (1 << 2)
#define ERROR (1 << 3)
#define FATAL (1 << 4)

#define NUM 1024 // 日志行缓冲区大小
#define LOG_NORMAL "log.normal" // 日志存放的文件名
#define LOG_ERR    "log.error"

const char *logLevel(int level) // 把日志等级转变为对应的字符串
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return "UNKNOW";
    }
}
//[日志等级][时间][pid]日志内容
void logMessage(int level, const char *format, ...) // 核心调用
{
    char logprefix[NUM]; // 存放日志相关信息
    time_t now_ = time(nullptr);
    struct tm *now = localtime(&now_);
    snprintf(logprefix, sizeof(logprefix), "[%s][%d年%d月%d日%d时%d分%d秒][pid:%d]",
             logLevel(level), now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec, getpid());

    char logcontent[NUM];
    va_list arg; // 声明一个变量arg指向可变参数列表的对象
    va_start(arg, format); // 使用va_start宏来初始化arg,将它指向可变参数列表的起始位置。
    // format是可变参数列表中的最后一个固定参数,用于确定可变参数列表从何处开始
    vsnprintf(logcontent, sizeof(logcontent), format, arg); // 将可变参数列表中的数据格式化为字符串,并将结果存储到logcontent中

    FILE *log =  fopen(LOG_NORMAL, "a");
    FILE *err = fopen(LOG_ERR, "a");
    if(log != nullptr && err != nullptr)
    {
        FILE *curr = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;
        if(level == ERROR || level == FATAL) curr = err;
        if(curr) fprintf(curr, "%s%s\n", logprefix, logcontent);

        fclose(log);
        fclose(err);
    }
}
cc=g++

.PHONY:all
all:Server Client

Server:calServer.cc
	$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11

Client:calClient.cc
	$(cc) -o $@ $^ -ljsoncpp -std=c++11

.PHONY:clean
clean:
	rm -f Server Client

.PHONY:cleanlog
cleanlog:
	rm -f log.error log.normal

image-20240227195945695


本节完…

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
OpenCV(Open Source Computer Vision Library)是一款开源的计算机视觉库,专门为图像和视频处理任务设计,广泛应用于学术研究、工业应用以及个人项目中。以下是关于OpenCV的详细介绍: 历史与发展 起源:OpenCV于1999年由英特尔公司发起,旨在促进计算机视觉技术的普及和商业化应用。该项目旨在创建一个易于使用、高效且跨平台的库,为开发者提供实现计算机视觉算法所需的基础工具。 社区与支持:随着时间的推移,OpenCV吸引了全球众多开发者和研究人员的参与,形成了活跃的社区。目前,OpenCV由非盈利组织OpenCV.org维护,并得到了全球开发者、研究机构以及企业的持续贡献和支持。 主要特点 跨平台:OpenCV支持多种操作系统,包括但不限于Windows、Linux、macOS、Android和iOS,确保代码能够在不同平台上无缝运行。 丰富的功能:库中包含了数千个优化过的函数,涵盖了计算机视觉领域的诸多方面,如图像处理(滤波、形态学操作、色彩空间转换等)、特征检测与描述(如SIFT、SURF、ORB等)、物体识别与检测(如Haar级联分类器、HOG、DNN等)、视频分析、相机校正、立体视觉、机器学习(SVM、KNN、决策树等)、深度学习(基于TensorFlow、PyTorch后端的模型加载与部署)等。 高效性能:OpenCV代码经过高度优化,能够利用多核CPU、GPU以及特定硬件加速(如Intel IPP、OpenCL等),实现高速图像处理和实时计算机视觉应用。 多语言支持:尽管OpenCV主要使用C++编写,但它提供了丰富的API绑定,支持包括C、Python、Java、MATLAB、JavaScript等多种编程语言,方便不同领域的开发者使用。 开源与免费:OpenCV遵循BSD开源许可证发布,用户可以免费下载、使用、修改和分发库及其源代码,无需担心版权问题。 架构与核心模块 OpenCV的架构围绕核心模块构建,这些模块提供了不同层次的功能: Core:包含基本的数据结构(如cv::Mat用于图像存储和操作)、基本的图像和矩阵操作、数学函数、文件I/O等底层功能。 ImgProc:提供图像预处理、滤波、几何变换、形态学操作、直方图计算、轮廓发现与分析等图像处理功能。 HighGui:提供图形用户界面(GUI)支持,如图像和视频的显示、用户交互(如鼠标事件处理)以及简单的窗口管理。 VideoIO:负责视频的读写操作,支持多种视频格式和捕获设备。 Objdetect:包含预训练的对象检测模型(如Haar级联分类器用于人脸检测)。 Features2D:提供特征点检测(如SIFT、ORB)与描述符计算、特征匹配与对应关系估计等功能。 Calib3d:用于相机标定、立体视觉、多视图几何等问题。 ML:包含传统机器学习算法,如支持向量机(SVM)、K近邻(KNN)、决策树等。 DNN:深度神经网络模块,支持导入和运行预训练的深度学习模型,如卷积神经网络(CNN)。
卷积神经网络(Convolutional Neural Networks, CNNs 或 ConvNets)是一类深度神经网络,特别擅长处理图像相关的机器学习和深度学习任务。它们的名称来源于网络中使用了一种叫做卷积的数学运算。以下是卷积神经网络的一些关键组件和特性: 卷积层(Convolutional Layer): 卷积层是CNN的核心组件。它们通过一组可学习的滤波器(或称为卷积核、卷积器)在输入图像(或上一层的输出特征图)上滑动来工作。 滤波器和图像之间的卷积操作生成输出特征图,该特征图反映了滤波器所捕捉的局部图像特性(如边缘、角点等)。 通过使用多个滤波器,卷积层可以提取输入图像中的多种特征。 激活函数(Activation Function): 在卷积操作之后,通常会应用一个激活函数(如ReLU、Sigmoid或tanh)来增加网络的非线性。 池化层(Pooling Layer): 池化层通常位于卷积层之后,用于降低特征图的维度(空间尺寸),减少计算量和参数数量,同时保持特征的空间层次结构。 常见的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling)。 全连接层(Fully Connected Layer): 在CNN的末端,通常会有几层全连接层(也称为密集层或线性层)。这些层中的每个神经元都与前一层的所有神经元连接。 全连接层通常用于对提取的特征进行分类或回归。 训练过程: CNN的训练过程与其他深度学习模型类似,通过反向传播算法和梯度下降(或其变种)来优化网络参数(如滤波器权重和偏置)。 训练数据通常被分为多个批次(mini-batches),并在每个批次上迭代更新网络参数。 应用: CNN在计算机视觉领域有着广泛的应用,包括图像分类、目标检测、图像分割、人脸识别等。 它们也已被扩展到处理其他类型的数据,如文本(通过卷积一维序列)和音频(通过卷积时间序列)。 随着深度学习技术的发展,卷积神经网络的结构和设计也在不断演变,出现了许多新的变体和改进,如残差网络(ResNet)、深度卷积生成对抗网络(DCGAN)等。
计算机网络(谢希仁第五版)课后答案 第一章 概述 1-01 计算机网络向用户可以提供那些服务? 答: 连通性和共享 1-02 简述分组交换的要点。 答:(1)报文分组,加首部 (2)经路由器储存转发 (3)在目的地合并 1-03 试从多个方面比较电路交换、报文交换和分组交换的主要优缺点。 答:(1)电路交换:端对端通信质量因约定了通信资源获得可靠保障,对连续传送大量数据效率高。 (2)报文交换:无须预约传输带宽,动态逐段利用传输带宽对突发式数据通信效率高,通信迅速。 (3)分组交换:具有报文交换之高效、迅速的要点,且各分组小,路由灵活,网络生存性能好。 1-04 为什么说因特网是自印刷术以来人类通信方面最大的变革? 答: 融合其他通信网络,在信息化过程中起核心作用,提供最好的连通性和信息共享,第一次提供了各种媒体形式的实时交互能力。 1-05 因特网的发展大致分为哪几个阶段?请指出这几个阶段的主要特点。 答:从单个网络APPANET向互联网发展;TCP/IP协议的初步成型   建成三级结构的Internet;分为主干网、地区网和校园网;   形成多层次ISP结构的Internet;ISP首次出现。 ...... 第二章 物理层 2-01 物理层要解决哪些问题?物理层的主要特点是什么? 答:物理层要解决的主要问题: (1)物理层要尽可能地屏蔽掉物理设备和传输媒体,通信手段的不同,使数据链路层感觉不到这些差异,只考虑完成本层的协议和服务。 (2)给其服务用户(数据链路层)在一条物理的传输媒体上传送和接收比特流(一般为串行按顺序传输的比特流)的能力,为此,物理层应该解决物理连接的建立、维持和释放问题。 (3)在两个相邻系统之间唯一地标识数据电路 物理层的主要特点: (1)由于在OSI之前,许多物理规程或协议已经制定出来了,而且在数据通信领域中,这些物理规程已被许多商品化的设备所采用,加之,物理层协议涉及的范围广泛,所以至今没有按OSI的抽象模型制定一套新的物理层协议,而是沿用已存在的物理规程,将物理层确定为描述与传输媒体接口的机械,电气,功能和规程特性。 (2)由于物理连接的方式很多,传输媒体的种类也很多,因此,具体的物理协议相当复杂。 2-02 归层与协议有什么区别? 答:规程专指物理层协议 2-03 试给出数据通信系统的模型并说明其主要组成构建的作用。 答:源点:源点设备产生要传输的数据。源点又称为源站。 发送器:通常源点生成的数据要通过发送器编码后才能在传输系统中进行传输。 接收器:接收传输系统传送过来的信号,并将其转换为能够被目的设备处理的信息。 终点:终点设备从接收器获取传送过来的信息。终点又称为目的站 传输系统:信号物理通道 ...... 第六章 应用层 6-01 因特网的域名结构是怎么样的?它与目前的电话网的号码结构有何异同之处? 答: (1)域名的结构由标号序列组成,各标号之间用点隔开: &hellip; . 三级域名 . 二级域名 . 顶级域名 各标号分别代表不同级别的域名。 (2)电话号码分为国家号结构分为(中国 +86)、区号、本机号。 6-02 域名系统的主要功能是什么?域名系统中的本地域名服务器、根域名服务器、顶级域名服务器以及权限域名权服务器有何区别? 答: 域名系统的主要功能:将域名解析为主机能识别的IP地址。 因特网上的域名服务器系统也是按照域名的层次来安排的。每一个域名服务器都只对域名体系中的一部分进行管辖。共有三种不同类型的域名服务器。即本地域名服务器、根域名服务器、授权域名服务器。当一个本地域名服务器不能立即回答某个主机的查询时,该本地域名服务器就以DNS客户的身份向某一个根域名服务器查询。若根域名服务器有被查询主机的信息,就发送DNS回答报文给本地域名服务器,然后本地域名服务器再回答发起查询的主机。但当根域名服务器没有被查询的主机的信息时,它一定知道某个保存有被查询的主机名字映射的授权域名服务器的IP地址。通常根域名服务器用来管辖顶级域。根域名服务器并不直接对顶级域下面所属的所有的域名进行转换,但它一定能够找到下面的所有二级域名的域名服务器。每一个主机都必须在授权域名服务器处注册登记。通常,一个主机的授权域名服务器就是它的主机ISP的一个域名服务器。授权域名服务器总是能够将其管辖的主机名转换为该主机的IP地址。 因特网允许各个单位根据本单位的具体情况将本域名划分为若干个域名服务器管辖区。一般就在各管辖区中设置相应的授权域名服务器。 6-03 举例说明域名转换的过程。域名服务器中的高速缓存的作用是什么? 答: (1)把不方便记忆的IP地址转换为方便记忆的域名地址。 (2)作用:可大大减轻根域名服务器的负荷,使因特网上的 DNS 查询请求和回答报文的数量大为减少。 6-04 设想有一天整个因特网的DNS系统都瘫痪了(这种情况不大会出现),试问还可以给朋友发送电子邮件吗? 答:不能;
【资源说明】 基于VMD-Attention-LSTM的时间序列预测模型python源码+模型+数据集+详细代码注释.zip 基于VMD-Attention-LSTM的时间序列预测模型(代码仅使用了一个较小数据集的训练及预测,内含使用使用逻辑,适合初学者观看,模型结构是可行的,有能力的请尝试使用更大的数据集训练) 根据LSTM层的需求,输入的数据应该为 [送入样本数, 循环核时间展开步数, 每个时间步输入特征个数] ,循环核时间展开步数我在代码中设计为使用前30天的数据,预测出第31天的数据,以此类推,每个时间步的输入特征个数我在代码中的设计为将当天的分解后特征每个时间段仅对应其中五个原数据的VMD的分解特征,经过实验当以所有时间原数据分解后的特征作为特征输入进网络时,数据量会被压缩的过小,导致网络严重的过拟合问题,当我们输入当前对应其五个原数据的VMD的分解特征时,对下推移并不会对结果产生影响,且模型过拟合问题得到巨大的缓解。 最后将数据分为训练集、测试集,对应的形状为(840,30,15)、(205,30,15)送入网络即可。 二、模型设计 模型设计在models下的vmd_attention_lstm下,模型分为两个部分,Attention层attention_3d_block 为点积注意力模块,Attention_lstm则为最终模型结构;其中模型结构中使用了两个128单元的LSTM层,一层Attention_LSTM组合,一层展平层及一层全连接输出层,并为了防止过拟合使用了Dropout层其参数为0.5,已经尽可能不影响输出的正则化函数;其中输入为(送入样本数,时间步,VMD分解后的特征),以此构成VMD-Attention-LSTM模型,模型结构图如下: 三、模型训练 模型训练上,经过测试128个LSTM神经元数量是个好的选择,学习率使用1e-4,Batch Size为128,使用CallBack函数返回其最优模型权重参数,使用Adam优化器及Huber损失函数,因为数据量小,我们使用500次训练迭代次数,取得其中最好的模型权重。 四、模型结果 最后,设计出预测应用,读取模型及保存后的权重信息;获取湖北原数据对数据进行预处理后的后100个时间点的数据输入预测模型,最后得到模型预测结果图: 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载使用,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!
计算机应用基础 一、名词解释 1、信息:对事物运动状态和特征的描述,而数据是载荷信息的物理符号。 2、管理信息:经过加工处理后对企业生产经营活动有影响的数据。 3、信息间的递归定义:管理数据和信息之间的区别是相对的,一个系统或一次处理所输 出的信息,可能是另一个系统或另一次处理的原始数据;低层决策所用的信息又可以成 为加工处理高一层决策所需信息的数据,这就是信息间的递归定义。 4、信息反馈:控制物流的输入信息作用于受控对象后,把产生的结果信息返回到输入端 ,并对信息再输入发生影响的过程。而上述作用于受控对象后的结果信息称为反馈信息 。 5、DSS:在半结构化决策活动过程中,通过人机对话,向决策者提供信息,协助决策者 发现和分析问题、探索决策方案,评价、预测和选择方案,以提高决策有效性的一种以 计算机为手段的信息系统。 6、GDSS:支持一群决策者为获得有效决策结果的计算机辅助系统。 7、智能支持系统:将人工智能技术引入决策支持系统而形成的一种信息系统。 8、制造资源计划(Manufacturing Resource Planning,MRPII)系统:COPICS是美国IBM公司开发的适用于各类制造业工厂的管理信 息系统,也是最早推出的MRPII商品化软件。 9、企业资源计划(Enterprise Resource Planning,ERP):根据计算机和网络技术的发展趋势和企业对供应链管理的需要,描绘 出一整套企业管理系统体系标准,其实质是在MRPII基础上,适应全球市场竞争供应链管 理的需求,对企业资源全面管理的思想。 10、企业信息化:企业利用现代信息技术,通过对信息资源的不断深入开发和广泛利用 ,不断提高生产、经营、管理、决策的效率和效益,进而提高企业经济效益、增强企业 竞争力的过程。 11、计算机集成制造系统(Computer Integreted Manufacturing System,CIMS):企业生产过程的自动化、智能化与企业管理决策的网络化、智能化两 个方面的结合,组成计算机集成制造系统。 12、企业业务流程重组(Business Process Reengineering,BPR):对企业的流程进行根本的再思考和彻底的再设计,以求得企业 的成本、质量、服务和速度等关键经营绩效指标有巨大的提高。 13、供应链管理:通过信息流、物流、资金流,将供应商、制造商、分销商、零售商直 到最终用户连成一个整体的管理模式。 14、虚拟企业:具有企业功能,但在企业体内没有执行这些功能的实体组织的企业。 15、企业组织的虚拟化:一是企业内部的虚拟化;二是企业组织之间的虚拟化。 16、电子商务(Electronic Commerce,EC):对整个贸易活动实现电子化。即交易各方通过计算机和通信网络进行 信息的发布、传递、存储、统计,以电子交易方式而不是通过纸介质信息交换或直接面 谈方式进行商业交易。 17、计算机软件:计算机程序、程序所使用的数据以及有关的文档资料的集合。 18、系统软件:直接控制和协调计算机、通信设备及其他外部设备的软件。 19、操作系统:控制和管理计算机硬件、软件资源,合理组织计算机工作,并为用户使 用计算机提供服务的软件。 20、数据通信:在收发站之间传送这些二进制代码序列的过程。 21、模拟通信系统:传递的信号为模拟信号,在时间和幅度取值上都是连续的。 22、数字通信系统:传递的信号为数字信号,在时间上是离散的,在幅度取值上是经过 量化的。 23、基本频带:使用数字信号传输数据,终端设备将数字信号转变成脉冲电信号时,这 种原始矩形脉冲信号固有的频带叫做基本频带,简称为基带。 24、调制:把数字信号转换为模拟信号的过程叫做调制 25、解调:将模拟信号还原为数字信号的过程叫做解调 26、通信协议:在通信过程中,通信双方都必须遵守的规则和约定。 27、企业内部网(Intranet):一个企业为实现内部管理和通信而建立的独立网络。 28、企业外部网(extranet):企业内部网对企业外部特定用户的安全延伸。 29、数据库(Data Base,DB):以一定的方式将相关数据组织在一起并存储在外存储器上所形成的、能为 多个用户共享的、与应用程序彼此独立的一组相互关联的数据集合。 30、数据库管理系统:帮助用户建立、使用和管理数据库的软件系统,简称为DBMS(Da ta Base Management System)。 31、数据库系统(Data Base System):以计算机系统为基础,以数据库方式管理大量共享数据的综合系统。 32、模式:描述逻辑结构的称为模式(或概念模式、逻辑模式) ,它是数据库数据的完整表示,是所有用户的公共数据视图。 33、实例:模式的一组值称为模式的一个实例。 34、模型:对现实世

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌云志.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值