网络协议栈应用层的意义(内含思维导图和解析图通俗易懂超易理解)

绪论​:
“节省时间的方法就是全力以赴的将所要做的事情完美快速的做完,不留返工重新学习的时间,才能省下时间给其他你认为重要的东西。”
本章主要讲到OSI网络协议栈中的应用层的作用和再次在应用层的角度理解协议的具体意义,以及序列化、反序列化和解决Tcp字节流边界问题的方法,最后通过一个实操题来具体的看到应用层所要完成的操作(其中包含了Socket网络编程和多线程内容没看的一定要提前看喔)

请添加图片描述
话不多说安全带系好,发车啦(建议电脑观看)。

OSI定制的七层网络协议栈:
在这里插入图片描述

1.应用层

1.1再谈“协议”

之前所写的协议,本质就是一个约定,而现在再把这个约定实体化:

现在通过网络版计数器,来更好的理解:
为了网络上更便利的发送,要进行序列化和反序列化操作

  1. 序列化:将协议对应的结构化数据,转换成“字符串”字节流(目的:方便网络发送)
    也就是将多条信息打包成一条信息(一个字符串)
  2. 反序列化:将“字符串”字节流,转换成结构化数据(为上层业务随时提前有效字段)
    也就是再把这条信息再分回多条
    在这里插入图片描述
    协议就是发送的信息的结构(上方发送来的信息的原本结构就是那三条信息)

对于这些信息就可以把协议描述成一个结构体,并且双方对这个协议结构体都提前约定好的
客户端和服务端用同一个结构体(数据类型)对特定的字段进行解释,那这就是计算机层面上的协议(计算机间的约定)
在这里插入图片描述
而在网络中不直接传递整个协议结构:而是通过序列化(直接把结构体地址传过去:也就是char*)后在网络中传送,
到对方后再进行反序列化操作解析(通过强转为协议类型后选择对应的数据填充协议结构:(sturct Message*)p->name …)。
这样双方就能进行序列化和反序列的操作(也就是协议的目的)

应用层在不同的系统,平台上会有很多种协议结构,这样就不能保证所有平台双方统一正确的读取(平台差异),所以经过序列化和反序列化就能很好的解决这个问题(提前约定了协议就能获取/发送所要的数据)。

1.2协议定制

在应用层角度来看网络:
对于用户和服务端来说他需要有请求和应答协议

  1. 用户需要有: 请求(发送)协议
  2. 服务端需要有:应答协议

其中我们通过send/write、read/recv,来进行数据的发送和接收。
而请求、应答也是在这些函数的基础上来实现的
在这里插入图片描述
send/write、read/recv本质其实是拷贝函数,将用户写的数据拷贝到发送缓冲区/将接受缓冲区拷贝到用户数据(而具体要什么时候发送给server,这是由TCP协议决定(内核决定))

TCP传输控制协议:TCP实际通信的时候,就是双方OS之间进行通信!

在发送用户空间:你认为你发了多少字节,但不一定会收多少字节 (要看双发缓冲区容量由TCP决定(具体下面写到tcp协议内部时就能清楚的知道,这里先记住即可)) ,这也表明了TCP是面向字节流的(不像UDP是不会出现这样问题的,因为其是面向数据报的(一次发送一个整个数据报文))。

所以在TCP中可能一次发送中报文无法发完,对此我们需要明确报文和报文之间的边界,才能防止读取时并没有读完就使用的错误

应用层如何解决TCP字节流边界问题的方法:

通过对协议序列化的字符串上加上特定字符来当作边界:
在序列化后的字符串前加上len\n来当其的边界和开头来区分报文


当Request请求协议写成:

class Request
{
	//...
private:
    int _data_x;
    int _data_y;
    char _oper;// + - *
};

序列化后的字符串:x op y
再对字符串添加特定字符:“len\nx op y” (len\n)

其中len表示的是后面数据的长度,\n用来区分前后len与数据,通过len和\n这样就能避免数据未读完的情况。

