UDP协议实现聊天功能例子

UDP服务端

协议

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <WinSock2.h>

enum CHAT_COMMAND
{
  CHAT_LOGIN,       //登录
  CHAT_ACK,         //确认
  CHAT_LOGIN_OK,    //成功
  CHAT_LOGIN_ERROR, //失败
  CHAT_ALL_USER,    //通知所有用户
  CHAT_LOGOUT,       //登录
  CHAT_SAY_ALL,       //对所有人说
  CHAT_SAY_TO,       //对人说
  CHAT_HEART,       //心跳  
};

struct  CHAT_HEAD_INFO
{
  int version;
  int cmd;
  int len;
};

struct CHAT_LOGIN_INFO
{
  char name[20];
};

struct CHAT_USER_INFO
{
  int         id;
  char        name[20];
  bool operator==(const CHAT_USER_INFO& info)
  {
    return id == info.id;
  }
};


struct CHAT_CLIENT_INFO
{
  int         id;
  char        name[20];
  sockaddr_in addr;
  //int time;

  bool operator==(const CHAT_CLIENT_INFO& info)
  {
    return id == info.id;
  }
};

//2 udp   len   65535
//1500 mtu
struct CHAT_SAY_ALL_INFO
{
  int         id;
  char        msg[200];
};

struct CHAT_SAY_TO_INFO
{
  int         sourceID;
  int         destID;
  char        msg[200];
};



ServerChat.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <WinSock2.h>
#include <windows.h>
#include <string>
#include <list>
#include "Protocol.h"

using namespace std;

#pragma comment(lib, "Ws2_32.lib")

//心跳包
//丢包

class ServerChat
{
public:
  ServerChat();
  ~ServerChat();
  void reportError(std::string s); //
  void showMessage(std::string msg);
  void createSocket();
  void bindSocket();
  void destroySocket();
  int getCommand();
  int parseCommand(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr);
private:
  SOCKET socket_;
  static int id_;
public:
  static list<CHAT_CLIENT_INFO> clients_;
public:
  int clientLogin(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr);
  bool nameExist(const char* buffer);
  int insertClientInfo(const char* buffer, sockaddr_in& caddr);
  int brocastUserList();
  int remoteClientInfo(CHAT_CLIENT_INFO& info);
  int brocastUserListHeart();//服务器发送心跳进行广播
  int clientReplyHeart(CHAT_USER_INFO& stUserInfo, sockaddr_in& caddr);//那个客户端回复了心跳
  //dxfgsdf
  int sengPacket(int cmd, char *buffer, int len, sockaddr& addr, int version = 1);
  int clientSayAll(CHAT_SAY_ALL_INFO& info);
  int clientSayTo(CHAT_SAY_TO_INFO& info);
};

ServerChat.cpp

#include "stdafx.h"
#define _CRT_SECURE_NO_WARNINGS
#include "ServerChat.h"
#include <algorithm>

int ServerChat::id_ = 0;
list<CHAT_CLIENT_INFO> ServerChat::clients_;

ServerChat::ServerChat()
{
  socket_ = INVALID_SOCKET;

  WORD wVersionRequested;
  WSADATA wsaData;
  wVersionRequested = MAKEWORD(2, 2);
  WSAStartup(wVersionRequested, &wsaData);

  createSocket();

  bindSocket();
}


ServerChat::~ServerChat()
{
  destroySocket();

  WSACleanup();
}

void ServerChat::reportError(std::string s)
{
  LPVOID lpMsgBuf;
  FormatMessage(
    FORMAT_MESSAGE_ALLOCATE_BUFFER |
    FORMAT_MESSAGE_FROM_SYSTEM |
    FORMAT_MESSAGE_IGNORE_INSERTS,
    NULL,
    GetLastError(),
    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
    (LPTSTR)&lpMsgBuf,
    0,
    NULL
    );
  printf("%s==>%s", s.c_str(), (char*)lpMsgBuf);

  LocalFree(lpMsgBuf);

}

