linux下服务器的搭建,socket+epoll模型+线程池+心跳检测+工厂模式;内含客户端的测试。

服务器搭建在linux环境下,为处理高并发,加入epoll模型来接收,客户端的监听以及数据接收;

通过对收包类型的判断用工厂模式来进行不同任务的处理;为啦实现每个模块的隔离,和代码的扩展性,将服务端套接字的类ServerSocket中的服务端套接字m_server和epoll树返回到主函数中进行处理,结构设计有点问题;此程序只设计啦两个简单的心跳检测和登陆请求模块,如需加入其他功能继承BasicWorke类,重写其中去函数可实现自定义功能。

以下代码基本都有注释;

ServerSocket:服务端套接字类;

m_struct.h  : 收发包数据结构体;

TaskQueue:线程池任务队列类;

ThreadPool:线程池类;  线程池用啦b站大佬 “爱编程的大炳”的线程池代码,进行修改增加任务参数。

BasicWorke:实现不同任务模块的基类;

HeartCheck:心跳检测,回发心跳包类;(继承于BasicWorke);

UpDataWorke:请求登陆类;(继承于BasicWorke);

1.ServerSocket.h服务端套接字类代码:

#pragma once
#include <iostream>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;


class ServerSocket
{
public:
	ServerSocket();
	virtual ~ServerSocket();
	bool init();
	int &getepoll();
	int& getserver();
private:
	int m_server;
	int epoll;
};

ServerSocket.cpp服务端套接字类代码:

#include "ServerSocket.h"

ServerSocket::ServerSocket()
{
	if (init()) {
		cout << "socket init ok" << endl;
	}
	else{
		cout << "socket init fail" << endl;
	}
}

ServerSocket::~ServerSocket()
{
	close(m_server);
}

bool ServerSocket::init()
{
	m_server = socket(AF_INET, SOCK_STREAM, 0);
	if (m_server < 0)
	{
		cout << "socket error" << endl;
		return false;
	}

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;         // IPv4
	addr.sin_port = htons(11111);      // port
	addr.sin_addr.s_addr = INADDR_ANY; // IP address
	if (bind(m_server, (struct sockaddr*)&addr, sizeof(addr)) < 0) { //绑定本地IP端口
		cout << "bind fail:" << endl;
		return false;
	}
	
	if (listen(m_server, 10)) {    //循环监听每次监听10个客户端连接
		cout << "listen fail:"<< endl;
		return false;
	}
	epoll = epoll_create(100);  //初始化epoll树;
	if (epoll == -1) {
		return false;
	}
	struct epoll_event ev;
	ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
	ev.data.fd = m_server;
	int ret = epoll_ctl(epoll, EPOLL_CTL_ADD, m_server, &ev);//将监听任务添加在epoll树上;
	while (ret == -1) {  //检查是否添加成功
		ret = epoll_ctl(epoll, EPOLL_CTL_ADD, m_server, &ev);//未添加成功循环添加;
	}
	return true;
}

int& ServerSocket::getserver()
{
	return m_server;
}

int& ServerSocket::getepoll()//将epoll和客户端m_server返回主函数;
{
	return epoll;
}

2.m_struct.h  : 收发包数据结构体;

#ifndef M_STRUCT_H
#define M_STRUCT_H

enum TypeInfo    //心跳检测枚举体
{
    HEART_CHECK_REQ,
    HEART_CHECK_RES,
};

struct Head    //数据包包头
{
    int type;
    int len;
};

struct HeartCheckReq    // 心跳检测包发包
{
    Head head;
    HeartCheckReq() {
        head.type = HEART_CHECK_REQ;
        head.len = sizeof(HeartCheckReq);
    }
};

struct HeartCheckRes   //心跳检测回包
{
    Head head;
    HeartCheckRes() {
        head.type = HEART_CHECK_RES;
        head.len = sizeof(HeartCheckRes);
    }
};

struct RequestLogin  //请求登陆数据包;
{
    Head head;
    RequestLogin(){
        head.type = 1;
        head.len = sizeof(RequestLogin);
    }
    char buf[32];
};