因为:只有读到了\n通过\n前面的len才能确定后面的数据长度继续往后读,反之没读到\n就一直读到\n为止才开始往后读(len是个数字他可能是数据也可能是加的特定字符)。

例:

缓冲区中的报文可能是:“len\nx op y”“len\nx op y”“len\nx op y”“len\nx op y”.当读取到第一个len后面的\n就表示找到了一个报文)
也可能是:“len\nx op y”“len\nx op y”“len\nx op y”"len\nx “(此时就看最后和正常的字符串比较是少了"op y”,此时就能通过len来知道字符串是不够的!!)

但这样打印出来的字符可能不好看(会直接全部在同一行)
所以一般会再在每个字符串后再加上\n来让打印换行:

“len\nx op y\n”,这样就即美观又能防止数据未读完的情况

(其中\n不属于报文的一部分,仅仅只是个约定)

第一个就变成:len\nx op y\nlen\nx op y\nlen\nx op y\nlen\nx op y \n
第二个就变成:len\nx op y\nlen\nx op ylen\nx op ylen\nx \n
(真正发送的报文,此时将""去了)

通过这样的方式就能实现将一个个报文区分出来,获取所要的数据


Response成员有:result(结果)、code(状态码)
同理在Response回复协议中序列化的字符串也写成:“len\nresult code\n”


对此通过上方就能理出应用层角度的通信过程:
客户端与服务器:
客户端发送Request请求,服务器返回Response应答

在下面模拟实现中,把序列化、反序列化都分成了两份:

  1. 处理 真正的数据(x op y)
  2. 添加报头(len\n真正的数据 \n)

也就是对应函数:

  1. 处理真正数据的序列化:Serialize、反序列化:DeSerialize
  2. 添加报头信息:Encode、除去报头信息:Decode

若实操有点难看,建议放进自己的如Vscode写代码的软件中

实操计算器,具体如下:

TcpClientMain.cc:
客户端的主要逻辑,具体逻辑已用数字标好了:

#include "Protocol.hpp"
#include "Socket.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <memory>
#include <iostream>
using namespace std;
using namespace Protocol;

int main(int argc, char *argv[])
{
//通过main参数输入ip和port(此处默认会,不会请自行搜索main参数如何使用)
    if (argc != 3)
    {
        cout << "Usage:" << argv[0] << "serverip serverport" << endl;
        return 0;
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);


    Socket *conn = new TcpSocket();//建立链接
    if (!conn->BuildConnectSocketMethod(serverip, serverport)) // BuildConnectSocketMethod : GreateSocket、
    {
        cerr << "connect" << serverip << ":" << serverport << " failed" << endl;
        return 0;
    }
    cout << "connect" << serverip << ":" << serverport << " success" << endl;

    // unique_ptr<Factory> factory = make_unique<Factory>();
    unique_ptr<Protocol::Factory> factory(new Protocol::Factory();
//创建工厂变量,工厂将会在协议Protocol类中实现(在后面的源文件中)

    srand(time(nullptr) ^ getpid());//生成随机数,来进行计算
    string opers = "+-*/%^=!";// +-*/ ,其余是故意写的来模仿错误情况 
    while (true)
    {
        int x = rand() % 100;//限制大小
        usleep(rand() % 7777);
        int y = rand() % 100;
        char oper = opers[rand() % opers.size()];//生成计算方法

        shared_ptr<Protocol::Request> req = factory->BuildRequest(x, y, oper);//通过工厂创建请求

        // 1. 对req进行序列化
        string req_str;
        req->Serialize(&req_str);
        // for print
        string testptring = req_str;
        testptring += " ";
        testptring += "= ";

        // 2. 拼接报头
        req_str = Encode(req_str);

        // 3. 发送
        conn->Send(req_str);

        string resp_str;
        while (true)
        {
            // 4. 接收响应
            if(!conn->Recv(&resp_str, 1024)) break;

            // 5. 除去报头
            string message;
            if (!Decode(resp_str, &message)) continue;//错误说明短了所以继续再读

            // 6. 得到了数据"result code",将数据结构化
            auto resp = factory->BuildResponse();
            resp->DeSerialize(message);

            //7. 打印结果:
            cout << testptring << resp->GetResult() << "[" << resp->GetCode() << "]" << endl;
            break;
        }
        sleep(1);
    }
    conn->CloseFd();

    return 0;
}

TcpServerMain.cc:
服务器主要执行处理获取的数据:

#include "Protocol.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Calculate.hpp"
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory> 
#include <pthread.h>

using namespace std;
using namespace CalCulateNS;
using namespace Protocol;

//服务器接收到数据,对数据进行处理方法
string HandlerRequest(string & inbufferstream,bool* error_code)
{
    *error_code = true;//数据没问题,只是读取不够,所以返回true
    //计算机对象
    Calculate calculate;
    //构建相应对象
    unique_ptr<Factory> fact(new Factory());
    auto req = fact->BuildRequest();

    string message;//存发来的信息

    string total_resp_string;
     while(Decode(inbufferstream,&message))//只有解析成功才往后
    {
        //反序列化
        if(!req->DeSerialize(message))
        {
            *error_code = false;//不可容忍的错误!
            return string();
        }

        //处理数据,并将处理好的数据以回复协议的形式返回
        auto resq = calculate.Cal(req);
        
        //5. 得到resq,再序列化准备发回给客户端
        string send_str;
        resq->Serialize(&send_str);
        //序列化后得到字符串 "result code"
        
        //6. 拼接len\n,形成字符串级别的相应报文
        send_str = Encode(send_str);
        
        // 7. 发送,将数据处理好存进total_resp_string 中返回到TCPServer.hpp中
        total_resp_string += send_str;
    }
    return total_resp_string;
}
 
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage:" << argv[0] << " port" << endl;
        return 0;
    }

    uint16_t localport = stoi(argv[1]);

    unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
    svr->loop();

    return 0;
}

