【计网】自定义协议与序列化(一) —— Socket封装于服务器端改写

news2024/11/27 10:48:10

🌎 应用层自定义协议与序列化


文章目录:

Tcp协议Socket编程

    应用层简介

    序列化和反序列化
      重新理解read/write/recv/send及tcp的全双工
      Socket封装
      服务器端改写


🚀应用层简介

  我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序, 都是在应用层。

  不论是Udp Socket编程还是Tcp Socket编程,所传的数据都是字符串类型的的数据,但是如果我们想要传输结构化的数据呢?什么是结构化的数据?其实在我们第一次说计算机网络时就已经提到过,结构化的数据就是协议,其本质就是 双方约定好的结构化的数据

  比如,我们如果要实现一个网络版的计算器,我们需要客户把待计算的两个数发过去,由服务器进行计算,最后把结果返回给客户端。如果我们依旧采用传统的Socket编程,不论是Tcp还是Udp Socket编程,都无法保证所收到的数据是完整的,比如:客户端要发送 123 * 123,但是服务器此时缓冲区快满了,只能收到 123 * 1,这个时候服务器端就会以 123 * 1作为客户端的请求进行处理,如此以来就会导致客户端想得到的结果不匹配,更有甚者,剩下的23后一批发来会不会导致新的客户端数据出现问题呢?

  所以这里我们准备了两套方案:

方案一

  客户端发送一个形如"1+1"的字符串,这个字符串中有两个操作数,都是整形,两个数字之间会有一个字符是运算符,运算符只能是 + ,数字和运算符之间没有空格

方案二

  定义结构体来表示我们需要交互的信息,发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体。这个过程我们叫做 序列化 反序列化


🚀序列化和反序列化

  首先先简单理解一下什么是序列化与反序列化,我们可以通过下图简单了解:

在这里插入图片描述

  仅仅是上面一张图我认为理解还是不够的,要更好的理解序列化和反序列化需要从下面入手:

✈️重新理解read/write/recv/send及tcp的全双工

  在重新理解这些接口之前,我们先来回顾一下进程向发消息到磁盘的过程:

在这里插入图片描述

  首先,用户需要发送消息,那么OS就会从文件描述符表中把3号文件描述符通过进程pcb返回给用户,用户此时通过write()接口对文件进行写数据,字符串会从write接口通过文件描述符表找到struct file对象,从而找到内核缓冲区,将字符串拷贝到缓冲区当中。随后缓冲区就会定期的向磁盘文件中刷新数据。

  这个我们在系统部分已经说过了,但是为什么要说这个呢?实际上,如果今天,我们把磁盘这个外设换为网络,实际上就变成了网络通信!我们之前说过,传输层和网络层协议属于操作系统内核的一部分! 如果今天主机想要通过网络进行Tcp协议通信,那么 在传输层OS会维护两个缓冲区,一个 发送缓冲区一个 接收缓冲区

在这里插入图片描述

  在应用层,我们以双方约定好的协议,将数据进行序列化处理成一批字符串。我们前面在使用Socket编程的时候,直观上,我们都认为是send/sendto直接将数据发送给了对端,recv/recvfrom直接从对端接收数据。实际上双方的IO系统调用并不会直接作用于网络。如果是发送端,则调用write/send/sendto 接口发送到传输层的发送缓冲区所以 write/send 本质是拷贝函数。将待发送的数据拷贝到发送缓冲区。

  而发送数据则是由 Tcp 协议自主决定如何发送数据,而Tcp通过网络向对端发送数据,实际上就是 将自己发送缓冲区的内容通过网络拷贝到对方的接收缓冲区当中所以 在传输层看来,是双方的操作系统在进行通信! 随后,对端的接收缓冲区就会通过 read/recv 等接口将数据拷贝到应用层,所以 read/recv 接口本质也是拷贝函数!最后将序列化的字符串交给上层,上层再根据协议进行反序列化,最终拿到相应的数据。