#endif // M_STRUCT_H

3.TaskQueue.h:线程池任务队列类头文件代码

#pragma once
#include <queue>
#include <pthread.h>

using callback = void(*)(void* arg,int* client);
//任务结构体
template<typename T>
struct Task
{
    Task()
    {
        function = nullptr;
        arg = nullptr;
        client = nullptr;
    }
    Task(callback f, void* arg,int *client)
    {
        function = f;
        this->arg = (T*)arg;
        this->client = client;
    }
    callback function;
    T* arg;
    int* client;
};

TaskQueue.cpp:线程池任务队列类源文件代码

#include "TaskQueue.h"

template<typename T>
TaskQueue<T>::TaskQueue()
{
    pthread_mutex_init(&m_mutex, NULL);
}

template<typename T>
TaskQueue<T>::~TaskQueue()
{
    pthread_mutex_destroy(&m_mutex);
}

template<typename T>
void TaskQueue<T>::addTask(Task<T> task)
{
    pthread_mutex_lock(&m_mutex);
    m_taskQ.push(task);
    pthread_mutex_unlock(&m_mutex);
}

template<typename T>
void TaskQueue<T>::addTask(callback f, void* arg,int *client)
{
    pthread_mutex_lock(&m_mutex);
    m_taskQ.push(Task<T>(f, arg, client));
    pthread_mutex_unlock(&m_mutex);
}

template<typename T>
Task<T> TaskQueue<T>::taskTask()
{
    Task<T> task;
    pthread_mutex_lock(&m_mutex);
    if (!m_taskQ.empty())
    {
        task = m_taskQ.front();
        m_taskQ.pop();
    }
    pthread_mutex_unlock(&m_mutex);
    return task;
}

4.ThreadPool.h:线程池类头文件代码;

#pragma once
#include "TaskQueue.h"
#include "TaskQueue.cpp"

template<typename T>
class ThreadPool
{
private:
    //任务队列
    TaskQueue<T>* taskQ;

    pthread_t managerID;       //管理者线程ID
    pthread_t* threadIDs;      //工作线程ID
    int minNum;                //最小线程数量
    int maxNum;                //最大线程数量
    int busyNum;               //忙的线程个数
    int liveNum;               //存活的线程个数
    int exitNum;               //要销毁的线程个数
    pthread_mutex_t mutexPool; //锁整个线程池
    pthread_cond_t notEmpty;   //任务队列是否为空了

    bool shutdown;             //是不是要销毁线程池,销毁为1,不销毁为0
    static const int NUMBER = 2;

public:
    //创建线程池并初始化
    ThreadPool(int min, int max);
    //销毁线程池
    ~ThreadPool();

    //给线程池添加任务
    void addTask(Task<T> task);

    //获取线程池中工作的线程个数
    int getBusyNum();

    //获取线程池中活着的线程个数
    int getAliveNum();

private:
    //工作的线程(消费者线程)任务函数
    static void* worker(void* arg);
    //管理者线程任务函数
    static void* manager(void* arg);
    //单个线程退出
    void threadExit();
};

ThreadPool.cpp:线程池类源文件代码;

#include "ThreadPool.h"
#include <iostream>
#include <string.h>
#include <string>
#include <unistd.h>
using namespace  std;

