认识“协议”

什么是协议

在计算机网络中,协议是指在网络中进行通信和数据交换时,双方遵循的规则和约定集合。它定义了数据的传输格式、顺序、错误处理、认证和安全性等方面的规范。

协议的设计和实现是计算机网络能够正常运行的基础。它确保了不同设备和系统之间能够相互理解和协作。协议定义了数据的结构和编码方式,规定了数据传输的方式和顺序,以及双方之间进行通信的交互规则。协议的标准化和普遍采用,使得计算机网络得以互联互通,并支持各应用和服务的实现。

为了满足不同的应用场景和需求,已经存在许多成熟的应用层协议。这些协议定义了在特定应用中进行通信和数据交换的规则和格式。(http、https、DNS、ftp、smtp…)。

结构化的数据传输

协议是一种 “约定”,socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些 “结构化的数据” 怎么办呢?

例如,我们需要实现一个网络版本的简易计算器。此时,客户端给服务端发送的数据中,包含一个左操作数、操作符、右操作数。然后由服务端接收处理之后再将结果发送给客户端。

此时,就遇到了一个问题,客户端发送给服务端的不是一个简单的字符串,而是一组结构化的数据。如果客户端将这些结构化的数据单独的通过网络发送给服务端,那么服务端很难将收到的数据进行排列形成正确的数据。因此,客户端最好将这些数据一次性进行发出,此时服务端获取到的就是一个完整的数据请求,客户端常见的方案有以下两种。

约定方案一:将结构化的数据组合成为一个字符串

  • 客户端发送一个形如 “5+7” 的字符串;
  • 这个字符串中有两个操作数,都是整形;
  • 两个数字之间会有一个字符是运算符;
  • 数字和运算符之间没有空格;

这种将结构化的数据组合成一个字符串的方式可以是一个简单的方法,适用于简单的数据结构和通信需求。通过约定字符串的格式和规则,可以将结构化数据转换为字符串进行传输。

约定方案二:将结构数据序列化和反序列化

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

通过序列化和反序列化可以更灵活地处理结构化数据,并支持复杂的数据类型和结构。通过定义一个结构体或对象来表示需要交互的信息,可以使用序列化算法将结构体转换为字符串形式进行传输。接收端则可以使用相同的序列化算法进行反序列化,将字符串转化为原始的数据结构。

序列化和反序列化

序列化和反序列化是将数据在不同表示形式之间进行转化的过程。

  • 序列化是指将数据从内存中的对象或数据结构转化为可存储或传输的格式,例如字符串、字节流或二进制数据。
  • 反序列化是将序列化之后的数据重新转化为内存中的对象或数据结构。

在这里插入图片描述

网络版本计算器

接下来我们使用自己制定的协议来写一个网络版本的计算器。

首先,将后续代码需要的日志文件引入,日志 log.hpp 代码如下:

#pragma once
#include <stdio.h>
#include <cstdarg>
#include <ctime>
#include <cassert>
#include <stdlib.h>
#include <cstring>
#include <cerrno>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

void logMessage(int level,const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    FILE *out = (level == FATAL) ? stderr : stdout;

    fprintf(out, "%s | %u | %s | %s\n",
            log_level[level],
            (unsigned int)time(nullptr),
            name == nullptr ? "unknow" : name,
            logInfo);

    va_end(ap); // ap = NULL
}

协议定制

要实现一个网络版本的计算器,就必须确保通信双方能够遵守某种协议约定。这里我们制定一套简单的协议约定。数据分为请求数据和响应数据。因此我们可以定义请求类和响应类来实现协议约定。

// 请求类
class Request {
public:
    int x_;
    int y_;
    char op_;
};

// 响应类
class Response {
public:
    int exitCode_; // 状态字段
    int result_;   // 计算结果
};

定义了一个名为 Request 的请求类和一个名为 Response 的响应类。这些类具有公共的成员变量来存储请求和响应的数据。

