套接字编程——TCP

1.接口函数。

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
 socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
 socklen_t addrlen);

2.单进程版本。

client.cc

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include<stdlib.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }
    //2. bind, 3. listen 4. accept             ??
    //client无需显示的bind, client->server
    //client -> connect!
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    //该函数做两件事情
    //1. 将点分十进制的字符串风格的IP,转化成为4字节IP
    //2. 将4字节由主机序列转化成为网络序列
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //server ip
    server.sin_port = htons(svr_port); // server port

    //2. 发起链接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cout << "connect server failed !" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行正常的业务请求了
    while(true)
    {
        std::cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer)-1, stdin);

        write(sock, buffer, strlen(buffer));

        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    return 0;
}

server.cc

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

 void ServiceIO(int new_sock)
 {
         //提供服务,我们是一个死循环
         while(true)
         {
             char buffer[1024];
             memset(buffer, 0, sizeof(buffer));
             ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
             if(s > 0)
             {
                 buffer[s] = 0; //将获取的内容当成字符串
                 std::cout << "client# " << buffer << std::endl;

                 std::string echo_string = ">>>server<<<, ";
                 echo_string += buffer;

                 write(new_sock, echo_string.c_str(), echo_string.size());
             }
             else if(s == 0){
                 std::cout << "client quit ..." << std::endl;
                 break;
             }
             else {
                 std::cerr << "read error" << std::endl;
                 break;
             }
         }
 }

// ./tcp_server 8081
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    //tcp server
    //1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
        std::cerr <<"socket error: " << errno << std::endl;
        return 2;
    }

    //2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }


    //3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
    //   一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
    //   我们当前写的是一个server, 周而复始的不间断的等待客户到来
    //   我们要不断的给用户提供一个建立连接的功能
    //
    //   设置套接字是Listen状态, 本质是允许用户连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }

    for( ; ; ) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
        //version 1: 单进程版,没人使用!
        ServiceIO(new_sock);
    }

    return 0;
}

单进程版本无人使用,一个客户端只有等另一个客户端结束会话才能通信。

多进程版本--信号。在信号章节中我们讲过在子进程退出会给父进程发送信号。我们忽略这个信号,子进程会自动释放资源

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

 void ServiceIO(int new_sock)
 {
         //提供服务,我们是一个死循环
         while(true)
         {
             char buffer[1024];
             memset(buffer, 0, sizeof(buffer));
             ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
             if(s > 0)
             {
                 buffer[s] = 0; //将获取的内容当成字符串
                 std::cout << "client# " << buffer << std::endl;

                 std::string echo_string = ">>>server<<<, ";
                 echo_string += buffer;

                 write(new_sock, echo_string.c_str(), echo_string.size());
             }
             else if(s == 0){
                 std::cout << "client quit ..." << std::endl;
                 break;
             }
             else {
                 std::cerr << "read error" << std::endl;
                 break;
             }
         }
 }

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    //tcp server
    //1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
        std::cerr <<"socket error: " << errno << std::endl;
        return 2;
    }

    //2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }


    //3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
    //   一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
    //   我们当前写的是一个server, 周而复始的不间断的等待客户到来
    //   我们要不断的给用户提供一个建立连接的功能
    //
    //   设置套接字是Listen状态, 本质是允许用户连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }

     signal(SIGCHLD, SIG_IGN); //在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    for( ; ; ) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;

		if(fork() == 0)
		{
			 ServiceIO(new_sock);
		}
		else
		{
			//do nothing
		}
       
    }

    return 0;
}

该版本可以实现多个客户端同时和服务器通信。