在这里插入图片描述

  如果对端接收缓冲区内没有数据,那么 read/recv 接口就会阻塞等待,为什么会阻塞等待?因为缓冲区里没数据,而 本质上是因为调用read/recv接口的进程在等待数据的到来才会做下一步动作,从而将进程状态从运行态转变为阻塞态,当收到数据的时候再从阻塞态转为运行态。同样,如果主机A通过write/send 接口没有数据需要发送,也会阻塞等待。如果我们单单看发送方,有人把数据往发送缓冲区内写,OS把发送缓冲区的内容发送走,这难道不就是一个简单的生产消费者模型吗生产者是用户,消费者是OS,交易场所是发送缓冲区。同样对于接收端来说,OS将数据通过网络拷贝到了接收缓冲区,上层用户需要通过 read/recv 来取出数据,那么对于接收方来说,也是一个生产消费者模型,只不过 生产者为 OS, 消费者为用户,交易场所为接收缓冲区

  由上面的例子,你认为 阻塞的本质是什么是在进行同步! 为什么会是同步?因为通信双方需要等待数据的发送或者接收,而他们接收的过程无非就是发送端将数据拷贝到发送缓冲区,tcp再通过网络将将数据拷贝到对方的接收缓冲区中,对端用户需要调用 read/recv 拷贝接收缓冲区的数据到应用层。由此观之,通信的本质就是拷贝! 那么我们主机 A 在通过发送缓冲区给主机B的接收缓冲区发消息的时候,主机B不也可以通过自己的发送缓冲区给主机A的接收缓冲区发消息吗?它们之间互不干扰,所以这就是Tcp支持全双工的原因! 在Socket编程中我们说,一个 sockfd 也是支持全双工的,也是因为,sockfd既可以向发送缓冲区中发数据,也可以从接收缓冲区中拷贝数据


✈️Socket封装

  我们对Socket进行封装,使其以后无论是tcp协议还是udp协议,变得更加简洁,更加有条理性,这是因为创建一个套接字的方式方法是比较套路化的,所以我们可以对其进行封装,我们把创建套接字,绑定ip和端口,网络监听,网络接收,发起链接,分别封装为五个 纯虚函数,将来让子类进行强制重写:

class Socket
{
public:
    virtual void CreateSocketOrDie() = 0;             // 创建套接字
    virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
    virtual void ListenSocketOrDie() = 0;             // 监听套接字
    virtual socket_sptr Accepter(InetAddr *addr) = 0;
    virtual bool Connector(InetAddr &addr) = 0;

public:
};

  如果我们想要创建服务器端与客户端的tcp socket通信,我们只需要调用不同的虚函数即可,当然也可以通过这些虚函数来组成udp socket通信,不过这里我们就不再实现udp的socket了:

class Socket
{
public:
    virtual void CreateSocketOrDie() = 0;             // 创建套接字
    virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
    virtual void ListenSocketOrDie() = 0;             // 监听套接字
    virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接收链接
    virtual bool Connector(InetAddr &addr) = 0;		  // 发起链接

public:
    void BuildListenSocket(InetAddr &addr)// 创建tcp套接字,绑定并监听
    {
        CreateSocketOrDie();
        BindSocketOrDie(addr);
        ListenSocketOrDie();
    }

    bool BuildClientSocket(InetAddr &addr)// 创建客户端套接字
    {
        CreateSocketOrDie();
        return Connector(addr);
    }
};

  那么,我们如果想要建立Tcp连接,我们就可以在TcpSocket类当中继承Socket类,这样我们就可以对基类成员虚函数进行重写,而重写的所有内容实际上就是我们之前写的TcpSocket的内容,这里我就不再过多赘述:

const static int gbacklog = 8;
using socket_sptr = std::shared_ptr<Socket>;// 重命名 Socket 类型的智能指针

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
};

class TcpSocket : public Socket
{
public:
    TcpSocket(int fd) : _sockfd(fd)
    {
    }

    void CreateSocketOrDie() override
    {
        // 创建链式套接字
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, sockfd is: %d", _sockfd);
    }

    void BindSocketOrDie(InetAddr &addr) override
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(addr.Port());
        local.sin_addr.s_addr = inet_addr(addr.IP().c_str());

        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "bind success, sockfd is: %d", _sockfd);
    }

    void ListenSocketOrDie() override
    {
        int n = ::listen(_sockfd, gbacklog);
        if (n < 0)
        {
            LOG(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG, "listen success, sockfd is: %d", _sockfd);
    }

    socket_sptr Accepter(InetAddr *addr) override// 返回一个智能指针,以便于将来可以通过智能指针对基类成员方法进行调用
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (sockfd < 0)
        {
            LOG(WARNNING, "accept error");
            return nullptr;
        }
		// accpet成功
        *addr = peer;
        socket_sptr sock = std::make_shared<TcpSocket>(sockfd);
        return sock;
    }

    bool Connector(InetAddr &addr) override
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(addr.Port());
        server.sin_addr.s_addr = inet_addr(addr.IP().c_str());
        int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
        if (n < 0)
        {
            std::cerr << "connect error" << std::endl;
            return false;
        }
        return true;
    }

