深入了解linux网络—— 自定义协议(上)

序列化和反序列化

我们知道,协议是一种约定;且在调用soccket相关通信接口时,都是以字符串的形式发送信息。

如果,要传输一些结构化数据呢?协议也是双方约定好的一种结构化数据

例如,现在要实现一个网络版本的计算器,客户端就要将要计算的数字和运算符发送给服务端,由服务端处理完毕之后再返回给客户端。

在客户端就势必会存在Request结构化字段,其中存储在要运算的数字xy和运算符oper

而客户端要发送一个类似于1+1的字符串给服务端,为了保证服务端在接收到消息字符串时知道如何去处理;

就要做好约定:字符串中存在两个操作数,两个数字之间存在一个操作符,操作符可能是+-*/

这里在客户端和服务端就存在结构化字段RequestResponce

在发送信息时将结构化字段转化为字符串信息。在接受到字符串信息后,也能将字符串转化为结构化数据。

序列化:将结构化信息转化为字符串信息

反序列化:将字符串信息转化为结构化信息

这里无论如何去实现,只要保证一端发送的数据,在另一端能够正确的进行解析即可。

而这种约定,就是 应用层协议

所以,在协议当中就势必要存在序列化和反序列化的相关方法

理解readwriterecvsendTCP支持全双工

我们知道TCP在进行通信时是支持全双工的(可以同时读写)

读写相关接口readwriterecvsend都是支持全双工的;如何理解呢?

  • 这里,在任何一台主机上,TCP连接既有发送缓冲区,也有接受缓冲区;所以就支持全双工(发送信息的同时,也可以接受信息)
  • writesend接口,本质上就是将数据拷贝到TCP的发送缓冲区中;而readrecv接口本质上就是从TCP的接受缓冲区中将数据拷贝到内核中。
  • 对于数据什么时候发送、发送多少、出错了怎么办都由TCP控制;TCP传输控制协议。

那也就是说,我们之前使用的readwrite接口都是将数据交给了操作系统,也都是从操作系统中读取数据。

我们找直到TCP是面向字节流的,而之前的文件也是面向字节流的。

之前在使用writeread进行文件读写时,写端可以调用了多次write,而读端可能一次调用read就将写端写的所有信息都读取出来了;也可以写端写了一半的数据被读取上来了。

所以,协议不仅要提供序列化和反序列化的方法;还要保证读取到的报文的完整性

socket封装

这里简单对socket进行封装,使用模版设计模式:

这里设计一个基类Socket,其中包含纯虚函数:对socketbindconnect等的封装。

而基类中还存在CreateTcpServerSocket方法,其中调用对socketbind等封装好的纯虚方法。

cpp 复制代码
class Socket
{
public:
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie() = 0;
    virtual void ListenOrDie() = 0;
    virtual void AcceptOrDie() = 0;
public:
    void CreateTcpServerSocket()
    {
        SocketOrDie();
        BindOrDie();
        ListenOrDie();
    }
};

这里只罗列出了部分方法,在后续实现中进一步完善其中方法。

有了Socket,现在就要实现TcpSocket,而TcpSocket类就要继承Socket类,实现Socket中的纯虚方法。

Tcp创建套接字:socketbindlisten这里就不详细介绍了。

cpp 复制代码
class TcpSocket : public Socket
{
public:
    void SocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(Level::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
    }
    void BindOrDie(int port) override
    {
        InetAddr addr(port);
        int n = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());
        if (n < 0)
        {
            LOG(Level::FATAL) << "bind error";
            exit(SOCKET_ERR);
        }
        LOG(Level::DEBUG) << "bind success, sockfd : " << _sockfd;
    }
    void ListenOrDie(int backlog) override
    {
        int n = listen(_sockfd, backlog);
        if (n < 0)
        {
            LOG(Level::FATAL) << "listen error";
            exit(SOCKET_ERR);
        }
        LOG(Level::DEBUG) << "listen success, sockfd : " << _sockfd;
    }
private:
    int _sockfd;
};

要绑定端口号,服务端就只需要端口号,这里端口号就通过参数传递;

listen的第二个参数backlog,这里也设置也可以通过参数传递,且也设置了缺省参数。