template<typename T>
ThreadPool<T>::ThreadPool(int min, int max)
{
    do
    {
        //实例化任务队列
        taskQ = new TaskQueue<T>;
        if (taskQ == nullptr)
        {
            cout << "malloc taskQ fail...\n";
            break;
        }
        threadIDs = new pthread_t[max];
        if (threadIDs == nullptr)
        {
            cout << "malloc threadIDs fail...\n";
            break;
        }
        memset(threadIDs, 0, sizeof(pthread_t) * max);
        minNum = min;
        maxNum = max;
        busyNum = 0;
        liveNum = min;  //和最小个数相等
        exitNum = 0;

        if (pthread_mutex_init(&mutexPool, NULL) != 0 ||
            pthread_cond_init(&notEmpty, NULL) != 0)
        {
            cout << "mutex or condition init fail...\n";
            break;
        }

        shutdown = false;
        //创建管理者线程
        //第三个参数在类内部,需要传入已经存在的地址(类实例化才有)
        //因此换成静态成员函数或友元函数;传入this是为了可以
        //访问其它成员属性和方法,静态方法只能访问静态的属性
        pthread_create(&managerID, NULL, manager, this);
        //创建工作线程                                 
        for (int i = 0; i < min; ++i)
        {
            pthread_create(&threadIDs[i], NULL, worker, this);
        }
        return;
    } while (0);

    //创建失败,释放资源
    if (threadIDs) delete[] threadIDs;
}

template<typename T>
ThreadPool<T>::~ThreadPool()
{
    //关闭线程池
    shutdown = true;
    //阻塞回收管理者线程池
    pthread_join(managerID, NULL);
    //唤醒阻塞的工作线程(消费者线程)
    for (int i = 0; i < liveNum; ++i)
    {
        pthread_cond_signal(&notEmpty);
    }

    //释放堆内存
    if (taskQ)
    {
        delete taskQ;
    }
    if (threadIDs)
    {
        delete[]threadIDs;
    }
    pthread_mutex_destroy(&mutexPool);
    pthread_cond_destroy(&notEmpty);
}

template<typename T>
void ThreadPool<T>::addTask(Task<T> task)
{
    if (shutdown)//已满
    {
        return;
    }
    //添加任务
    taskQ->addTask(task);
    pthread_cond_signal(&notEmpty);//唤醒工作线程
}

template<typename T>
int ThreadPool<T>::getBusyNum()
{
    pthread_mutex_lock(&mutexPool);
    int busyNum = this->busyNum;
    pthread_mutex_unlock(&mutexPool);
    return busyNum;
}

template<typename T>
int ThreadPool<T>::getAliveNum()
{
    pthread_mutex_lock(&mutexPool);
    int aliveNum = this->liveNum;
    pthread_mutex_unlock(&mutexPool);
    return aliveNum;
}

template<typename T>
//工作线程
void* ThreadPool<T>::worker(void* arg)
{
    //利用传入的this类调用该成员属性和方法
    ThreadPool* pool = static_cast<ThreadPool*>(arg);

    while (true)
    {
        pthread_mutex_lock(&pool->mutexPool);
        //当前任务队列是否为空
        while (pool->taskQ->taskNumber() == 0 &&!pool->shutdown)
        {
            //阻塞工作线程
            pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);

            //判断是否要销毁线程,其实是管理者线程执行这段代码
            if (pool->exitNum > 0)
            {
                pool->exitNum--;
                if (pool->liveNum > pool->minNum)
                {
                    pool->liveNum--;
                    pthread_mutex_unlock(&pool->mutexPool);
                    pool->threadExit();
                }
            }
        }

        //判断线程池是否关闭了
        if (pool->shutdown)
        {
            pthread_mutex_unlock(&pool->mutexPool);//释放锁
            pool->threadExit();//线程退出
        }

        //从任务队列中取出一个任务
        Task<T> task = pool->taskQ->taskTask();

        pool->busyNum++;
        //解锁
        pthread_mutex_unlock(&pool->mutexPool);
        cout << "thread " << to_string(pthread_self()) << " start working...\n";

        task.function(task.arg,task.client);//执行任务函数
       
        task.arg = nullptr;
        delete task.arg;
        cout << "thread " << to_string(pthread_self()) << " end working...\n";

        pthread_mutex_lock(&pool->mutexPool);
        pool->busyNum--;
        pthread_mutex_unlock(&pool->mutexPool);
    }
    return NULL;
}