服务器的主要逻辑在TCPServer.hpp内:
结合之前学的线程,让线程执行任务ThreadRun:

#pragma once
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <functional>
using namespace std;

using fun_t = std::function<string(string &,bool* error_code)>;

class TcpServer;
class ThreadData
{
public:
    ThreadData(TcpServer* tcp_this,Socket* sockp):_this(tcp_this),_sockp(sockp)
    {}
    TcpServer* _this;
    Socket* _sockp;
};

class TcpServer
{
public:
    TcpServer(uint16_t port, fun_t request) : _port(port), _listensocket(new TcpSocket()), _handler_request(request)
    {
        _listensocket->BuildListenSocketMethod(port, backlog);
    }

    static void* PhreadRun(void*argv)//PthreadRun是个类的成员方法所以用static,不要this指针才能满足void* (*) (void*)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(argv);
        string bufferin; 
        while(true)
        {
            bool ok = true;//用来确定是否出错
            //读取数据,不关心数据,只进行读
            //1. 接收信息
            if(!td->_sockp->Recv(&bufferin,1024)) 
            {
                break;
            }
            //2. 处理报文数据,对获取数据进行反序列化处理后得到结果,再序列化发送回去
            string send_string = td->_this->_handler_request(bufferin,&ok);//回调不仅会出去,还会回来!
            //读发送数据,不关心数据,只进行发送
            if(ok)
            {
                //3. 发送数据
                if(!send_string.empty())
                {
                    td->_sockp->Send(send_string);
                }
            }
            else
            {
                break;
            }

        }
        td->_sockp->CloseFd();
        delete td->_sockp;
        delete td;
        return nullptr;
    }

    void loop()
    {
        for (;;)
        {
            string peerip;
            uint16_t peerport;
            Socket *newsock = _listensocket->AcceptConnection(&peerip, &peerport);//会返回Socket*
            if (newsock == nullptr)
            {
                cout << "AcceptConnection fail";
                continue;
            }
            cout << "获取一个新连接,sockfd:" << newsock->GetSockfd() << " client info:" << peerip << ":" << peerport;
            pthread_t tid;
            ThreadData* td = new ThreadData(this,newsock);
            pthread_create(&tid, nullptr, PhreadRun, td);
        }
    }
    ~TcpServer()
    {
        delete _listensocket;
    }