void ServerChat::showMessage(std::string msg)
{
  printf(msg.c_str());
}

void ServerChat::createSocket()
{
  /*
  1.创建socket
  */
  int result;
  socket_ = socket(AF_INET, SOCK_DGRAM, 0); //udp
  if (INVALID_SOCKET == socket_)
  {
    reportError("socket\n");
    return;
  }
  showMessage("socket ok\n");
}

void ServerChat::bindSocket()
{
  //2.绑定端口
  sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(5566);
  addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
  //addr.sin_addr.S_un.S_addr = ADDR_ANY;  //任何地址
  int result = bind(socket_, (sockaddr*)&addr, sizeof(addr));
  if (result == SOCKET_ERROR)
  {
    reportError("bind\n");
    return;
  }
  showMessage("bind ok\n");

  showMessage("server start...\n");
}

void ServerChat::destroySocket()
{
  if (socket_ != INVALID_SOCKET)
  {
    //关闭
    shutdown(socket_, SD_BOTH);
    showMessage("shutdown ok\n");

    closesocket(socket_);
    showMessage("closesocket ok\n");
  }
}

int ServerChat::getCommand()
{
  sockaddr_in caddr;
  int len = sizeof(caddr);
  CHAT_HEAD_INFO head;
  int result;
  char *buffer = NULL;


  //接受消息
  result = recvfrom(socket_, (char*)&head, sizeof(head), 0, (sockaddr*)&caddr, &len);
  if (result <= 0)
  {
    reportError("recvfrom\n");
    return 0;
  }

  buffer = new char[head.len];
  result = recvfrom(socket_, buffer, head.len, 0, (sockaddr*)&caddr, &len);
  if (result <= 0)
  {
    delete[] buffer;
    reportError("recvfrom\n");
    return 0;
  }

  if (parseCommand(head, buffer, caddr) < 0)
  {
    delete[] buffer;
    return -1;
  }


  delete[] buffer;
  return 0;
}

int ServerChat::parseCommand(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr)
{
  int result = 0;
  switch (head.cmd)
  {
  case CHAT_LOGIN:
    result = clientLogin(head, buffer, caddr);
    break;
  case CHAT_LOGOUT:
    result = remoteClientInfo(*(CHAT_CLIENT_INFO*)buffer);
    break;
  case CHAT_SAY_ALL:
    result = clientSayAll(*(CHAT_SAY_ALL_INFO*)buffer);
    break;
  case CHAT_SAY_TO:
    result = clientSayTo(*(CHAT_SAY_TO_INFO*)buffer);
  case CHAT_HEART:
    result = clientReplyHeart(*(CHAT_USER_INFO*)buffer,caddr);
    break;
  }
  return result;
}


int ServerChat::clientLogin(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr)
{
  CHAT_HEAD_INFO sendHead;
  sendHead.version = 1;
  sendHead.len = 0;
  int result;

  if (nameExist(buffer))
  {
    sendHead.cmd = CHAT_LOGIN_ERROR;
    result = sendto(socket_, (char*)&sendHead, sizeof(sendHead), 0, (sockaddr*)&caddr, sizeof(caddr));
    if (result <= 0)
    {
      reportError("sendto");
      return -1;
    }
  }

  //save client info
  insertClientInfo(buffer, caddr);
  return 0;
}


bool ServerChat::nameExist(const char* buffer)
{
  //遍历名字是否重复
  for (auto info : clients_)
  {
    if (strcmp(buffer, info.name) == 0)
    {
      return true;
    }
  }
  return false;
}


