计算机网络:基于TCP协议的自定义协议实现网络计算器功能

序言:上个博客我们基于UDP协议实现了聊天室的功能,我们介绍了socket,bind,sendto,recvform函数调用,这个博客我们将实现自定义上层应用层协议传输层协议我们采用TCP协议,我们将在这篇博客中介绍为什么TCP支持全双工通信,如何去反序列化和序列化,为什么需要反序列化和序列化,废话不多说让我们开始我们的内容!

如果没有看我上一篇博客的同学快去看吧!《计算机网络:UDP网络编程》

一、序列化和反序列化

1、概念

序列化 :将内存中的对象(Object)、数据结构(如字典、列表) 转换为可存储或可传输的格式(如字节流、字符串、文件等)的过程。

目的是将复杂的数据结构 "扁平化",以便于在网络中传输(如跨服务器、跨语言通信)或写入磁盘(如持久化存储)。
反序列化:将序列化后的字节流、字符串等格式还原为内存中原始的对象或数据结构的过程。化反序列化。

2、为什么需要序列化和反序列化?

在前面的博客中我们说过协议其实就是一个个的结构体,现在我们想一个问题如果我们把这些结构体直接发送到网络中对方可以接收到吗?答案是可以但可能无法正确的解析,因为我们客户端软件使用的软件和我们服务器端软件使用的语言等可能是不一样的,可能我们的服务器是C语言或者C++编写,而我们的客户端软件是用python,Java等语言编写的,它们的语法是不一样的对于这个结构体的解析也是不一样的,我们举一个具体的例子,例如结构体内存对齐可能就不一样,这样就导致我们的数据无法直接通信,这时就需要我们的序列化把数据变成大家都认识的样子发送给对方,对方再通过我们约定好的协议内容还原出来,这就叫做反序列化,这样我们就可以实现数据的跨平台传输。

3、Json

1、介绍

Jsoncpp 是⼀个⽤于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,⼴泛⽤于各种需要处理 JSON 数据的 C++ 项⽬中。

特性:

  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。

  2. 高性能:Jsoncpp 的性能经过优化,能够⾼效地处理⼤量 JSON 数据。

  3. 全面支持:⽀持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。

  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,⽅便开发者调试。

当使⽤Jsoncpp库进⾏JSON的序列化和反序列化时,确实存在不同的做法和⼯具类可供选择。以下是对Jsoncpp中序列化和反序列化操作的详细介绍:

2、安装

bash 复制代码
ls /usr/include/jsoncpp

我们可以先用这个指令查看我们是否已经安装过了Json,这个命令可以寻找再我们系统的头文件目录下是否包含了jsoncpp的头目录。

bash 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

这里有Ubuntu和Centos平台下的安装方法。

3、序列化

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <string>

int main()
{
    Json::Value root;
    root["name"] = "张三";
    root["age"] = "18";
    Json::FastWriter writer;
    std::string str = writer.write(root);
    std::cout << str << std::endl;
    return 0;
}

​Json的序列化比较简单,大体可以分为两个部分:创建Value,将值放入进去,再创建writer,将json里的值转换成字符串。

4、反序列化

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <string>

std::string Serialize()
{
    Json::Value root;
    root["name"] = "josn";
    root["age"] = "18";
    Json::FastWriter writer;
    std::string str = writer.write(root);
    return str;
}
void Deserialize(std::string &s)
{
   
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(s, root);
        std::string name = root["name"].asString();
        std::string age = root["age"].asString();
        std::cout << name << std::endl
                  << age << std::endl;
}
int main()
{
    std::string s = Serialize();
    Deserialize(s);
    return 0;
}

Josn的反序列化也是很简单的,总体也可以分为两个部分,第一是创建一个Value对象用来存放值,第二个是Reader对象用来解析字符串再把值从字符串中放入到Value对象中

注意:

在序列化的时候传入的值的类型要和反序列化时转化出来的值类型要相同,举个例子比如上面的代码"age"我们当时序列化的时候如果给的是"18",那么分序列化的时候将要std::string age = root["age"].asString();而不能是std::string age = root["age"].asInt();这样会导致Json抛异常,如果你想要接收的类型是int类型那你序列化的时候就要root["age"] = 18。

