为了让我们的代码结构划分更加清晰,我们先对部分功能进行封装

Socket封装

对Socket中经常需要使用到的接口进行封装。 采用模板方法模式进行设计,通过子类继承并重写父类方法来实现。

Socket.hpp

代码语言:javascript

AI代码解释

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;

const int BACK = 16;

namespace SocketModule
{
    // 模板方法模式
    // 基类大部分函数都是虚函数
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t prot) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void ConnectOrDie(InetAddr &addr) = 0;
        virtual ssize_t Recv(string &buffer) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual void Close() = 0;

        void BuildTcpSocket(uint16_t prot, int backlog = BACK)
        {
            SocketOrDie();
            BindOrDie(prot);
            ListenOrDie(backlog);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() {}

        TcpSocket(int fd)
            : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "socket error";
                exit(SOCK_ERR);
            }
        }

        void BindOrDie(uint16_t prot) override
        {
            InetAddr addr(prot);
            int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "bind error";
                exit(BIND_ERR);
            }
        }

        void ListenOrDie(int backlog) override
        {
            int ret = listen(_sockfd, backlog);
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "listen error";
                exit(LISTEN_ERR);
            }
        }

        // 客户端
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (fd < 0)
            {
                LOG(LogLevel::ERROR) << "accept error";
                exit(ACCP_ERR);
            }

            // 将客户端信息带出
            InetAddr addr(peer);
            *client = addr;
            // accept成功后返回一个新的套接字,用于服务
            // 可以直接返回fd,也可以返回新对象(的指针)
            return std::make_shared<TcpSocket>(fd);
        }

        void ConnectOrDie(InetAddr &addr) override
        {
            int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "connect error";
                exit(CONN_ERR);
            }
        }

        ssize_t Recv(string &buffer) override
        {
            char str[1024];
            ssize_t n = recv(_sockfd, str, sizeof(str), 0);
            if (n)
            {
                // 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
                // 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
                // 所以,我们必须自己显示的加上!
                // 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
                // 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
                // 最终导致json串解析的时候失败!

                str[n] = 0;
                buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
            }
            return n;
        }

        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }

        void Close() override
        {
            close(_sockfd);
        }

        int Sockfd()
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}

协议定制

协议定制主要分为:序列化与反序列化、编码与解码

序列化与反序列化

在之前的文章中,我们曾讲过:协议在实现上,其本质是结构体!大家都遵守协议,那么涉及网络通信的数据都必须以结构体的形式存放!

而序列化,就是将“结构体”变成“字节流”!虽然协议的本质是结构体,网络通信的数据都是用结构体存放的,但是想要把数据通过网络发送给对方,其数据就必须是“字节流”

将结构体转化为“字节流”有两种主流流派:

流派一:

直接发送结构体本身!既发送二进程数据。

很高效,很简单,速度快!但是!这就意味着发送方和接收方都必须是C++环境,并且结构体的内布局必须一模一样!

不推荐!

流派二:

结构化数据文本化!

直接将结构体的数据按照一定规则,进行文本化。仅发送文本、接收文本。接收的文件再按照规则进行反序列化!再次得到结构化数据。

推荐!

我们这里采用结构化数据文本化!那么具体改如何做呢?通过Json库实现!

认识JsonCpp库

JsonCpp库是C++中最通用的JSON处理库之一。主要功能就是进行序列化、反序列化。

序列化 (Serialization):将结构化数据,打包成一个标准的JSON字符串

反序列化 (Deserialization):把收到是JSON字符串,解析回结构化数据

使用样例:

由于JsonCpp库的第三方库,编译时一定要带路径:g++ -o @^ -l jsoncpp

代码语言:javascript

AI代码解释

#include <iostream>
#include <unistd.h>
#include <string>
#include <jsoncpp/json/json.h> //头文件注意!!
using namespace std;

// 序列化
int main()
{
    // Json::Value 对象
    Json::Value root;
    root["one"] = "yi";
    root["two"] = "er";
    root["three"] = "san";

    string str = root.toStyledString();
    cout << str << endl;

    // Json::FastWriter 对象,不添加额外空格、换行符
    Json::FastWriter fast;
    str = fast.write(root);
    cout << str << endl;

    // Json::StreamWriterBuilder
    Json::StreamWriterBuilder stream;
    std::unique_ptr<Json::StreamWriter> writer(stream.newStreamWriter());
    stringstream ss;
    writer->write(root, &ss);
    cout << ss.str() << endl;
}

代码语言:javascript

AI代码解释

hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
{
        "one" : "yi",
        "three" : "san",
        "two" : "er"
}

{"one":"yi","three":"san","two":"er"}

{
        "one" : "yi",
        "three" : "san",
        "two" : "er"
}

代码语言:javascript

AI代码解释

// 反序列化
int main()
{
    // string str = {"\" one ":\" yi ",\" three ":3,\" two ":" er \""};
    string str = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
    Json::Value root;
    Json::Reader read;
    read.parse(str, root);

    cout << root["name"].asString() << endl;
    cout << root["city"].asString() << endl;
    cout << root["age"].asInt() << endl;
}

代码语言:javascript

AI代码解释

hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
张三
北京
30

通过使用JsonCpp库,我们就可以实现序列化与反序列化了!

编码与解码
为什么要编码与解码

封包与解包是为了解决 TCP 的“粘包”和“半包”问题,明确消息的边界。 如果说序列化是为了让数据能“顺着网线爬过去”,那么编码就是为了让接收方知道“这一坨数据爬到哪里才算是个头”。

根本原因:TCP是面向字节流的,因此TCP发送数据,是不能像UDP一样保证其数据是完整的数据报。它的传输方式像流水一样,而不是像快递包裹一样。因为 TCP 这种“流水”特性,在接收端会发生两种灾难性的情况:粘包:当发送方快速的发送了多条消息时,在底层这多条数据会被全部交给发送缓冲区中,由OS负责进行发送!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