int ServerChat::insertClientInfo(const char* buffer, sockaddr_in& caddr)
{
  CHAT_CLIENT_INFO info;
  info.id = ++id_;
  strcpy(info.name, buffer);
  info.addr = caddr;
  clients_.push_back(info);

  printf("%s:%d==>%s login\n",
    inet_ntoa(caddr.sin_addr),
    htons(caddr.sin_port), 
    info.name);

  CHAT_HEAD_INFO sendHead;
  sendHead.version = 1;
  sendHead.cmd = CHAT_LOGIN_OK;
  sendHead.len = sizeof(CHAT_USER_INFO);
  int result = sendto(socket_, (char*)&sendHead, sizeof(sendHead), 0, (sockaddr*)&caddr, sizeof(caddr));
  if (result <= 0)
  {
    reportError("sendto");
    return -1;
  }


  result = sendto(socket_, (char*)&info, sizeof(CHAT_USER_INFO), 0, (sockaddr*)&caddr, sizeof(caddr));
  if (result <= 0)
  {
    reportError("sendto");
    return -1;
  }


  //brocast user list
  brocastUserList();
  return 0;
}

int ServerChat::remoteClientInfo(CHAT_CLIENT_INFO& info)
{
  list<CHAT_CLIENT_INFO>::iterator it;

  clients_.remove(info);

  //brocast user list
  brocastUserList();
  return 0;
}

int ServerChat::brocastUserList()
{
  CHAT_HEAD_INFO head;
  head.version = 1;
  head.cmd = CHAT_ALL_USER;
  head.len = clients_.size() * sizeof(CHAT_USER_INFO);
  CHAT_USER_INFO *userInfo = new CHAT_USER_INFO[clients_.size()];

  int i = 0;
  for (auto info : clients_)
  {
    userInfo[i].id = info.id;
    strcpy(userInfo[i++].name, info.name);
  }

  //send
  for (auto info : clients_)
  {
    sendto(socket_, (char*)&head, sizeof(head), 0, (sockaddr*)&info.addr, sizeof(sockaddr));
    sendto(socket_, (char*)userInfo, head.len, 0, (sockaddr*)&info.addr, sizeof(sockaddr));
  }


  return 0;
}

int ServerChat::brocastUserListHeart()
{

  CHAT_HEAD_INFO head;
  head.version = 1;
  head.cmd = CHAT_HEART;
  head.len = 1;
  char ch = 'F';
  //send
  if (clients_.size() > 0)
  {
    for (auto info : clients_)
    {
      int nRet = sendto(socket_, (char*)&head, sizeof(head), 0, (sockaddr*)&info.addr, sizeof(sockaddr));
      if (nRet <= 0)
      {
        return -1;
      }
      nRet = sendto(socket_, &ch, sizeof(ch), 0, (sockaddr*)&info.addr, sizeof(sockaddr));
      if (nRet <= 0)
      {
        return -1;
      }
    }
  }
  return 0;
}

extern CRITICAL_SECTION cs;
int ServerChat::clientReplyHeart(CHAT_USER_INFO& stUserInfo, sockaddr_in& caddr)
{
  EnterCriticalSection(&cs);
  CHAT_CLIENT_INFO info;
  info.id = stUserInfo.id;
  strcpy(info.name, stUserInfo.name);
  info.addr = caddr;
  clients_.push_back(info);

  printf("%s:%d==>%s Online\n",
    inet_ntoa(caddr.sin_addr),
    htons(caddr.sin_port),
    info.name);
  LeaveCriticalSection(&cs);
  return 0;
}


int ServerChat::clientSayAll(CHAT_SAY_ALL_INFO& info)
{
  for (auto cinfo : clients_)
  {
    int result = sengPacket(CHAT_SAY_ALL, (char*)&info, sizeof(info), (sockaddr&)cinfo.addr);
  }

  return 0;
}

int ServerChat::sengPacket(int cmd, char *buffer, int len, sockaddr& addr, int version)
{
  CHAT_HEAD_INFO head;
  head.version = version;
  head.cmd = cmd;
  head.len = len;

  //代码重构
  int result = sendto(socket_, (const char*)&head, sizeof(head), 0, (sockaddr*)&addr, sizeof(addr));
  if (result <= 0)
    return -1;

  result = sendto(socket_, buffer, len, 0, (sockaddr*)&addr, sizeof(addr));
  if (result <= 0)
    return -1;

  return 0;
}