还可以实现另一个多进程的版本

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

 void ServiceIO(int new_sock)
 {
         //提供服务,我们是一个死循环
         while(true)
         {
             char buffer[1024];
             memset(buffer, 0, sizeof(buffer));
             ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
             if(s > 0)
             {
                 buffer[s] = 0; //将获取的内容当成字符串
                 std::cout << "client# " << buffer << std::endl;

                 std::string echo_string = ">>>server<<<, ";
                 echo_string += buffer;

                 write(new_sock, echo_string.c_str(), echo_string.size());
             }
             else if(s == 0){
                 std::cout << "client quit ..." << std::endl;
                 break;
             }
             else {
                 std::cerr << "read error" << std::endl;
                 break;
             }
         }
 }

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    //tcp server
    //1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
        std::cerr <<"socket error: " << errno << std::endl;
        return 2;
    }

    //2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }


    //3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
    //   一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
    //   我们当前写的是一个server, 周而复始的不间断的等待客户到来
    //   我们要不断的给用户提供一个建立连接的功能
    //
    //   设置套接字是Listen状态, 本质是允许用户连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }

     signal(SIGCHLD, SIG_IGN); //在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    for( ; ; ) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
       



        //version 2 版本-> 2.1
         pid_t id = fork();
         if(id < 0){
             continue;
          }
         else if( id == 0 ){ //曾经被父进程打开的fd,是否会被子进程继承呢? 无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
            //child
             close(listen_sock);

             if(fork() > 0) exit(0); //退出的是子进程

             //向后走的进程,其实是孙子进程
             ServiceIO(new_sock);
             close(new_sock);
             exit(0);
         }
         else {
             //father,不需要等待
             //do nothing!
             waitpid(id, nullptr, 0); //这里等待的时候会不会被阻塞呢? 不会
             close(new_sock);
         }
       
    }

    return 0;
}

该方案是在子进程中在创建孙子进程,然后关闭子进程,此时孙子进程变成孤儿进程,他就会被操作系统接管。资源也会被操作系统释放。

多线程版本

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

 void ServiceIO(int new_sock)
 {
         //提供服务,我们是一个死循环
         while(true)
         {
             char buffer[1024];
             memset(buffer, 0, sizeof(buffer));
             ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
             if(s > 0)
             {
                 buffer[s] = 0; //将获取的内容当成字符串
                 std::cout << "client# " << buffer << std::endl;

                 std::string echo_string = ">>>server<<<, ";
                 echo_string += buffer;

                 write(new_sock, echo_string.c_str(), echo_string.size());
             }
             else if(s == 0){
                 std::cout << "client quit ..." << std::endl;
                 break;
             }
             else {
                 std::cerr << "read error" << std::endl;
                 break;
             }
         }
 }

 void *HandlerRequest(void *args)
 {
     pthread_detach(pthread_self());
     int sock = *(int *)args;
     delete (int*)args;
	 ServiceIO(sock);
     close(sock);
}

// ./tcp_server 8081
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    //tcp server
    //1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
        std::cerr <<"socket error: " << errno << std::endl;
        return 2;
    }

    //2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }


    //3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
    //   一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
    //   我们当前写的是一个server, 周而复始的不间断的等待客户到来
    //   我们要不断的给用户提供一个建立连接的功能
    //
    //   设置套接字是Listen状态, 本质是允许用户连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }


    for( ; ; ) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;

        //version 3, 曾经被主线程打开的fd,新线程是否能看到,是否共享?
         pthread_t tid;
         int * pram = new int(new_sock);
         pthread_create(&tid, nullptr, HandlerRequest, pram);
   

       
    }

    return 0;
}

 线程池版(单例(懒汉))

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>
#include "Task.hpp"
#include "thread_pool.hpp"

using namespace ns_threadpool;
using namespace ns_task;

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

 void ServiceIO(int new_sock)
 {
         //提供服务,我们是一个死循环
         while(true)
         {
             char buffer[1024];
             memset(buffer, 0, sizeof(buffer));
             ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
             if(s > 0)
             {
                 buffer[s] = 0; //将获取的内容当成字符串
                 std::cout << "client# " << buffer << std::endl;

                 std::string echo_string = ">>>server<<<, ";
                 echo_string += buffer;

                 write(new_sock, echo_string.c_str(), echo_string.size());
             }
             else if(s == 0){
                 std::cout << "client quit ..." << std::endl;
                 break;
             }
             else {
                 std::cerr << "read error" << std::endl;
                 break;
             }
         }
 }

 void *HandlerRequest(void *args)
 {
     pthread_detach(pthread_self());
     int sock = *(int *)args;
     delete (int*)args;
	 ServiceIO(sock);
     close(sock);
}