private:
    int _sockfd;
};

  这样写的好处是main函数和客户端的工作量就降低了很多,我们可以通过多态式的调用来完成Socket通信:

// 多态式调用
std::unique_ptr<Socket> listensock = std::make_unique<TcpSocket()>;
listensock->BuildListenSocket();// 直接建立起了连接

std::unique_ptr<Socket> clientsock = std::make_unique<TcpSocket()>;
clientsock->BuildClientSocket();// 客户端Socket套接字建立

  这样,listensock或者clientsock虽然表面调用的是Socket基类,但是由于基类内的纯虚函数都在子类实现,所以会间接调用子类对父类纯虚函数的重写,这就是多态式调用。而以上对于Socket的封装,内置抽象函数(纯虚函数),需要子类强制重新实现的这种方式,是一种设计模式,称为 模版方法模式


✈️服务器端改写

  除此之外,我们把Socket进行了封装,那么TcpServer也就不需要像Tcp Socket编程那样进行写了,为了松耦合,我们把TcpServer类冗余部分全部删除,TcpServer帮我们的目的是:创建套接字,获取客户端链接,再去处理请求 三个部分,至于如何处理请求,就不该是TcpServer类所关心的了。

  首先,我们不再需要原本的初始化部分,因为我们对Socket进行了封装,我们只需要在TcpServer构造函数中进行调用即可:

TcpServer(int port)
    : _localaddr("0", port)// 0表示接收任意地址
    , _listensock(std::make_unique<TcpSocket>())// 子类对象构造父类指针,以便于多态式调用
    , _isrunning(false)
{
    _listensock->BuildListenSocket(_localaddr);// 多态式调用, 构建了ListenSocket, 一步到位
}

private:
    InetAddr _localaddr;
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;

  一个Socket的智能指针就可以实现多态式调用进行初始化Tcp套接字部分,对于具体的任务是如何处理的虽然TcpServer不该关心,但是如何分配任务以及分配任务的方式是我们需要操心的,在上一篇文章中我们有四种分配方式,后面三种选择任何一种都可,在这里我选择多线程的方式分配任务。

  不需要知道具体的任务细节,只需要TcpServer其提供一个可调用的任务接口即可,我们使用function将Service接口封装为一个新的类型 io_service_t类型,在初始化部分与线程回调函数部分我们都可以对其进行调用:

using namespace socket_ns;
// socket_ns exists: using socket_sptr = std::shared_ptr<Socket>;

class TcpServer;

// 对任务进行封装
using io_service_t = std::function<void (socket_sptr sockfd, InetAddr client)>;

class ThreadData
{
public:
    ThreadData(socket_sptr fd, InetAddr addr, TcpServer* s)
        :sockfd(fd)
        ,clientaddr(addr)
        ,self(s)
    {}
public:
    socket_sptr sockfd;// using socket_sptr = std::shared_ptr<Socket>;
    InetAddr clientaddr;
    TcpServer *self;
};

// TcpServer的目的是为了创建套接字,获取链接,再去处理,具体如何处理,就不需要TcpServer关心了
class TcpServer
{
public:
    TcpServer(int port, io_service_t service)
        : _localaddr("0", port)
        , _listensock(std::make_unique<TcpSocket>())
        , _service(service)
        , _isrunning(false)
    {
        _listensock->BuildListenSocket(_localaddr);
    }

    // 线程回调函数
    static void* HandlerSock(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        td->self->_service(td->sockfd, td->clientaddr);
        delete td;
        return nullptr;
    }

    void Loop()
    {
        _isrunning = true;
        // 不能直接收数据,必须先获取连链接
        while(_isrunning)
        {
            InetAddr peeraddr;
            socket_sptr normalsock = _listensock->Accepter(&peeraddr);// // using socket_sptr = std::shared_ptr<Socket>;
            if(normalsock == nullptr) continue;

            pthread_t t;
            ThreadData *td = new ThreadData(normalsock, peeraddr, this);
            pthread_create(&t, nullptr, HandlerSock, td);// 将线程分离
        }
        _isrunning = false;
    }