int ServerChat::clientSayTo(CHAT_SAY_TO_INFO& info)
{
  for (auto cinfo : clients_)
  {
    if (cinfo.id == info.destID)
    {
      int result = sengPacket(CHAT_SAY_TO, (char*)&info, sizeof(info), (sockaddr&)cinfo.addr);
      break;
    }
    
  }

  return 0;
}

 

 

main.cpp

// Server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "ServerChat.h"

CRITICAL_SECTION cs;
//心跳包处理
//心跳包直接放在cmd中就可以,客户端回个id和名字就可以,回复的放在新的链表中,对比旧的链表,如果旧的链表中没有新的,说明不在线,进行链表清理

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
  ServerChat* pstServer = (ServerChat*)lpParameter;
  while (true)
  {
    Sleep(20000);
    EnterCriticalSection(&cs);
    //发送心跳包
    int nRet=pstServer->brocastUserListHeart();
    if (nRet < 0)
    {
      printf("广播心跳时出错\n");
      return -1;
    }
    pstServer->clients_.clear();//发送心跳后清理,统计还有谁在线,把在线的人重新放入链表中
    LeaveCriticalSection(&cs);
  }
  return 0;
}

int main()
{

  //初始化临界区
  InitializeCriticalSection(&cs);

  ServerChat server;
  HANDLE hTread = CreateThread(
    NULL,          //安全属性
    0,             //使用默认栈大小,一般是1M,在链接选项(堆栈保留大小)可以修改
    ThreadProc,      //线程回调函数
    (LPVOID)&server, //自定义参数
    0,               //线程是否立即运行
    NULL);    //传出参数:线程ID
  if (hTread == NULL)
  {
    printf("创建线程失败\n");
    return 0;
  }
  while (true)
  {
    if (server.getCommand() < 0)
      break;
  }
  

  return 0;
}

UDP客户端

ClientChat.h

#pragma once

#include <WinSock2.h>
#include <windows.h>
#include <string>
#include <list>
#include "../../Server/Server/Protocol.h"

using namespace std;

#pragma comment(lib, "Ws2_32.lib")

class ClientChat
{
public:
  ClientChat();
  ~ClientChat();
  void createSocket();
  void destroySocket();
  int getCommand();
  int parseCommand(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr);
  void reportError(std::string s); //
  void showMessage(std::string msg);
  void shoWelcom();
  void createWorkThread();
  int inputCommand();
private:
  static DWORD WINAPI workThread(LPVOID lpParameter);
private:
  SOCKET socket_;
  sockaddr_in addr_;
  static CHAT_USER_INFO myuser_;
  static list<CHAT_USER_INFO> users_;
public:
  int login();
  int chatUserInfo(CHAT_USER_INFO *userInfo, int count);
  int showUserList();
  int logout();
  int sengPacket(int cmd, char *buffer, int len, int version = 1);
  int sayAll();
  int sayTo();
  int sendHeart();
};

ClientChat.cpp

#include "stdafx.h"
#include "ClientChat.h"

list<CHAT_USER_INFO> ClientChat::users_;
CHAT_USER_INFO  ClientChat::myuser_;

ClientChat::ClientChat()
{
  socket_ = INVALID_SOCKET;

  WORD wVersionRequested;
  WSADATA wsaData;
  wVersionRequested = MAKEWORD(2, 2);
  WSAStartup(wVersionRequested, &wsaData);

  createSocket();

  addr_.sin_family = AF_INET;
  addr_.sin_port = htons(5566);
  addr_.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);  //127.0.0.1
}


ClientChat::~ClientChat()
{
  destroySocket();

  WSACleanup();
}