template<typename T>
void* ThreadPool<T>::manager(void* arg)
{
    ThreadPool* pool = static_cast<ThreadPool*>(arg);
    while (!pool->shutdown)
    {
        //每隔3s检测一次
        sleep(3);

        //取出线程池中任务的数量、当前线程的数量、忙的线程的数量
        pthread_mutex_lock(&pool->mutexPool);;
        int queueSize = pool->taskQ->taskNumber();
        int liveNum = pool->liveNum;
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexPool);

        //添加线程
        //任务的个数>存活的线程个数&amp;&amp;存活的线程数<最大线程数
        if (queueSize > liveNum-busyNum && liveNum < pool->maxNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            int counter = 0;//默认最大增加2个工作线程
            for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; ++i)              
            {
                if (pool->threadIDs[i] == 0)
                {
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);
                    counter++;
                    pool->liveNum++;
                }
            }
            pthread_mutex_unlock(&pool->mutexPool);
        }

        //销毁线程
        //忙的线程*2<存活的线程数&amp;&amp;存活的线程>最小线程数
        if (busyNum * 2 < liveNum && liveNum > pool->minNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            pool->exitNum = NUMBER;
            pthread_mutex_unlock(&pool->mutexPool);
            //让工作的线程自杀
            for (int i = 0; i < NUMBER; ++i)
            {
                pthread_cond_signal(&pool->notEmpty);//唤醒阻塞的线程
            }
        }
    }
    return NULL;
}

template<typename T>
void ThreadPool<T>::threadExit()
{
    pthread_t tid = pthread_self();
    for (int i = 0; i < maxNum; ++i)
    {
        if (threadIDs[i] == tid)
        {
            threadIDs[i] = 0;
            cout << "threadExit() called, " << to_string(tid) << " exiting...\n";
            break;
        }
    }
    pthread_exit(NULL);//线程退出
}

5.BasicWorke.h:实现不同任务模块的基类头文件代码;

#pragma once
#include<iostream>
using namespace std;

class BasicWorke
{
public:
	BasicWorke();
	virtual ~BasicWorke();
	virtual void worker(char* buf, int* client)=0;
};

BasicWorke.cpp:实现不同任务模块的基类源文件代码;

#include "BasicWorke.h"

BasicWorke::BasicWorke()
{
	cout << "BasicWorke" << endl;
 
}

BasicWorke::~BasicWorke()
{
	cout << "~BasicWorke" << endl;
}

6.HeartCheck.h:心跳检测,回发心跳包类头文件;

#include "BasicWorke.h"
#include <iostream>
#include <map>
using namespace std;

class HeartCheck :
    public BasicWorke
{
public:
    HeartCheck(bool openCheck = true);
    virtual ~HeartCheck();
    virtual void worker(char* buf, int *client);
    void heartCheck();
private:
    map<int, int> m_clientSockets;
    bool m_openCheck; // 是否开启心跳检测的开关// 客户端套接字及阈值
};

HeartCheck.cpp:心跳检测,回发心跳包类源文件;

#include "HeartCheck.h"
#include "m_struct.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

#define HEART_CHECK_TIMES 8 // 心跳检测阈值,次数为 8
pthread_mutex_t heartmap;   //心跳检测客户端map集合锁;防止多线程同时修改同一键值对出错。

HeartCheck::HeartCheck(bool openCheck)
	:m_openCheck(openCheck)
{
	cout << "HeartCheck" << endl;
}

HeartCheck::~HeartCheck()
{
	cout << "~HeartCheck" << endl;
}

void HeartCheck::worker(char* buf, int *client)
{
	pthread_mutex_trylock(&heartmap);
	m_clientSockets[*client] = HEART_CHECK_TIMES;//有新连接发来心跳检测包,重置此链接的阈值;
	cout << "recv heart package" << endl;
	pthread_mutex_unlock(&heartmap);
	HeartCheckRes res;
	send(*client, (char*)&res, res.head.len, 0);
}