5、各种序列化和反序列化的优缺点

这里我们选择Json的原因也是因为我们第一次接触到序列化和反序列化需要一个更利于我们人去看的一种方式。

下面我们有一张图可以帮我们更好的理解序列化和反序列化:

二、TCP的全双工

什么是全双工?

在同一时刻既可以进行发数据又可以进行收数据,实现双向数据的并行传输,这种方式就是全双工。

为什么TCP可以进行全双工通信?

因为在底层TCP链接既有发送缓冲区,又有接受缓冲区,所以TCP可以在内核中可以在发消息的同时还可以收消息,实现了全双工通信。

read,write,recv,send的本质是什么?

当我们想把数据发送到网络上的时候调用write或者send,write和send会把数据直接通过网卡直接发送到网络上吗?答案是不是,因为操作系统才是软硬件的管理者,它不相信任何人,任何人想要调用底层的硬件都需要去经过操作系统之手,所以我们没有办法直接通过write和send直接将数据发送到网络上,其实调用write和send只是把数据写入到我们的发送缓冲区,至于什么时候发送,一次发多少这个不是由上层决定的是由内核决定的,换一句话来说这就是面向字节流式。

为什么TCP可以只通过一个文件描述符既可以向缓冲区中写也可以向缓冲区中读?

fd的本质其实就是一个下标,再详细一点是文件描述表的下标,这个文件描述符中的每个成员的类型都是struct file*类型指向的都是一个个的struct file类型,在这个struct file中连接了这两个缓冲区,所以才可以实现通过这一个文件描述符既可以去读又可以去写。

为什么当我们的接受缓冲区为空的时候read/recv会阻塞,当我们的发送缓冲区为满的时候send/write会阻塞?

因为我们的用户和内核形成了一个生产者消费者模型,交易场所就是我们的缓冲区,阻塞其实是生产者和消费者在进行同步。

理解上面的那些问题之后我们可以总结出出一个结论:主机通信的本质:就是把发送方的发送缓冲区中的数据拷贝到接受方的接受缓冲区中,换一句话来说,通信的本质其实就是拷贝。

我们应该如何去理解TCP是面向字节流?

TCP有发送缓冲区,上层将数据从上层拷贝到内核的缓冲区中,由内核根据网络环境和对方的接受能力去决定发送的大小,内核可能给对方发送一个,一个半,四个都是有可能的,所以它是面向字节流的并不像面向数据报的UDP一次发送一整个完整的报文,除此之外我们还有一个方法判断协议是面向字节流还是面向数据包,我们可以看收发次数是是否一样,这个方法的本质其实还是面向字节流发送可能一次发送多个数据报也有可能一个都不发送,对方接收数据包也有可能一次接收多个,所以才导致收发次数不相同。

三、TCP的粘包问题

上面我们说了TCP是面向字节流的,发送方可能一次发送多个报文,那对方收到这一堆报文如何去进行分解把一个个报文从这一堆报文中拿出来呢?这就需要我们自定义协议自己去解决。

我们如何去解决TCP粘包问题呢?

我们要设计一种结构可以上协议可以进行分离。

我们可以在每个字符之间用空格作字符间分隔符,/r/n作报文间分隔符,再在每个报文的前面添加报文长度,当对方收到报文的时候读取报文头部的报文长度字符就可以知道报文多长然后向后读取这么长的长度提取出整个报文,如果后面的长度不够的话,就暂时不去读取等到下一次从内核缓冲区内读取数据再去拆分出完整报文。

四、项目设计

下面我们要写的代码是一个实现网络计算器的功能,上层我们选择自定义协议的方式,传输层我们选择TCP协议的方式,因为我们是基于TCP协议实现的网络计算器所以我们在上层的协议中需要自定义协议去解决报文分离,报文的序列化和反序列化的功能。

1、上层功能的实现

这个部分我们是实现计算器的功能,我们希望自定义协议把报文处理完了之后,调用我们计算器的相关功能,处理之后再将结果给自定义协议层封装报文再去反序列化再向下交付。

cpp 复制代码
#pragma once
#include "Com.hpp"
#include "Protocol.hpp"