// ./tcp_server 8081
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    //tcp server
    //1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0) {
        std::cerr <<"socket error: " << errno << std::endl;
        return 2;
    }

    //2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }


    //3. 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信
    //   一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
    //   我们当前写的是一个server, 周而复始的不间断的等待客户到来
    //   我们要不断的给用户提供一个建立连接的功能
    //
    //   设置套接字是Listen状态, 本质是允许用户连接
    const int back_log = 5;
    if(listen(listen_sock, back_log) < 0){
        std::cerr << "listen error" << std::endl;
        return 4;
    }

  //   signal(SIGCHLD, SIG_IGN); //在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    for( ; ; ) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_sock < 0)
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
        //version 4:进程或者线程池
        //version2,3: a. 创建线程、进程无上限 b. 当客户链接来了,我们才给客户创建进程/线程
        //1. 构建一个任务
        Task t(new_sock);
        //2. 将任务push到后端的线程池即可
        PthreadPool<Task>::GetInstace()->PushTask(t);



    }

    return 0;
}

Task.cpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include<stdlib.h>


namespace ns_task
{
    class Task
    {
    private:
       int _sock;
    public:
        // void (*callback)();
        Task() = default;
        Task(int sock)
            :_sock(sock)
        {

        }
        int Run()
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(_sock, buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0; //将获取的内容当成字符串
                std::cout << "client# " << buffer << std::endl;
                //拉取逻辑
                std::string echo_string = ">>>server<<<, ";
                echo_string += buffer;

                write(_sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0)
            {
                std::cout << "client quit ..." << std::endl;
                // break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                // break;
            }
           
        }
        ~Task() {}
    };
}

thread_pool.hpp

#include <iostream>
#include <queue>
#include <pthread.h>

namespace ns_threadpool
{
    template <class T>
    class PthreadPool
    {
    public:
        PthreadPool(int num = 5)
            : _num(num)
        {
            pthread_mutex_init(&_mutex, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }
        static PthreadPool<T> * GetInstace()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            if (_ins == nullptr)
            {
                pthread_mutex_lock(&lock);
                if (_ins == nullptr)
                {
                    _ins = new PthreadPool<T>();
                    _ins->InitPthreadPool();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }

            return _ins;
        }
        static void *Rountine(void *args) //因为在类中所以必须要是静态成员,因为有this指针
        {
            pthread_detach(pthread_self());
            PthreadPool<T> *q = (PthreadPool<T> *)args;
            for (;;)
            {
                q->Lock();
                while (q->IsEmpty())
                {
                    q->Wait();
                }
                T t;
                q->PopTask(&t);
                q->Unlock();
                t.Run();
            }
        }
        void InitPthreadPool()
        {
            pthread_t tid;
            for (size_t i = 0; i < _num; ++i)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this);
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            _task_queue.push(in);
            Unlock();

            WackUp();
        }
        ~PthreadPool()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_cond);
        }

    private:
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mutex);
        }
        void PopTask(T *out)
        {
            *out = _task_queue.front();
            _task_queue.pop();
        }
        bool IsEmpty()
        {
            return _task_queue.empty();
        }
        void Wait()
        {
            pthread_cond_wait(&_cond, &_mutex);
        }
        void WackUp()
        {
            pthread_cond_signal(&_cond);
        }

    private:
        std::queue<T> _task_queue;
        int _num;
        pthread_mutex_t _mutex;
        pthread_cond_t _cond;
        static PthreadPool<T>* _ins;
    };

    template <class T>
    PthreadPool<T>* PthreadPool<T>::_ins = nullptr;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值