请求类中包括两个操作数和一个操作符,响应类中包含一个计算结果以及该次计算的状态字段。状态字段用于表示该次的计算是否符合计算要求。

约定状态字段对应的含义如下:

  • 0 - 表示计算成功;
  • -1 - 表示除0错误;
  • -2 - 表示模0错误;
  • -3 - 表示非法操作符;

注意,计算结果只有在状态码为0的时候才有意义,否则计算的结果是没有意义的。

序列化和反序列化

下面代码实现了一个简单的请求类和响应类,并提供了序列化和反序列化的方法来将结构化的数据转换为字符串,并将字符串转换会结构化的数据。根据定义的宏 MY_SELF 的是否定义,可以使用自定义的序列化方法或者使用 JSON 库进行序列化。

#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"

#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define OPS "+-*/%"
#define BUFFER_SIZE 1024
// #define MY_SELF 1

// encode,整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
    std::string encodein = std::to_string(len);
    encodein += CRLF;
    encodein += in;
    encodein += CRLF;
    return encodein;
}

// decode,这个序列化之后的字符串进行提取长度
// 1.必须具有完整的长度   2.必须具有和len相符合的有效载荷
// 3.具备上述两个长度才返回有效载荷和len,否则,就是一个检测函数
std::string decode(std::string &in, uint32_t *len)
{
    assert(len);
    // 1.确认是否是一个包含len的有效字符串
    *len = 0;
    std::size_t pos = in.find(CRLF);
    if (pos == std::string::npos)
        return "";
    // 2.提取长度
    std::string Len = in.substr(0, pos);
    int intLen = atoi(Len.c_str());
    // 3.确认有效载荷也是符合要求的
    int surplus = in.size() - 2 * CRLF_LEN - pos;
    if (surplus < intLen)
        return "";
    // 4.确认有完整的报文结构
    std::string package = in.substr(pos + CRLF_LEN, intLen);
    *len = intLen;
    // 5.将当前的报文完整的从in中移除掉
    int removeLen = Len.size() + package.size() + 2 * CRLF_LEN;
    in.erase(0, removeLen);
    // 6.正常返回
    return package;
}

class Request
{
public:
    Request() {}
    ~Request() {}

    // 序列化 - 结构化的数据 -> 字符串
    void serialize(std::string *out)
    {
#ifdef MY_SELF
        std::string xStr = std::to_string(x_);
        std::string yStr = std::to_string(y_);

        *out = xStr;
        *out += SPACE;
        *out += op_;
        *out += SPACE;
        *out += yStr;
#else
        // 1.Value对象,json基于KV,json是有两套操作方法的
        Json::Value root;
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;

        Json::FastWriter fw;
        *out = fw.write(root);
#endif
    }

    // 反序列化 - 字符串 -> 结构化的数据
    bool deserialize(std::string &in)
    {
#ifdef MY_SELF
        // 100 + 200
        std::size_t spaceOne = in.find(SPACE);
        if (std::string::npos == spaceOne)
            return false;
        std::size_t spaceTwo = in.rfind(SPACE);
        if (std::string::npos == spaceTwo)
            return false;

        std::string dataOne = in.substr(0, spaceOne);
        std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
        std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
        if (oper.size() != 1)
            return false;

        x_ = atoi(dataOne.c_str());
        y_ = atoi(dataTwo.c_str());
        op_ = oper[0];
        return true;
#else
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);
        x_ = root["x"].asInt();
        y_ = root["y"].asInt();
        op_ = root["op"].asInt();
        return true;
#endif
    }

    void debug()
    {
        std::cout << "debug---------------------" << std::endl;
        std::cout << x_ << " " << op_ << " " << y_ << std::endl;
        std::cout << "debug---------------------" << std::endl;
    }

public:
    int x_;
    int y_;
    char op_;
};

class Response
{
public:
    Response() {}
    ~Response() {}