class Cal
{
public:
    Cal()
    {

    }
    ~Cal()
    {
        
    }
    Response Execute(Request &req)
    {
        Response res(0, 0);
        switch (req.GetOper())
        {
        case '+':
            res.SetResult(req.GetX() + req.GetY());
            break;
        case '-':
            res.SetResult(req.GetX() - req.GetY());
            break;
        case '/':
        {
            if (req.GetY() == 0)
            {
                res.SetCode(1);
                LOG(LogLevel::WARNNING) << "除0";
            }
            else
            {
                res.SetResult(req.GetX() / req.GetY());
            }
        }
        break;
        case '%':
        {
            if (req.GetY() == 0)
            {
                res.SetCode(2);
                LOG(LogLevel::WARNNING) << "模0";
            }
            else
            {
                res.SetResult(req.GetX() % req.GetY());
            }
        }
        break;
        default:
            res.SetCode(3);
            LOG(LogLevel::WARNNING) << "Unkowed oper";
            break;
        }
        return res;
    }
};

2、自定义协议

这个部分我们实现自定义协议,我们需要自定义协议具有序列化,反序列化功能,有从内核缓冲区中读取完整报文的能力,将完整报文读取上来之后进行反序列化处理之后交付给计算器部分处理,处理完再由自定义协议反序列化调用send写到内核缓冲区内。

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include "Log.hpp"
#include "Com.hpp"
#include <jsoncpp/json/json.h>
#include "InetAddr.hpp"
#include "Socket.hpp"

const static std::string sep = "/r/n";

class Request
{
    public:
    Request(int x , int y , char oper)
        :_x(x)
        ,_y(y)
        ,_oper(oper)
    {
    }
    Request()
    {

    }
    ~Request()
    {
    }
    std::string  Serialize()
    {
        Json::Value root;
        root["X"] = _x;
        root["Y"] = _y;
        root["Oper"] = _oper;


        Json::FastWriter writer;
        std::string json_str = writer.write(root);
        return json_str;
    }

    bool Deserialize(std::string in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in , root);
        if(ret)
        {
            _x = root["X"].asInt();
            _y = root["Y"].asInt();
            _oper = root["Oper"].asInt();
        }
        return ret;
    }

    void SetX(int x)
    {
        _x = x;
    }
    void SetY(int y)
    {
        _y = y;
    }
    void SetOper(char oper)
    {
        _oper = oper;
    }
    char GetOper()
    {
        return _oper;
    }
    int GetX()
    {
        return _x;
    }
    int GetY()
    {
        return _y;
    }
    private:
    int _x;
    int _y;
    char _oper;
};

class Response
{
    public:
    Response(int result ,int code)
        :_result(result)
        ,_code(code)
    {

    }
    Response()
    {

    }
    ~Response()
    {

    }

    std::string Serialize()
    {
        Json::Value root;
        root["Result"] = _result;
        root["Code"] = _code;


        Json::FastWriter writer;
        std::string json_str = writer.write(root);
        return json_str;
    }

    bool Deserialize(std::string in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ret = reader.parse(in , root);
        if(ret)
        {
            _result = root["Result"].asInt();
            _code = root["Code"].asInt();
        }
        return ret;
    }
    void SetResult(int result)
    {
        _result = result;
    }
    void SetCode(int code)
    {
        _code = code;
    }
    void ShowResult()
    {
        std::cout << _result <<"[" << _code << "]";
    }
    private:
    int _result;
    int _code;
}; 

using func_t = std::function<Response (Request&)>;

class Protocol
{
    public:
    Protocol()
    {

    }
    Protocol(func_t service)
        :_service(service)
    {

    }
    ~Protocol()
    {

    }

    std::string Encode(std::string& json_str)
    {
        std::string json_len_str = std::to_string(json_str.size());
        std::string package = json_len_str + sep + json_str + sep;

        return package;
    }