    ~TcpServer()
    {}
private:
    InetAddr _localaddr;// 本地ip + port
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;

    io_service_t _service;
};

  在Loop函数中,由于在TcpServer中,我们重写了Accept()方法,所以我们不需要在Loop中写裸的accept()原生接口了,我们直接使用_listensock进行调用Accept()方法,会返回一个TcpSocket的对象,对象中本就存在sockfd。这样我们就可以成功获取连接了,再将其交给线程去处理即可。


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙动画开发07——粒子动画

1、概 述 粒子动画是在一定范围内随机生成的大量粒子产生运动而组成的动画。 动画元素是一个个粒子&#xff0c;这些粒子可以是圆点、图片。我们可以通过对粒子在颜色、透明度、大小、速度、加速度、自旋角度等维度变化做动画&#xff0c;来营造一种氛围感&#xff0c;比如下…

C语言学习 12(指针学习1)

一.内存和地址 1.内存 在讲内存和地址之前&#xff0c;我们想有个⽣活中的案例&#xff1a; 假设有⼀栋宿舍楼&#xff0c;把你放在楼⾥&#xff0c;楼上有100个房间&#xff0c;但是房间没有编号&#xff0c;你的⼀个朋友来找你玩&#xff0c;如果想找到你&#xff0c;就得挨…

【pyspark学习从入门到精通19】机器学习库_2

目录 估计器 分类 回归 聚类 管道 估计器 估计器可以被看作是需要估算的统计模型&#xff0c;以便对您的观测值进行预测或分类。 如果从抽象的 Estimator 类派生&#xff0c;新模型必须实现 .fit(...) 方法&#xff0c;该方法根据在 DataFrame 中找到的数据以及一些默认或…

结构方程模型(SEM)入门到精通:lavaan VS piecewiseSEM、全局估计/局域估计;潜变量分析、复合变量分析、贝叶斯SEM在生态学领域应用

目录 第一章 夯实基础 R/Rstudio简介及入门 第二章 结构方程模型&#xff08;SEM&#xff09;介绍 第三章 R语言SEM分析入门&#xff1a;lavaan VS piecewiseSEM 第四章 SEM全局估计&#xff08;lavaan&#xff09;在生态学领域高阶应用 第五章 SEM潜变量分析在生态学领域…

JQuery -- 第九课

文章目录 前言一、JQuery是什么&#xff1f;二、JQuery的使用步骤1.引入2.书写位置3. 表示方法 三、JQuery选择器1.层级选择器2. 筛选选择器3. 排他思想4. 精品展示 四、jQuery样式操作1. 修改样式2.类操作1. 添加2. 移除3. 切换 五、jQuery动画1. 显示和隐藏2. 滑动1. slide2.…

无人机探测:光电侦测核心技术算法详解!

核心技术 双光谱探测跟踪&#xff1a; 可见光成像技术&#xff1a;利用无人机表面反射的自然光或主动光源照射下的反射光&#xff0c;通过高灵敏度相机捕捉图像。该技术适用于日间晴朗天气下的无人机探测&#xff0c;具有直观、易于识别目标的特点。 红外成像技术&#xff1…

Java使用replaceAll替换时不使用正则表达式

前言 public String replaceAll(String regex, String replacement) {return Pattern.compile(regex).matcher(this).replaceAll(replacement);}在使用String.replaceAll() 方法时&#xff0c;由于入参时regex &#xff0c;而入参刚好是正则表达式的字符该怎么办&#xff1f;我…

计算机毕业设计Hadoop+Spark音乐推荐系统 音乐预测系统 音乐可视化大屏 音乐爬虫 HDFS hive数据仓库 机器学习 深度学习 大数据毕业设计

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

途普科技企业知识中台完成华为昇思MindSpore技术认证

近日&#xff0c;北京途普科技有限公司&#xff08;以下简称“途普科技”&#xff09;作为华为昇腾大模型方向的应用软件伙伴&#xff0c;核心产品企业知识中台已成功与华为AI框架昇思MindSpore完成相互兼容性认证。这一成就标志着途普科技在AI领域与华为的合作进一步加深&…

自由学习记录(25)

只要有修改&#xff0c;子表就不用元表的参数了&#xff0c;用自己的参数&#xff08;只不过和元表里的那个同名&#xff09; 子表用__index“继承”了父表的值&#xff0c;此时子表仍然是空表 一定是创建这样一个同名的变量在原本空空的子表里&#xff0c; 传参要传具体的变…