void ClientChat::createSocket()
{
  /*
  1.创建socket
  */
  int result;
  socket_ = socket(AF_INET, SOCK_DGRAM, 0); //udp
  if (INVALID_SOCKET == socket_)
  {
    reportError("socket\n");
    return;
  }
  showMessage("socket ok\n");
}

void ClientChat::destroySocket()
{
  if (socket_ != INVALID_SOCKET)
  {
    //关闭
    shutdown(socket_, SD_BOTH);
    showMessage("shutdown ok\n");

    closesocket(socket_);
    showMessage("closesocket ok\n");
  }
}

int ClientChat::getCommand()
{
  CHAT_HEAD_INFO head;
  sockaddr_in caddr;
  int len = sizeof(caddr);
  int result;
  char *buffer = NULL;

  //接受消息
  result = recvfrom(socket_, (char*)&head, sizeof(head), 0, (sockaddr*)&caddr, &len);
  if (result < 0)
  {
    return 0;
  }

  if (head.len > 0)
  {
    buffer = new char[head.len];
    result = recvfrom(socket_, buffer, head.len, 0, (sockaddr*)&caddr, &len);
    if (result < 0)
    {
      delete[] buffer;
      reportError("recvfrom");
      return -1;
    }
  }


  if (parseCommand(head, buffer, caddr) < 0)
  {
    delete[] buffer;
    return -1;
  }


  delete[] buffer;
  return 0;
}

int ClientChat::parseCommand(CHAT_HEAD_INFO& head, char* buffer, sockaddr_in caddr)
{
  int result = 0;

  //回复消息
  switch (head.cmd)
  {
  case CHAT_LOGIN_OK:
  {
    showMessage("login ok");
    myuser_ = *(CHAT_USER_INFO*)buffer;
    break;
  }
  case CHAT_LOGIN_ERROR:
  {
    showMessage("login error");
    break;
  }
  case CHAT_SAY_ALL:
  {
    CHAT_SAY_ALL_INFO *info = (CHAT_SAY_ALL_INFO*)buffer;
    for (auto user : users_)
    {
      if (user.id == info->id)
      {
        printf("%s say:%s\n", user.name, info->msg);
        break;
      }
    }
    break;
  }
  case CHAT_SAY_TO:
  {
    CHAT_SAY_TO_INFO *info = (CHAT_SAY_TO_INFO*)buffer;
    for (auto user : users_)
    {
      if (user.id == info->sourceID)
      {
        printf("%s say:%s\n", user.name, info->msg);
        break;
      }
    }
    break;
  }
  case CHAT_ALL_USER:
  {
    CHAT_USER_INFO *userInfo = (CHAT_USER_INFO*)buffer;
    int count = head.len / sizeof(CHAT_USER_INFO);
    result = chatUserInfo(userInfo, count);
    showUserList();
    break;
  }
  case CHAT_HEART:
  {
    //发送心跳包
    result = sendHeart();
    break;
  }
  }
  return result;
}

void ClientChat::reportError(std::string s)
{
  LPVOID lpMsgBuf;
  FormatMessage(
    FORMAT_MESSAGE_ALLOCATE_BUFFER |
    FORMAT_MESSAGE_FROM_SYSTEM |
    FORMAT_MESSAGE_IGNORE_INSERTS,
    NULL,
    GetLastError(),
    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
    (LPTSTR)&lpMsgBuf,
    0,
    NULL
    );
  printf("%s==>%s", s.c_str(), (char*)lpMsgBuf);

  LocalFree(lpMsgBuf);
}

void ClientChat::showMessage(std::string msg)
{
  puts(msg.c_str());
}

void ClientChat::shoWelcom()
{
  printf("#################################\n");
  printf("#                               #\n");
  printf("#                               #\n");
  printf("#      Welcom BB Talk           #\n");
  printf("#                               #\n");
  printf("#                               #\n");
  printf("#################################\n");
}