private:
    uint16_t _port;
    Socket *_listensocket;
public:
    fun_t _handler_request;
};

协议的实现:
Protocol.hpp:

#pragma once
#include <iostream>
#include <memory>
using namespace std;
namespace Protocol
{
    const string ProtSep = " ";
    const string LineBreakSep = "\n";

//添加报头信息
    string Encode(const string&message)//"len\nx op y\n"
    {
        string len = to_string(message.size());//计算数据的长度,然后再把这个长度变成字符串
        string package = len + LineBreakSep + message + LineBreakSep;//将报头信息添加进去,还包括了两个\n
        return package;
    }

//其中注意的是package不一定是一个完整的报文,他可能长了也可能短了,对此我们要进行处理!
//短了:"len"、"len\nx" 
//长了:"len\nx op y\n""len"、...
    bool Decode(string&package,string *message)
    {
        auto pos = package.find(LineBreakSep);//首先找到\n,来确定len,若\n找不到则表示短了!
        if(pos == string::npos) return false;//\n都找不到直接返回错误!

//到了此处至少能取出len了!
        string lens = package.substr(0,pos);//取出字符串len
        int len =  stoi(lens);//将len转换成整形

//取出len后,计算长度,判断传递进来的字符串package和实际字符串长度
        int total = lens.size() + len + 2 * LineBreakSep.size();//字符串实际长度
//若
        if(total > package.size()) return false;//如果传递进来的长度小于实际长度则一定未完全将字符串传递过来!

        //否则则直接取出len长度的实际信息的字符串即可!
        *message = package.substr(pos + LineBreakSep.size() , len);
        //最后把取出的删除!
        package.erase(0,total);
        return true;
    }

//请求协议
    class Request
    {
    public:
        Request():_data_x(0),_data_y(0),_oper(0)
        {}
        Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op)
        {}
        void Debeg()
        {
            cout << "_data_x: " << _data_x << endl;
            cout << "_data_y: " << _data_y << endl;
            cout << "_oper: " << _oper << endl;
        }
        void Inc()
        {
            _data_x++;
            _data_y++;
        }

        // 结构化数据 -> 字符串
        bool Serialize(string *out)
        {

            *out = to_string(_data_x) + ProtSep + _oper + ProtSep + to_string(_data_y);
            return 0;
        }

        bool DeSerialize(string &in) //"x op y"
        {
            auto left = in.find(ProtSep);
            if (left == string::npos)
                return false; // 表示没找到!
            auto right = in.rfind(ProtSep);
            if (right == string::npos)
                return false; // 表示没找到!

            _data_x = stoi(in.substr(0, left));
            _data_y = stoi(in.substr(right + ProtSep.size()));
            string oper = in.substr(left + ProtSep.size(), right - (left + ProtSep.size()));
            if (oper.size() != 1)
                return false;

            _oper = oper[0];
            return true;
        }
        int GetX(){return _data_x;}
        int GetY(){return _data_y;}
        char GetOper(){return _oper;}

    private:
        //"x op y\n",以\n结尾,当读取到\n表示当前报文读完了
        // len是报文字描述字段
        //"len"\n"x op y",其中len可以理解成报文、后面的x op y理解成有效载荷
        // 并且len 后面加\n进行分隔,len就表述了后面有效载荷的数据长度(也就是要读取的长度)
        //"len\nx op y"
        int _data_x;
        int _data_y;
        char _oper; // + - *
    };