    // 序列化
    void serialize(std::string *out)
    {
#ifdef MY_SELF

        // "exitCode_ result_"
        std::string ec = std::to_string(exitCode_);
        std::string res = std::to_string(result_);

        *out = ec;
        *out += SPACE;
        *out += res;
#else
        Json::Value root;
        root["exitcode"] = exitCode_;
        root["result"] = result_;
        Json::FastWriter fw;
        *out = fw.write(root);
#endif
    }

    // 反序列化 - 不仅仅是在网路中应用,本地也是可以直接使用的
    bool deserialize(std::string &in)
    {
#ifdef MY_SELF
        // "0 300"
        std::size_t pos = in.find(SPACE);
        if (std::string::npos == pos)
            return false;
        std::string codeStr = in.substr(0, pos);
        std::string restStr = in.substr(pos + SPACE_LEN);

        exitCode_ = atoi(codeStr.c_str());
        result_ = atoi(restStr.c_str());
        return true;
#else
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);
        exitCode_ = root["exitcode"].asInt();
        result_ = root["result"].asInt();
        return true;
#endif
    }

    void debug()
    {
        std::cout << "debug---------------------" << std::endl;
        std::cout << "exitCode = " << exitCode_ << " result = " << result_ << std::endl;
        std::cout << "debug---------------------" << std::endl;
    }
    
public:
    // 退出状态,0表示运算结果合法,非0表示表示运行结果是非法的,!0是几就表示是因为什么原因错了。
    int exitCode_;
    int result_;
};

bool makeRequest(std::string &str, Request *req)
{
    // 1+1
    char strcmp[BUFFER_SIZE];
    snprintf(strcmp, sizeof strcmp, "%s", str.c_str());
    char *left = strtok(strcmp, OPS);
    if (!left)
        return false;
    char *right = strtok(nullptr, OPS);
    if (!right)
        return false;
    char mid = str[strlen(left)];

    req->x_ = atoi(left);
    req->y_ = atoi(right);
    req->op_ = mid;

    return true;
}
  • debug 方法用于调试,打印对象的成员变量值。
  • makeRequese 函数用于解析字符串并生成请求对象。
  • 此外,代码还定义了一些变量和辅助函数来处理字符串的拼接和分割。
  • 若要使用第三方库 jsoncpp 来处理 JSON 数据的序列化和反序列化。需要确保项目中包含了这个库,并在编译时链接到正确的库文件。

服务端代码

TCP 服务器我们使用多线程版本的,当服务器初始化完成并启动之后。当有一个客户端来请求服务器时,服务器就为其创建一个新的线程用于服务该客户端。这里,我们为客户端提供的是简单的计算器功能,服务端完成客户端给出的计算并给客户端返回结果。

class ServerTcp; // 声明一下

static Response calculator(const Request &req)
{
    Response resp;
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
    {
        if (req.y_ == 0)
            resp.exitCode_ = -1; // -1,除0
        else
            resp.result_ = req.x_ / req.y_;
    }
    break;
    case '%':
    {
        if (req.y_ == 0)
            resp.exitCode_ = -2; // -2,模0
        else
            resp.result_ = req.x_ % req.y_;
    }
    break;
    default:
        resp.exitCode_ = -3; // -3,非法操作符
        break;
    }
    return resp;
}