void HeartCheck::heartCheck()
{
	while (m_openCheck) {
		for (auto it = m_clientSockets.begin(); it != m_clientSockets.end(); ) {
			pthread_mutex_trylock(&heartmap);
			it->second--; // 阈值递减
			pthread_mutex_unlock(&heartmap);
			cout << "client:" << it->first << "yuzhi value:" << it->second << endl;
			if (it->second == 0) {  //发现无用长连接;从map中去除此连接;
				cout << "find long unuse connect,client:" << it->first << endl;
				pthread_mutex_trylock(&heartmap);
				close(it->first); // 关闭客户端套接字
				// 删除
				m_clientSockets.erase(it++); // it++ 防止迭代器失效
				pthread_mutex_unlock(&heartmap);
			}
			else {
				it++;
			}
		}
		sleep(5); // 间隔5秒轮询一次
	}
}

7.UpDataWorke.h:请求登陆类头文件;

#pragma once
#include "BasicWorke.h"
#include "m_struct.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;
class UpDataWorke :
    public BasicWorke
{
public:
	UpDataWorke();
	virtual ~UpDataWorke();
	virtual void worker(char* buf, int* client);
};

UpDataWorke.cpp:请求登陆类源文件

#include "UpDataWorke.h"
#include <string.h>

UpDataWorke::UpDataWorke()
{
}

UpDataWorke::~UpDataWorke()
{
}

void UpDataWorke::worker(char* buf, int* client)
{
	struct RequestLogin* login = (struct RequestLogin*)buf;
	cout << "客户端:" << login->buf << endl;
	strcpy(login->buf, "允许登陆,成功");
	send(*client, (char*)login, login->head.len, 0);
}

      主函数main.cpp;

#include <stdio.h>
#include <pthread.h>
#include <iostream>
#include "BasicWorke.h"
#include "HeartCheck.h"
#include "UpDataWorke.h"
#include "ThreadPool.h"
#include "ThreadPool.cpp"
#include "ServerSocket.h"
#include "m_struct.h"
#include <unistd.h>
#include <stdlib.h>
using namespace std;

static HeartCheck heartCheck; //实例化心跳检测的类;
ThreadPool<char*> pool(3, 10);//初始化线程池;最少3条,最多10条;可根据自己需要进行设置;

void taskFunc(void* arg,int *client) {  //线程池添加的任务函数
	int type = *(int*)arg;
	BasicWorke* basicWorke = nullptr;// 实例化基类BasicWorke使用动态多态去实现不同的实现功能;
	if (type == HEART_CHECK_REQ) {   //若要添加实现自定义模块派生类,在此进行多态判断;
		basicWorke = &heartCheck;//接收客户端心跳包,调用heartCheck并回复心跳包
	}
	else if (type == 1) {
		basicWorke = new UpDataWorke;//当接收到登陆请求时,调用UpDataWorke类实现
	}
	basicWorke->worker((char*)arg, client);
	if (dynamic_cast<HeartCheck*>(basicWorke) == nullptr) { //若不是心跳检测线程则析构;
		delete basicWorke;
	}
}

void worke(int& client, char* buf) {
	pool.addTask(Task<char*>(taskFunc, buf, &client));
}

void* heatcheck1(void* arg) {
	heartCheck.heartCheck();
	return NULL;
}
int main()
{
	ServerSocket server;     //启动客户端
	int m_server = server.getserver();
	int epoll = server.getepoll();  //接收epoll模型与服务端套接字

	pthread_t  heartCheck_pthread;    //单起一条线程用于一直心跳检测;
	pthread_create(&heartCheck_pthread, NULL, heatcheck1, NULL);
	pthread_detach(heartCheck_pthread);

	struct epoll_event evs[1024];
	int size = sizeof(evs) / sizeof(struct epoll_event);
	// 持续检测

	struct epoll_event ev;
	ev.events = EPOLLIN;    // 水平触发;
	ev.data.fd = m_server;
	int ret;
	 
	while (1)
	{
		// 调用一次, 检测一次
		int num = epoll_wait(epoll, evs, size, -1);//检测当前epoll树中已经连接的文件描述符(客户端套接字)
		for (int i = 0; i < num; ++i)
		{
			 
			// 取出当前的文件描述符
			int curfd = evs[i].data.fd;
			// 判断这个文件描述符是不是用于监听的
			if (curfd == m_server)
			{
				// 建立新的连接
				int cfd = accept(curfd, NULL, NULL);
				// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
				//ev.events = EPOLLIN;    // 读缓冲区是否有数据
				ev.data.fd = cfd;
				ret = epoll_ctl(epoll, EPOLL_CTL_ADD, cfd, &ev);
				if (ret == -1)
				{
					perror("epoll_ctl-accept");
					exit(0);
				}
			}
			else
			{
				cout << "have data already" << endl;
				// 处理通信的文件描述符
				// 接收数据
				char buf[1024];
				memset(buf, 0, sizeof(buf));
				int len = recv(curfd, buf, sizeof(buf), 0);
				if (len == 0)
				{
					printf("客户端已经断开了连接\n");
					// 将这个文件描述符从epoll模型中删除
					epoll_ctl(epoll, EPOLL_CTL_DEL, curfd, NULL);
					close(curfd);
				}
				else if (len > 0)  //处理数据包;
				{
					worke(curfd, buf);
				}
				else
				{
					perror("recv");
					exit(0);
				}
			}
		}
	}
	pthread_exit(&heartCheck_pthread);//退出心跳检测线程;
	return 0;
}