这些参数都由调用用CreateTcpServerSocket来传递。

cpp 复制代码
//Socket类
class Socket
{
protected:
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(int port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual int AcceptOrDie() = 0;
public:
    void CreateTcpServerSocket(int port, int backlog = 6)
    {
        SocketOrDie();
        BindOrDie(port);
        ListenOrDie(backlog);
    }
};

有了上述这些,服务端就只需要调用CreateTcpServerSocket,传递端口号和backlog(可以不传);即可创建套接字、绑定端口号和设置监听状态。

TcpServer封装实现

有了上述实现的TcpSocket,现在先来完善一点TcpServer

对于TcpServer成员,这里就设置成智能指针对象;基类Socket智能指针执行派生类对象。

对于TcpServer构造函数,只需要调用Socket类中的CreateTcpSeverSocket方法将端口号传递进去即可。

cpp 复制代码
class TcpServer
{
public:
    TcpServer() {}
    TcpServer(int port) : _socket(std::make_unique<TcpSocket>())
    {
        _socket->CreateTcpServerSocket(port);
    }

private:
    std::unique_ptr<Socket> _socket;
};

这里简单测试一下,创建TcpServer对象,然后程序休眠,查看一下日志和listen状态即可。

可以看到,创建套接字、绑定端口号和设置监听状态都是成功的。

那现在就要让服务器运行起来,就要有accept获取连接请求。

accept封装

TcpSocket实现AcceptOrDie,对accept的封装。(AcceptOeDie返回值暂时设置为int

accept除了获取连接请求外,还会获取对方的struct sockaddr_in和长度,这里就通过输出性参数见对方的sockaddr传递出去(这里使用封装好的InetAddr即可

cpp 复制代码
//Socket
class Socket
{
protected:
    virtual int AcceptOrDie(InetAddr *addr) = 0;
public:
};

class TcpSocket : public Socket
{
public:
    int AcceptOrDie(InetAddr *addr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (fd < 0)
        {
            LOG(Level::FATAL) << "accept error";
            return -1;
        }
        LOG(Level::DEBUG) << "accept success";
       	addr->Set(peer);
        return fd;
    }
private:
    int _sockfd;
};

这里如果获取连接请求失败,就直接返回-1,由调用方去处理accept的情况。

上述中返回的是accept返回的,用来通信的文件描述符;

但是这里我们都对socket进行了封装,这里就可以直接返回一个std::shared_ptr<TcpSocket>的智能指针对象;

这样在调用读写操作时,就可以面向对象式调用。(统一化,TcpServer包含的就是指向Socket的智能指针对象)

cpp 复制代码
//Socket
class Socket
{
protected:
    // virtual int AcceptOrDie(InetAddr *addr) = 0;
    virtual std::shared_ptr<Socket> AcceptOrDie(InetAddr *addr) = 0;
public:
};
class TcpSocket : public Socket
{
public:
    TcpSocket()
    {
    }
    TcpSocket(int fd) : _sockfd(fd) {}
    std::shared_ptr<Socket> AcceptOrDie(InetAddr *addr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (fd < 0)
        {
            LOG(Level::FATAL) << "accept error";
            return nullptr;
        }
        LOG(Level::DEBUG) << "accept success";
        addr->Set(peer);
        return std::make_shared<TcpSocket>(fd);
    }

private:
    int _sockfd;
};

实现了对accept的封装,那在TcpServer中,运行时,只需要通过智能指针对象调用AcceptOrDie既可以获得用来通信的TcpSocket

获取连接请求成功之后,就要进行通信,这里使用多进程版;

子进程创建子进程,让孙子进程去执行。

子进程和父进程都要关闭不要的文件描述符,那对应的Socket就还需要提供一个Close方法来关闭文件描述符。

对于孙子进程如何进行服务:

这里,就通过回调函数,由上层去决定如何进行服务。(这里要自定义协议,就先使用回调函数)

cpp 复制代码
using func_t = std::function<void(std::shared_ptr<Socket> fd, InetAddr &client)>;
class TcpServer
{
public:
    TcpServer() {}
    TcpServer(int port, func_t func) : _socket(std::make_unique<TcpSocket>()), _func(func)
    {
        _socket->CreateTcpServerSocket(port);
    }
    void Start()
    {
        while (true)
        {
            InetAddr peer;
            auto fd = _socket->AcceptOrDie(&peer);
            if (fd == nullptr)
            {
                exit(ACCEPT_ERR);
            }
            // 通信
            int id = fork();
            if (id < 0)
            {
                LOG(Level::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if (id == 0)
            {
                _socket->Close();
                if (fork() > 0)
                    exit(OK);
                _func(fd, peer);
            }
            else
            {
                // 父进程
                fd->Close();
                waitpid(id, nullptr, 0);
            }
        }
    }

private:
    std::unique_ptr<Socket> _socket;
    func_t _func;
};//Socket
class Socket
{
public:
    virtual void Close() = 0;
};
class TcpSocket : public Socket
{
public:
    void Close() override
    {
        close(_sockfd);
    }
private:
    int _sockfd;
};
//TcpServer
using func_t = std::function<void(std::shared_ptr<Socket> fd, InetAddr &client)>;
class TcpServer
{
public:
    TcpServer() {}
    TcpServer(int port, func_t func) : _socket(std::make_unique<TcpSocket>()), _func(func)
    {
        _socket->CreateTcpServerSocket(port);
    }
    void Start()
    {
        while (true)
        {
            InetAddr peer;
            auto fd = _socket->AcceptOrDie(&peer);
            if (fd == nullptr)
            {
                exit(ACCEPT_ERR);
            }
            // 通信
            int id = fork();
            if (id < 0)
            {
                LOG(Level::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if (id == 0)
            {
                _socket->Close();
                if (fork() > 0)
                    exit(OK);
                _func(fd, peer);
            }
            else
            {
                // 父进程
                fd->Close();
                waitpid(id, nullptr, 0);
            }
        }
    }
private:
    std::unique_ptr<Socket> _socket;
    func_t _func;
};

这样,服务器在获取连接请求成功后,就让孙子进程(孤儿进程),去完成服务,父进程继续等待连接请求。

至于如何进行服务,就由上层传递的回调函数来决定。

自定义协议

这里要实现网页版计算器,我们就要制定相关协议(结构化数据、序列化反序列化等等)

要自定义协议,首先要有结构化数据,这里定义Request(请求结构化字段)、Responce(结果结构化字段)以及协议字段protocol

cpp 复制代码
class Request
{
public:
    Request() {}
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) {}

private:
    int _x;
    int _y;
    char _oper;
};

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

private:
    int _result; // 结果
    int _code;   // 标识计算是否出错
};
class protocol
{
    public:
};

到这里,本篇文章大致内容就结束了

简答总结:

  • 序列化和反序列化
  • 理解TCP面向字节流,支持全双工。
  • 协议需要提供对应的序列化和反序列化方法、并且要保证读取到报文的完整性。
  • socket的封装、TcpServer的封装实现
  • 自定义协议 :结构化数据ResquestResponce

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
bcgbsh7 小时前
Linux开机启动脚本(cron 的 @reboot 特性)
linux·cron
听风吹雨yu7 小时前
RK3588从数据集到训练到部署YoloV8
linux·yolo·开源·rk3588·rknn
野犬寒鸦7 小时前
从零起步学习Redis || 第十一章:主从切换时的哨兵机制如何实现及项目实战
java·服务器·数据库·redis·后端·缓存
要做朋鱼燕7 小时前
【AES加密专题】1.AES的原理详解和加密过程
运维·网络·密码学·c·加密·aes·嵌入式工具
iconball8 小时前
个人用云计算学习笔记 --19 (MariaDB服务器)
linux·运维·笔记·学习·云计算
Lynnxiaowen8 小时前
今天我们开始学习python3编程之python基础
linux·运维·python·学习
Chandler248 小时前
一图掌握 操作系统 核心要点
linux·windows·后端·系统
破坏的艺术9 小时前
DNS 加密协议对比:DoT、DoH、DoQ
网络·dns
dragoooon349 小时前
[Linux系统编程——Lesson6.进程切换与调度]
linux·运维·服务器