    bool Decode(std::string& str , std::string* package)
    {
        auto pos = str.find(sep);
        if(std::string::npos == pos)
        {
            //字符串中连报头都没有
            return false;
        }
        //字符串中至少有报头可以提取继续判断
        std::string package_len_str = str.substr(0 , pos);//报文的报头
        size_t json_len_int = std::stoi(package_len_str);//json串应该的长度

        size_t str_len = str.size();//传入的字符串的长度
        //50/r/n{json_str}/r/n
        size_t package_len_int = package_len_str.size() + 2*sep.size() + json_len_int;//报文的长度
        if(str_len < package_len_int)
        {
            return false;
        }
        //到这里字符串至少有一个完整的报文
        *package = str.substr(pos  + sep.size() , json_len_int);
        str.erase(0 , package_len_int );
        return true;
    }

    void GetRequest(std::shared_ptr<Socket> sock , InetAddr client)
    {
        std::string bufferqueue;
        while(true)
        {
            int n = sock->Recv(&bufferqueue);
            std::cout <<"Recv" <<std::endl;
            if(n > 0)
            {
                std::string package;
                while(Decode(bufferqueue , &package))
                {
                    //有完整的json串
                    //反序列化

                    Request req;
                    bool ret = req.Deserialize(package);
                    if(!ret)
                    {
                        LOG(LogLevel::ERROR) << "Request Deserialize failed ,IP:" << client.GetIP();
                        continue;
                    }
                    LOG(LogLevel::ERROR) << "Request Deserialize success";
                    //拿到json串调用上层处理
                    Response  res;
                    res = _service(req);
                    //上层处理完应该序列化
                    std::string json_str = res.Serialize();
                    //序列化完成之后应该去添加报头
                    std::string send_message = Encode(json_str);
                    //发送报文
                    std::cout << "走到了server send的地方" <<std::endl;
                    int send_t = sock->Send(send_message);
                    if(send_t < 0)
                    {
                        LOG(LogLevel::ERROR) << "send " << client.GetIP() << "failed";
                    }
                    else{
                        LOG(LogLevel::DEBUG) << "send " << client.GetIP() << "success";

                    }
                }
            }
            else if(0 == n)
            {
                LOG(LogLevel::DEBUG) << "client quit" ;
                break;
            }
            else
            {
                LOG(LogLevel::ERROR) << "server recv error";
                break;
            }
        }
    }

    bool GetResponse(std::string& str , std::shared_ptr<Socket>& sock, Response& res)
    {
        while(true)
        {
           int n = sock->Recv(&str);
           if(n > 0 )
           {
                std::string package;
                while(Decode(str , &package))
                {
                 
                    res.Deserialize(package);

                    
                }
                return true;
           }
           else if(0 == n)
           {
                LOG(LogLevel::DEBUG) << "server quit";
                break;
                
           }
           else
           {
                LOG(LogLevel::ERROR) << "client recv error";
                break;
           }
        }
        return false;
    }

    std::string BuildRequestMethod(int x , int y , char oper)
    {
        Request req;
        req.SetX(x);
        req.SetY(y);
        req.SetOper(oper);
        std::string json_str = req.Serialize();
        return Encode(json_str);
    }
    private:
    func_t _service;
};

3、服务器

这个部分,我们采用多进程的方式去处理请求,父进程用来去listen套接字,将连接好的套接字给子进程去处理相应的请求。

但我们面临的一个问题是子进程退出的时候需要父进程去阻塞式回收要不然会造成内存泄漏的问题,如果父进程要去回收的话将会阻塞等到子进程结束,我们有两个解决办法一个是将SIGCHLD信号设置为忽略,父进程将不会关心子进程的退出,子进程的资源由操作系统去回收,第二个是子进程在创建一个孙子进程将业务给孙子进程去处理,子进程退出父进程直接回收孙子进程的父进程没了就会变成孤儿进程被操作系统回收,而子进程直接退出也不会造成父进程的阻塞。

TcpServer.hpp

cpp 复制代码
#pragma once
#include "Com.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include <functional>
#include <memory>
#include <sys/types.h>
#include <sys/wait.h>
using server_func_t = std::function<void(std::shared_ptr<Socket> sock , InetAddr client)>;