//回应协议
    class Response
    {
    public:
        Response():_result(0),_code(0)
        {}
        Response(int result, int code) : _result(result), _code(code)
        {}
        bool Serialize(string *out)
        {
            *out = to_string(_result) + ProtSep + to_string(_code);
            return true;
        }

        bool DeSerialize(string &in)
        {
            auto pos = in.find(ProtSep);
            if (pos == string::npos)
                return false; // 表示没找到!

            _result = stoi(in.substr(0, pos));
            _code = stoi(in.substr(pos + ProtSep.size()));
            return true;
        }
        void SetResult(int result) {_result = result;}
        void SetCode(int code) {_code = code;}

        int GetResult(){ return _result; }
        int GetCode(){ return _code; }
    private:
        int _result; 
        int _code;
    };


    // 简单的工厂模式,通过工厂模式构造出对应的协议类!
    class Factory
    {
    public:
        shared_ptr<Request> BuildRequest()
        {
            shared_ptr<Request> req = make_shared<Request>();
            return req;
        }

        shared_ptr<Request> BuildRequest(int x, int y, char op)
        {
            shared_ptr<Request> req = make_shared<Request>(x, y, op);
            return req;
        }

        shared_ptr<Response> BuildResponse()
        {
            shared_ptr<Response> resp = make_shared<Response>();
            return resp;
        }

        shared_ptr<Response> BuildResponse(int result, int code)
        {
            shared_ptr<Response> resp = make_shared<Response>(result, code);
            return resp;
        }
    };
}

所实现的功能,计算器:

#pragma once

#include <memory>
#include "Protocol.hpp"
#include <iostream>

namespace CalCulateNS
{
    enum
    {
        Success = 0,
        DivZeroErr,
        ModZeroErr,
        Unknown
    };

    class Calculate
    {
    public:
        Calculate() {}

        shared_ptr<Protocol::Response> Cal(shared_ptr<Protocol::Request> req)
        {
            shared_ptr<Protocol::Response> resp = fact->BuildResponse();
            resp->SetCode(Success);
            switch (req->GetOper())
            {
                case '+':
                    resp->SetResult(req->GetX() + req->GetY());
                    break;
                case '-':
                    resp->SetResult(req->GetX() - req->GetY());
                    break;
                case '*':
                    resp->SetResult(req->GetX() * req->GetY());
                    break;
                case '/':
                {
                    if (req->GetY() == 0)
                    {
                        resp->SetCode(DivZeroErr);
                    }
                    else
                    {
                        resp->SetResult(req->GetX() / req->GetY());
                    }
                }
                break;
                case '%':
                {
                    if (req->GetY() == 0)
                    {
                        resp->SetCode(ModZeroErr);
                    }
                    else
                    {
                        resp->SetResult(req->GetX() % req->GetY());
                    }
                }
                break;
                default:
                    resp->SetCode(Unknown);
                break;
            }

            return resp;
        }

        ~Calculate() {}

    private:
        shared_ptr<Protocol::Factory> fact;
    };
}

套接字通信原理,Socket.hpp:

#pragma once
#include <iostream>
//网络常用的四个头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> 
#include <unistd.h>
#include <string.h>
#include <string>
using namespace std;
#define CONV(addrptr) ((struct sockaddr*)addrptr)

const static int sockdefault = -1;
const static int backlog = 5;

enum{
    SocketError = 1,
    BindError,
    ListenError 
};
//封装一个基类,socket接口类
//模板方法,设计模型
class Socket
{
public:
    virtual ~Socket(){}
    virtual void GreateSocket() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie(int backlog) = 0;//       int listen(int sockfd, int backlog);
    virtual Socket* AcceptConnection(std::string* sockip,std::uint16_t* sockport) = 0;//输出型参数
    virtual bool ConnectServer(std::string& sockip,std::uint16_t sockport) = 0;
    virtual int GetSockfd() = 0;
    virtual void SetSockfd(int sockfd) = 0;
    virtual void CloseFd() = 0;
    virtual bool Recv(string* buffer, int size) = 0;
    virtual bool Send(string& buffer) = 0;
public:

    void BuildListenSocketMethod(uint16_t port,int backlog)
    {
        GreateSocket();
        BindSocketOrDie(port);  
        ListenSocketOrDie(backlog);
    }    
    bool BuildConnectSocketMethod(std::string& sockip,std::uint16_t& sockport)
    {
        GreateSocket();
        return ConnectServer(sockip,sockport);
    }
    void BuildNormalSocketMethod(int sockfd)
    {
        SetSockfd(sockfd);
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket(int sockfd = sockdefault):_sockfd(sockfd)
    {} 
    ~TcpSocket()
    {}

    void GreateSocket() override
    {   
        _sockfd = ::socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd < 0) 
        {
            exit(SocketError);
            cout << "GreateSocket failed" << endl;
        }
    }