void netCalc(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    // 9\r\n100 + 200\r\n    9\r\n100 + 200\r\n
    std::string inbuffer;
    while (true)
    {
        // 定义一个请求对象
        Request req;
        char buffer[128];
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s == 0)
        {
            logMessage(NOTICE, "client[%s:%d] close sock,service done.", clientIp.c_str(), clientPort);
            break;
        }
        else if (s < 0)
        {
            logMessage(WARINING, "read client[%s:%d] error,errorCode: %d,errorMessage: %s", clientIp.c_str(), clientPort, errno, strerror(errno));
            break;
        }
        // read success
        buffer[s] = 0;
        inbuffer += buffer;

        // 1.检查inbuffer是否已经具有了一个strPackage
        uint32_t packgeLen = 0;
        std::string package = decode(inbuffer, &packgeLen);
        if (packgeLen == 0)
            continue; // 无法提取一个完整的报文,继续提取
        // 2.已经获取了一个完整的package
        if (req.deserialize(package))
        {
            // 3.处理逻辑,输入的是一个req,得到一个resp
            Response resp = calculator(req); // resp是一个结构化的数据
            // 4.对resp进行序列化
            std::string respPackage;
            resp.serialize(&respPackage);
            // 5.对报文进行encode
            respPackage = encode(respPackage, respPackage.size());
            // 6.简单进行发送
            write(sock, respPackage.c_str(), respPackage.size());
        }
    }
}

class ThreadData
{
public:
    uint16_t clientPort_;
    std::string clientIp_;
    int sock_;
    ServerTcp *this_;

    ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
        : clientPort_(port), clientIp_(ip), sock_(sock), this_(ts) {}
};

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) {}
    ~ServerTcp() {}

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            logMessage(FATAL, "socket:%s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMessage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);

        // 2. bind
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind:%s", strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5) < 0)
        {
            logMessage(FATAL, "bind:%s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);
    }

    void loop()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 4. 获取连接,accept的返回值是一个新的socket fd
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                // 获取连接失败,继续获取
                logMessage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 4.1 获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);

            logMessage(DEBUG, "accept:%s | %s[%d], socket fd:%d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);

            // 5. 提供服务,echho -> 小写 -> 大写
            // v2版本 - 多线程
            // 多线程不需要关闭文件描述符,因为多线程会共享文件描述符表!
            ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, (void *)td);
        }
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        netCalc(td->sock_, td->clientIp_, td->clientPort_);
        delete td;
    }
private:
    int listenSock_;
    uint16_t port_;
    std::string ip_;
};

代码解释:

  • 定义了一个 calculator 函数,用于根据请求对象进行计算并返回响应对象。根据请求的操作符,执行相应的计算,并将结果储存在响应对象中。
  • netCalc 函数用于吃力与客户端的网路通信。在循环中,读取客户端发送的数据,并解析出完整的请求报文。然后,调用 calculator 函数进行计算,并将计算结果序列化为响应报文发送给客户端。
  • 其余的 TCP 服务器相关的代码在 TCP网络程序 中有详细的解释。

客户端代码

接下来实现一个简单的客户端程序,可以向服务端发送请求并接收响应。其主要作用是与服务器进行通信,实现请求和响应的交互。

客户端代码如下:

volatile bool quit = false;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " prot ip" << std::endl;
    std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8080\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t serverPort = atoi(argv[2]);
    std::string serverIp = argv[1];

    // 1. 创建socket SOCK_STREAM
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket: " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }

    // 2. connect,向服务器发起连接请求
    // 2.1 先填充需要连接的远端主机的基本信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_aton(serverIp.c_str(), &server.sin_addr);
    // 2.2 发送请求,connect 会自动帮我们进行bind
    if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        std::cerr << "connect: " << strerror(errno) << std::endl;
        exit(CONN_ERR);
    }

    std::string message;
    while (!quit)
    {
        message.clear();
        std::cout << "Place Enter# ";
        std::getline(std::cin, message);
        if (strcasecmp(message.c_str(), "quit") == 0)
        {
            quit = true;
            continue;
        }

        // message=trimStr(message);
        Request req;
        if (!makeRequest(message, &req))
            continue;
        std::string package;
        req.serialize(&package);
        std::cout << "debug->serialize-> " << package << std::endl;
        package = encode(package, package.size());
        std::cout << "debug->encode-> \n" << package << std::endl;

        ssize_t s = write(sock, package.c_str(), package.size());
        if (s > 0)
        {
            char buffer[1024];
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if (s > 0)
                buffer[s] = 0;
            std::string echoPackage = buffer;
            Response resp;
            uint32_t len = 0;

            std::string tmp = decode(echoPackage, &len);
            if (len > 0)
            {
                echoPackage = tmp;
                resp.deserialize(echoPackage);
                printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
            }
        }
        else if (s <= 0)
        {
            break;
        }
    }
    return 0;
}