运行结果;

 客户端代码:

client.h头文件代码;

#ifndef CLIENT_H
#define CLIENT_H

#include <QMainWindow>
#include <QTcpSocket>
#include <QTimer>
#include "m_struct.h"

namespace Ui {
class Client;
}

class Client : public QMainWindow
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = 0);
    ~Client();

private slots:
    void heartCheckSlot();
    void myRead();
    void on_pushButton_clicked();

private:
    Ui::Client *ui;
    int m_checkTimes;
    QTcpSocket *m_client;
    QTimer *m_heartTimer;
};

#endif // CLIENT_H

client.cpp源文件代码;

#include "client.h"
#include "ui_client.h"
#include <QDebug>

#define HEART_CHECK_TIMES 8 // 心跳阈值

Client::Client(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::Client)
{
    ui->setupUi(this);
    m_client = new QTcpSocket(this);
     m_client->connectToHost("192.168.140.128",11111);
    m_checkTimes = HEART_CHECK_TIMES; // 设置初始阈值
    if(m_client->waitForConnected()){
        qDebug()<<"conn ok";
        connect(m_client,SIGNAL(readyRead()),this,SLOT(myRead()));
        m_heartTimer = new QTimer(this);

        connect(m_heartTimer,SIGNAL(timeout()),this,SLOT(heartCheckSlot()));

        m_heartTimer->start(5*1000);
    }else{
        qDebug()<<"conn fail"<<m_client->errorString();
    }
}

Client::~Client()
{
    delete ui;
}

void Client::heartCheckSlot()
{
    HeartCheckReq req;
    qDebug()<<m_client->write((char*)&req,req.head.len);
    m_checkTimes=m_checkTimes-1; // 阈值递减
    qDebug()<<"阈值:"<<m_checkTimes;
    if(m_checkTimes <= 0){
        // 断线重连
        m_client->close();
        m_client->connectToHost("192.168.140.128",11111);
        if(m_client->waitForConnected()){
            m_checkTimes=HEART_CHECK_TIMES;
            qDebug()<<"断线重连成功";
        }
    }
}

void Client::myRead()
{
    QByteArray buffer = m_client->readAll();
    m_checkTimes = HEART_CHECK_TIMES; // 重置阈值
    int type = *(int*)buffer.data();
    if(type == HEART_CHECK_RES){
        qDebug()<<"收到心跳响应";
    }
    else if(type==1){
        qDebug()<<"登陆成功";
    }
}



void Client::on_pushButton_clicked()
{
    struct RequestLogin login;
    strcpy(login.buf,"请求登陆");
    m_client->write((char*)&login,login.head.len);
}

主函数main.cpp

#include "client.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Client w;
    w.show();

    return a.exec();
}

界面及运行结果:

耐心看完的您麻烦点个关注,thanks;后附完整的服务器和客户端的完成的代码包;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值