class TcpServer
{
    public:
    TcpServer(uint16_t port,server_func_t func)
        :_func(func)
        ,_port(port)
        ,_listensockptr(std::make_unique<TcpSocket>())
        ,_isrunning(false)
    {
        _listensockptr->BuildTcpSocketMethod(port , 8);
    }
    ~TcpServer()
    {

    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            InetAddr client;
            auto sock = _listensockptr->Accept(&client); 
            if(sock == nullptr)
            {
                continue;
            }
            pid_t pid = fork();
            if(pid > 0)
            {
                //子进程
                if(fork() == 0)
                {
                    exit(OK);
                }
                //孙子进程
                _listensockptr->Close();
                _func(sock , client);
                sock->Close();
                exit(OK);
            }
            //父进程
            waitpid(pid , nullptr ,0);
            sock->Close();
        }
        _isrunning = false;
    }
    private:
    server_func_t _func;
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
};

TcpServer.cpp

cpp 复制代码
#include "Com.hpp"
#include <functional>
#include <memory>
#include <iostream>
#include "InetAddr.hpp"
#include "NetCal.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
void Usage()
{
    std::cout << "./TcpServer server_port" << std::endl;
}

int main(int argc , char* args[])
{
    if(argc != 2)
    {
        Usage();
        return 2;
    }

    uint16_t port = std::stoi(args[1]);
    InetAddr server(port);
    Cal cal;
    
    std::unique_ptr<Protocol> pro = std::make_unique<Protocol>([&cal](Request& req){
           return  cal.Execute(req);
    });

    std::unique_ptr<TcpServer> tcpsvr = std::make_unique<TcpServer>(server.GetPort() , [&pro](std::shared_ptr<Socket> sock , InetAddr client){
            pro->GetRequest(sock , client);
    });

    tcpsvr->Start();
    
    return 0;
}

4、客户端

cpp 复制代码
#include "Com.hpp"
#include <functional>
#include <memory>
#include <iostream>
#include "InetAddr.hpp"
#include "NetCal.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"

void Usage()
{
    std::cout <<"./TcpClient server_ip server_port" << std::endl;
}
void GetData(int& x , int& y, char& oper)
{
    std::cout << "Plase Enter#: " <<std::endl;
    std::cin >> x;
    std::cout << "Plase Enter#: " <<std::endl;
    std::cin >> y;
    std::cout << "Plase Enter#: " <<std::endl;
    std::cin >> oper;
}
int main(int argc , char* args[])
{
    if(argc != 3)
    {
        Usage();
        return 2;
    }

    InetAddr server(args[1] , std::stoi(args[2]));

    std::shared_ptr<Socket> sock =std::make_unique<TcpSocket>();
    sock->BuildTcpClientSocketMethod();
    sock->Connect(server);
    std::unique_ptr<Protocol> pro = std::make_unique<Protocol>();
    while(true)
    {
        int x,y;
        char oper;
        GetData(x , y , oper);

        std::string package = pro->BuildRequestMethod(x , y , oper);
        sock->Send(package);

        Response res;
        std::string str;
        pro->GetResponse(str, sock , res);//GetResponse这个方法还有问题
        res.ShowResult();
    }
    sock->Close();
    return 0;
}

下面有这个写代码的关系图,可以帮助大家更好的理解这些代码。

本篇关于Linux的文件理解与操作的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持纠正!!!

相关推荐
张人玉2 小时前
TCP 的三次握手和四次挥手
网络·tcp/ip·c#
qq_254674414 小时前
华为交换机上配置基于 IP 地址的 ACL
网络
宁雨桥4 小时前
WebSocket 核心 API 速查表
网络·websocket·网络协议
打不了嗝 ᥬ᭄5 小时前
【Linux】网络层协议
linux·网络·c++·网络协议·http
九河云6 小时前
华为云ECS与Flexus云服务器X实例:差异解析与选型指南
大数据·运维·服务器·网络·人工智能·华为云
头发还没掉光光7 小时前
Linux网络初始及网络通信基本原理
linux·运维·开发语言·网络·c++
七夜zippoe8 小时前
Ascend C流与任务管理实战:构建高效的异步计算管道
服务器·网络·算法
一叶飘零_sweeeet8 小时前
手写 RPC 框架
java·网络·网络协议·rpc
fei_sun14 小时前
【复习】计网每日一题1105大题---ARP、NAT、路由器、IP数据报、冲突域、广播域、100BASE-F、10BASE-T
网络