运行测试:

编写 makefile 构建程序,如下所示,需要定义 MY_SELF 时将 Method=-DMY_SELF 写上,不需要定义时就不需要写。

.PHONY:all
all:clientTcp serverTcpd
Method=-DMY_SELF

clientTcp:clientTcp.cc
	g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp
serverTcpd:serverTcp.cc
	g++ -o $@ $^ $(Method) -std=c++11 -lpthread -ljsoncpp

.PHONY:clean
clean:
	rm -f serverTcpd clientTcp

使用我们写的序列化与反序列化代码进行测试。

  • 17
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: BACnet协议是一种开放的通信协议,用于建筑自动化和控制系统中的设备之间的通信。它允许设备相互交流和共享信息,以实现更有效的能源管理和自动化控制。BACnet支持多种通信媒介,包括以太网、RS485和无线技术等。 BACnet协议具有可扩展性和兼容性的优点,它可以适应不同的硬件平台和软件应用,并支持多种语言和协议栈。它提供了一种标准的方式来获取、监视和控制楼宇自动化设备,如空调系统、照明系统、安全和监控系统等。 总体来说,BACnet协议是智能建筑和自动控制系统中不可或缺的一部分,它简化了设备之间的通信,提高了设备之间的互操作性和兼容性,使得控制系统更易于集成和管理。BACnet协议的应用还在不断发展,未来将会有更多领域会涉及到它。 ### 回答2: BACnet协议是一种用于建筑自动化和控制网络开放式协议。它是全球范围内使用最多的 HVAC(供暖、通风及空调)和能源管理系统协议之一。 BACnet协议支持现代通讯技术和标准,并具有大量的功能,如数据传输、传感器、控制器、调度器、通知和报警。 BACnet协议使得不同的设备和系统可以通过一个网络相互通信和交互,不但增加了系统的功能和互操作性,而且降低了系统的成本和维护费用。 此外,BACnet协议可以支持不同的网络拓扑,从简单的点对点连接到复杂的分布式系统。他还可以兼容不同的厂商和设备,为建筑自动化系统提供了更好的灵活性和可扩展性。 总体而言,了解和掌握BACnet协议对于建筑自动化行业从业人员来说是非常重要的,因为它是当前最流行和最适用的通讯协议之一。 ### 回答3: BACnet是建筑自动化和控制网络开放标准协议,是一种基于数据传输的通讯协议。BACnet协议的特点是兼容多种硬件和操作系统平台,能够快速地集成不同厂商、不同制造日期、不同功能的设备,并在多个系统平台上运行。 BACnet协议基于客户-server模式,可以支持各种设备之间的互通,包括控制器、传感器、执行器等。此外, BACnet协议支持多种数据类型和通信方式,可以轻松获取传感器的实时数据、历史数据、设备状态和控制命令等信息。通过BACnet协议,可以实现设备的智能化,提高设备的可靠性和安全性。 目前,BACnet协议已被广泛应用于各种建筑系统,包括通风、冷却、供暖、照明、安防等。通过BACnet协议,可以实现不同系统之间的互通,并可以轻松地增加、删除、修改设备,从而提高了设备的灵活性和可扩展性。 总之,BACnet协议是值得关注的一种开放标准协议,它可以在不同的设备、不同的系统之间实现互通和数据交换,从而实现建筑的智能化控制,并提高了设备的可靠性和安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

风&57

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

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

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

打赏作者

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

抵扣说明:

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

余额充值