    void BindSocketOrDie(uint16_t port) override
    {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;//不指定ip
        local.sin_port = htons(port);

        int n = ::bind(_sockfd,CONV(&local),sizeof(local));
        if(n < 0)
        {
            exit(BindError);
            cout << "BindError failed" << endl;
        } 
    }
    //int listen(int sockfd, int backlog);
    void ListenSocketOrDie(int backlog) override   
    {
        int n = ::listen(_sockfd,backlog);
        if(n < 0)
        {
            exit(ListenError);
            cout << "ListenError failed" << endl;
        } 
    }

    Socket* AcceptConnection(std::string* sockip,std::uint16_t* sockport) override
    {
        struct sockaddr_in addr;
        socklen_t len = sizeof(addr);
        int newsocket = accept(_sockfd,CONV(&addr),&len);
        if(newsocket < 0) return nullptr;

        Socket* s = new TcpSocket(newsocket);

        *sockport = ntohs(addr.sin_port); 
        *sockip = inet_ntoa(addr.sin_addr); 
        return s;
    }
    
    bool ConnectServer(std::string& sockip,std::uint16_t sockport) override
    {
        struct sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(sockip.c_str());
        server.sin_port = htons(sockport);

        socklen_t len = sizeof(server);

        int n = connect(_sockfd,CONV(&server),len);//你这是啥??
        //你是一个客户端,你为什么要accept?可能是写错了
        //改下为connect,应该就没啥问题了   还有什么问题吗?我先试下 好的,我先下了啊哈
        if(n == 0) return true;
        else return false;
    }

    int GetSockfd()
    {
        return _sockfd;
    }
    void SetSockfd(int sockfd)
    {
        _sockfd = sockfd;
    }

    void CloseFd() override
    {
        if(_sockfd > sockdefault) ::close(_sockfd);
    }

    bool Recv(string* buffer ,int size) override
    {
        char bufferin[size];
        size_t n = recv(_sockfd,bufferin,size-1,0);
        if(n > 0)
        {
            bufferin[n] =0;
            *buffer += bufferin;//此处是+=故意让其拼接!
            return true;
        }
        return false;
    }

    bool Send(string& buffer)override
    {
        send(_sockfd,buffer.c_str(),buffer.size(),0);
        return true;
    }
    
private:
    int _sockfd;
};

1.3成熟的序列化和反序列化方案:

常见的序列化协议

  1. json:允许采用 {"key ", “value”} 的方式将信息组织起来
  2. protobuf
  3. xml

下面我们将使用json

centos 7.9安装JSON流程:
sudo yum install jsoncpp-devel
ubuntu:
sudo apt install list libjsoncpp-dev

序列化:

Json::Value root;//Json::Value类型
//像map中的[]的使用一样
root["k1"] = 1;
root["k2"] = "string";
Json::FastWrite writer;
string s = wirter.write(root);//序列化生成字符串

反序列化:

bool Deserialize(string &in)
{
	Json::Value root;
	Json::Reader reader;
	
	bool res = reader.parse(in,root);
	//第一个参数是一个流,从in字符串流中获取数据反序列化给到root
	if(res)
	{
		_result = root["result"].asInt();//asInt转化成整形
		_code = root["code"].asInt();
	}
	return res;
}


当我们写完这些代码后,回过去看ISO七层模型中的顶上三层,应用、表示、对话层
在这里插入图片描述
不难发现他们分别对应着
会话层对应着:Socket套接字实现的连接和断开操作(connect、accept)

表示层对应着:协议的定制以及序列化和反序列化的操作(Protocol、Serialize)

应用层对应着:最终所要实现的功能(Calculate)


本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量计算机网络细致内容,早关注不迷路。

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

溟洵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值