DWORD WINAPI ClientChat::workThread(LPVOID lpParameter)
{
  ClientChat *obj = (ClientChat*)lpParameter;

  while (true)
  {
    if (obj->getCommand() < 0)
      break;
  }


  return 0;
}

void ClientChat::createWorkThread()
{
  CreateThread(NULL, 0, workThread, (LPVOID)this, 0, NULL);
}

int ClientChat::inputCommand()
{
  int input;
  printf("1.Login  2.Brocast  3.Talk  4.Logout\n");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
    login();
    break;
  case 2:
    sayAll();
    break;
  case 3:
    sayTo();
    break;
  case 4:
    logout();
    break;
  }
  return 0;
}


int ClientChat::login()
{
  //登录
  CHAT_HEAD_INFO head;
  CHAT_LOGIN_INFO login;
  int result;

  head.version = 1;
  head.cmd = CHAT_LOGIN;
  head.len = sizeof(login);

  printf("name:");
  scanf("%s", login.name);

  result = sendto(socket_, (const char*)&head, sizeof(head), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  result = sendto(socket_, (const char*)&login, sizeof(login), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  return 0;
}


int ClientChat::chatUserInfo(CHAT_USER_INFO *userInfo, int count)
{
  users_.clear();
  for (int i = 0; i < count; i++)
    users_.push_back(userInfo[i]);
 
  return 0;
}


int ClientChat::showUserList()
{
  printf("list:");
  for (auto info : users_)
    printf("[%d]%s ", info.id, info.name);
  printf("\n");

  return 0;
}


int ClientChat::logout()
{
  //登录
  CHAT_HEAD_INFO head;
  int result;

  head.version = 1;
  head.cmd = CHAT_LOGOUT;
  head.len = sizeof(myuser_);

  //代码重构
  result = sendto(socket_, (const char*)&head, sizeof(head), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  result = sendto(socket_, (const char*)&myuser_, sizeof(myuser_), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  return 0;
}

int ClientChat::sengPacket(int cmd, char *buffer, int len, int version)
{
  CHAT_HEAD_INFO head;
  head.version = version;
  head.cmd = cmd;
  head.len = len;

  //代码重构
  int result = sendto(socket_, (const char*)&head, sizeof(head), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  result = sendto(socket_, buffer, len, 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
    return -1;

  return 0;
}


int ClientChat::sayAll()
{
  CHAT_SAY_ALL_INFO info;
  info.id = myuser_.id;
  printf("msg:");
  scanf("%20s", info.msg);

  return sengPacket(CHAT_SAY_ALL, (char*)&info, sizeof(info));
}


int ClientChat::sayTo()
{
  CHAT_SAY_TO_INFO info;
  info.sourceID = myuser_.id;

  char name[20];
  printf("name:");
  scanf("%20s", name);
  for (auto user : users_)
  {
    if (strcmp(user.name, name) == 0)
    {
      info.destID = user.id;
      printf("msg:");
      scanf("%20s", info.msg);
      return sengPacket(CHAT_SAY_TO, (char*)&info, sizeof(info));
      break;
    }
  }

  return 0;
}

int ClientChat::sendHeart()
{
  CHAT_HEAD_INFO head;
  int result;

  head.version = 1;
  head.cmd = CHAT_HEART;
  head.len = sizeof(myuser_);
  
  result = sendto(socket_, (const char*)&head, sizeof(head), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
  {
    return -1;
  }
  result = sendto(socket_, (const char*)&myuser_, sizeof(myuser_), 0, (sockaddr*)&addr_, sizeof(addr_));
  if (result <= 0)
  {
    return -1;
  }
  return 0;
}

main.cpp

// Client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "ClientChat.h"

int main()
{
  ClientChat client;

  client.shoWelcom();

  client.createWorkThread();

  while (true)
  {
    if (client.inputCommand() < 0)
      break;
  }
  return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值