【Nginx】核心概念与安装配置解释

文章目录 1. 概述2. 核心概念2.1.Http服务器2.2.反向代理2.3. 负载均衡 3. 安装与配置3.1.安装3.2.配置文件解释3.2.1.全局配置块3.2.2.HTTP 配置块3.2.3.Server 块3.2.4.Location 块3.2.5.upstream3.2.6. mine.type文件 3.3.多虚拟主机配置 4. 总结 1. 概述 Nginx是我们常用的…

AIGC-----AIGC在虚拟现实中的应用前景

AIGC在虚拟现实中的应用前景 引言 随着人工智能生成内容&#xff08;AIGC&#xff09;的快速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的应用也迎来了新的契机。AIGC与VR的结合为创造沉浸式体验带来了全新的可能性&#xff0c;这种组合不仅极大地降低了VR内容的…

学习笔记035——MySQL索引

数据库索引 索引是为了提高数据的查询速度&#xff0c;相当于给数据进行编号&#xff0c;在查找数据的时候就可以通过编号快速找到对应的数据。 索引内部数据结构&#xff1a;B Tree 主键自带索引。 如&#xff1a; insert into user (id, name) values (1,f); insert int…

C语言数据结构-链表

C语言数据结构-链表 1.单链表1.1概念与结构1.2结点3.2 链表性质1.3链表的打印1.4实现单链表1.4.1 插入1.4.2删除1.4.3查找1.4.4在指定位置之前插入或删除1.4.5在指定位置之后插入或删除1.4.6删除指定位置1.4.7销毁链表 2.链表的分类3.双向链表3.1实现双向链表3.1.1尾插3.1.2头插…

计算机网络 网络安全基础——针对实习面试

目录 网络安全基础你了解被动攻击吗&#xff1f;你了解主动攻击吗&#xff1f;你了解病毒吗&#xff1f;说说基本的防护措施和安全策略&#xff1f; 网络安全基础 网络安全威胁是指任何可能对网络系统造成损害的行为或事件。这些威胁可以是被动的&#xff0c;也可以是主动的。…

上海乐鑫科技一级代理商飞睿科技,ESP32-C61高性价比WiFi6芯片高性能、大容量

在当今快速发展的物联网市场中&#xff0c;无线连接技术的不断进步对智能设备的性能和能效提出了更高要求。为了满足这一需求&#xff0c;乐鑫科技推出了ESP32-C61——一款高性价比的Wi-Fi 6芯片&#xff0c;旨在为用户设备提供更出色的物联网性能&#xff0c;并满足智能设备连…

初识java(2)

大家好&#xff0c;今天我们来讲讲java中的数据类型。 java跟我们的c语言的数据类型有一些差别&#xff0c;那么接下来我们就来看看。 一.字面常量&#xff0c;其中&#xff1a;199&#xff0c;3.14&#xff0c;‘a’&#xff0c;true都是常量将其称为字面常量。&#xff08;…

MMCM DRP动态配置方法(超详细讲解)

一、MMCM 源语介绍 1、调用源语 2、调用Clocking Wizard IP 调用Clocking Wizard IP核选择使用MMCM资源时&#xff0c;IP内部也是调用的MMCM源语。 Clocking Wizard IP中启用MMCM DRP接口方法&#xff1a; 在Clocking Wizard IP中设置分频倍频系数方法&#xff1a; IP核中生…

对于GC方面,在使用Elasticsearch时要注意什么?

大家好&#xff0c;我是锋哥。今天分享关于【对于GC方面&#xff0c;在使用Elasticsearch时要注意什么&#xff1f;】面试题。希望对大家有帮助&#xff1b; 对于GC方面&#xff0c;在使用Elasticsearch时要注意什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java…

Spring Boot 与 Spring Cloud Alibaba 版本兼容对照

版本选择要点 Spring Boot 3.x 与 Spring Cloud Alibaba 2022.0.x Spring Boot 3.x 基于 Jakarta EE&#xff0c;javax.* 更换为 jakarta.*。 需要使用 Spring Cloud 2022.0.x 和 Spring Cloud Alibaba 2022.0.x。 Alibaba 2022.0.x 对 Spring Boot 3.x 的支持在其发行说明中…