黑马程序员——08 Socket基础

------- Windows Phone 7手机开发.Net培训、期待与您交流! -------

——socket相关概念

1.Socket的英文原义为插座

作为通信机制,取后一种意思,也称套接字,用于描述ip地址和端口的痛心句柄

用一句话概括:即两个程序通信用的

2.socket类似于电话插座:

电话网=网络

电话的通话双方=相互通信的两个程序

电话号码=ip地址

——不同的端口对应不同的服务

http80端口

ftp21端口

smtp23端口

——Socket有两种类型

l  流式socketSTREAM):一种面向连接的Socket,针对面向连接的TCP服务应用

特点:安全,但效率低

l  数据报式SocketDARAGRAN:一种无连接的Socket,对英语无连接的UDP服务应用

特点:不安全(丢失,顺序混乱,在接收端要分析重排及要求重发),但效率高

——Socket一般应用模式(服务端和客户端)

                           --------------->1.welcoming socket

2.client socket<—bytes—> 3.connection socket

Clientprocess            Sever process  

过程:

1. 服务端welcoming socket开始监听端口(负责监听客户端连接信息)

2. 客户端client socket连接服务段指定端口(负责接收和发送服务端消息)

3. 服务端welcoming socket监听到客户端连接,创建connectionsocket(负责和客户端通信)

 

l  服务端的socket(只要需要两个)

1.一个负责接收客户端连接请求(但不负责与客户通信)

2.每成功分接收到一个客户端的连接便在服务端产生一个对应的Socket

a)接收到客户端连接时创建

b)为每个连接成功的客户端创建一个对应的Socket(负责和客户端通信)

l  客户端的Socket

1.必须制定要连接的服务端地址和端口

2.通过创建一个Socket对象来初始化一个到服务器端的TCP连接

——socket的通讯过程

l  服务端:

1. 申请一个socket

2. Socket.Bind();:绑定到一个ip地址和一个端口上

3. 开始侦听,等待接受连接

l  客户端:

1.申请一个socket

2. Socket.connect();:连接服务器(指明ip地址和端口号)

3.服务器接到连接请求后,产生一个新的socket(端口大于1024)与客户端连接并进行通讯,原侦听socket继续侦听

 ——ip和端口的作用

要想把一台计算机上的信息传递到另一台计算机上,必须通过ip和端口来识别和传递

:

计算机1ip192.168.1.1,其中有一个qq程序,这个程序的端口为100

计算机2ip192.168.1.2,其中有一个qq程序和msn程序,两个程序的端口分别为101102

 把计算机1qq中的信息传递给计算机2上的qq的过程:

1.      计算机1通过网络找到计算机2ip地址和计算机2上的qq程序端口

2.      通过网络传递信息给这台计算机2ip和端口

数据过程(192.168.1.1100>192.168.1.2101>192.168.1.1100

这样就能把计算机1qq的信息传递给计算机2上的qq

 ——服务端负责监听的客户端请求的套接字操作

过程:

引用:System.Net

            System.Net.Sockets;

1.创建 服务端 负责监听的套接字,参数(使用ip4寻址协议,使用流式连接,使用tcp协议传输数据)

2.获得文本框中的ip地址对象

3.创建包含ip和port的网络节点对象

4.将负责监听的套接字绑定到唯一的ip地址和port端口上

5.设置监听队列的长度

启动服务控件中的代码如下:

private void btnStartBegain_Click(object sender, EventArgs e)
        {
            //创建 服务端 负责监听的套接字,参数(使用ip4寻址协议,使用流式连接,使用tcp协议传输数据)
            Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //获得文本框中的ip地址对象
            IPAddress adress = IPAddress.Parse(txtIP.Text.Trim());
            //创建包含ip和port的网络节点对象
            IPEndPoint endpoint = new IPEndPoint(adress, int.Parse(txtPort.Text.Trim()));
            //将负责监听的套接字绑定到唯一的ip地址和port端口上
            socketWatch.Bind(endpoint);
            //设置监听队列的长度
            socketWatch.Listen(10);
            //开始监听客户端连接请求(Accept方法会阻断当前线程)
            Socket socConnetion=socketWatch.Accept();
            ShowMsg("客户端连接成功");
        }
        void ShowMsg(string msg)
        {
            txtMsg.AppendText(msg+"\r\n");
        }

 

——客户端连接服务器

要点解析:

因为Accept方法会阻断当前线程导致ui卡死,所以这里创建一个新线程来解决问题

1.服务端:

修改后的代码

public Form1()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls= false;//关闭对文本框的跨线程操作检查
        }
        Thread threadWatch = null;//创建一个监听套接字的线程
        Socket socketWatch = null;//负责监听的套接字
        privatevoidbtnStartBegain_Click(object sender, EventArgs e)
        {
            //创建 服务端 负责监听的套接字,参数(使用ip4寻址协议,使用流式连接,使用tcp协议传输数据)
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //获得文本框中的ip地址对象
            IPAddress adress = IPAddress.Parse(txtIP.Text.Trim());
            //创建包含ip和port的网络节点对象
            IPEndPoint endpoint = new IPEndPoint(adress, int.Parse(txtPort.Text.Trim()));
            //将负责监听的套接字绑定到唯一的ip地址和port端口上
            socketWatch.Bind(endpoint);
            //设置监听队列的长度
            socketWatch.Listen(10);
            //开始监听客户端
            threadWatch = new Thread(Watch);//实例化这个线程,并传入监听的套接字的方法
            threadWatch.IsBackground = true;//设置为后台线程
            threadWatch.Start();//启动线程
            ShowMsg("服务器监听成功~");
        }
        void Watch()
        {
         Socket socConnection =socketWatch.Accept();
                //创建运行通信套接字的Receive方法的线程
                ShowMsg("客户端连接成功~");
            }
        }
        void ShowMsg(string msg)
        {
            txtMsg.AppendText(msg+"\r\n");
        }

2.客户端:

连接服务端的代码:

privatevoid btnConnect_Click(object sender, EventArgs e)
        {
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());//获得ip
            IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));//网络节点
            //创建客户端套接字
            Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //向指定的ip和端口(enpoint)发送连接请求
             socketClient.Connect(endpoint);
             showMsg("与服务端连接成功");
         }
        void showMsg(string msg)
        {
            txtMsg.AppendText(msg + "\r\n");
        }


——循环监听客户端的连接请求

要点解析:

因为要不断的监听来自客户端的消息,所以服务端就必须不断的创建新的socket来负责监听,这个时候只需在Watch方法里添加一个while循环就能实现循环监听

修改后的Watch方法:

void Watch()
        {
            while (true)//持续不断的监听新的客户端连接请求
            {
                //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
                Socket socConnection =socketWatch.Accept();
                ShowMsg("客户端连接成功~");
            }
        }

——服务端向客户端发送数据,客户端循环接收数据

服务端:

//发送消息到客户端
        privatevoid button1_Click(object sender, EventArgs e)
        {
           
                string StrMsg =txtSendMsg.Text.Trim();
                //将字符串转成方便网络传送的二进制数组
                byte[] arrMsg =System.Text.Encoding.UTF8.GetBytes(StrMsg);
                socConnection.Send(arrMsg);
                ShowMsg("成功发送:" + StrMsg);
 
}

客户端:

同服务端循环监听客户端连接请求一样,这里需要不断的接收来自服务端的消息,所有需要用到while循环

客户端循环接收数据的代码:

void RecMsg()
        {
            while (true)
            {
                //定义一个接收的缓存区(2m字节组)
                byte[] arrMsgRec = new byte[1024 * 1024 * 2];
                //将接收到的数据存入arrMsgRec,并返回真正接收到的数据的长度
                int length = socketClient.Receive(arrMsgRe
               //此时是将数组所有的元素都转成字符串,而真正接收到的只有服务端发来的几个字符            
               string strArrMsgRec = System.Text.Encoding.UTF8.GetString(arrMsgRec, 0, length);
               showMsg("Admin:" + strArrMsgRec);
            }
          }

——服务端向多个客户端发送数据

要点解析:

因为要向多个客户端发送数据,所以服务端必须存储每个客户端的ip和端口,不然消息只会发给和服务端最后连接的客户端上,这个时候就需要用到字典泛型来存储每个和服务连接的客户端的ip和端口

服务端:

1. 首先实例化一个字典集合:

//保存了服务器端所有负责和客户端通信的套接字

Dictionary<string, Socket> Dic = new Dictionary<string, Socket>();

2.客户端每请求一次连接,服务端就在列表中存储该客户端的ip地址并相应的生成一个Key(键值),并ip地址为键

修改后的Watch方法:

void Watch()
        {
            while (true)//持续不断的监听新的客户端连接请求
            {
                //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
                Socket socConnection =socketWatch.Accept();
                //向列表控件中添加一个客户端的ip端口字符串,作为客户端的唯一标识
               lbOnline.Items.Add(socConnection.RemoteEndPoint.ToString());
                //将于客户端通信的套接字对象 socConnection 添加到键值对集合中,并以客户端ip端口作为键
               Dic.Add(socConnection.RemoteEndPoint.ToString(), socConnection);
               
                ShowMsg("客户端连接成功~" + socConnection.RemoteEndPoint.ToString());
            }
        }

3.获取列表中的Key,以便选择发送信息,通过key找到字典集合中对应的与某个客户端通信的 套接字 的send方法,发送数据给对方

修改后的发送消息到客户端的代码:

//发送消息到客户端
        privatevoid button1_Click(object sender, EventArgs e)
        {
    
                string StrMsg =txtSendMsg.Text.Trim();
                //将要发送的字符串专成utf8对应的字节数组
                byte[] arrMsg =System.Text.Encoding.UTF8.GetBytes(StrMsg);
                //获得列表中选中的key
                string strClientKey =lbOnline.Text;
                //通过key找到字典集合中对应的 与某个客户端通信的 套接字 的send方法,发送数据给对方
                Dic[strClientKey].Send(arrSendMsg);
                //socConnection.Send(arrMsg);
                ShowMsg("成功发送:" + StrMsg);
       }

——服务端群发消息

要点解析:要想客户端群发消息就需要拿到字典里所有的键值,这时就需要用一个循环

群发消息代码:

//服务器向客户端群发消息
        privatevoid btnSendToAll_Click(object sender, EventArgs e)
        {
            string StrMsg =txtSendMsg.Text.Trim();
            byte[] arrMsg =System.Text.Encoding.UTF8.GetBytes(StrMsg);
            foreach (Socket S in Dic.Values)
            {
                S.Send(arrMsg);
 
            }
            ShowMsg("群发成功");

 ——服务端通过新建线程见监听客户端消息

要点解析:

因为socketReceive方法会阻断当前线程,所以需要为每一个负责通信的套接字创建一个对应的新的线程,负责调用通信套接字的Receive方法来监听客户端的数据。

1.服务端

a)  首先复制一份客户端接收消息的方法,并做一些修改:

     //接收客户端的消息
        void RecMsg(object socketClientPara)
        {
            while (true)
            {
                Socket socketClient =socketClientPara asSocket;
                //定义一个接收的缓存区(2m字节组)
                byte[] arrMsgRec = new byte[1024 * 1024 * 2];
                //将接收到的数据存入arrMsgRec,并返回真正接收到的数据的长度
                int length = socketClient.Receive(arrMsgRec);
                //此时是将数组所有的元素都转成字符串,而真正接收到的只有服务端发来的几个字符          
                string strArrMsgRec = System.Text.Encoding.UTF8.GetString(arrMsgRec,1, length - 1);
                ShowMsg("客户端:"+strArrMsgRec);
                }
          }

b)在Watch方法里创建新线程:负责调用通信套接字的Receive方法来监听客户端数据:

修改后的Watch方法:

void Watch()
        {
            while (true)//持续不断的监听新的客户端连接请求
            {
                //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
                Socket socConnection =socketWatch.Accept();
                //向列表控件中添加一个客户端的ip端口字符串,作为客户端的唯一标识
               lbOnline.Items.Add(socConnection.RemoteEndPoint.ToString());
                //将于客户端通信的套接字对象 socConnection 添加到键值对集合中,并以客户端ip端口作为键
                Dic.Add(socConnection.RemoteEndPoint.ToString(),socConnection);
                //创建运行通信套接字的Receive方法的线程
                Thread threadRec = new Thread(RecMsg);
                threadRec.IsBackground = true;//设置为后台线程
                threadRec.Start(socConnection);//传入参数,启动线程
                //将线程保存到字典里,方便以后做“踢人”功能
               DicThread.Add(socConnection.RemoteEndPoint.ToString(), threadRec);
                ShowMsg("客户端连接成功~" + socConnection.RemoteEndPoint.ToString());
            }
        }

c)在启动服务控件中添加新线程来负责监听:

修改后的代码:

privatevoidbtnStartBegain_Click(object sender, EventArgs e)
        {  //创建 服务端 负责监听的套接字,参数(使用ip4寻址协议,使用流式连接,使用tcp协议传输数据)
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //获得文本框中的ip地址对象
            IPAddress adress = IPAddress.Parse(txtIP.Text.Trim());
            //创建包含ip和port的网络节点对象
            IPEndPoint endpoint = new IPEndPoint(adress, int.Parse(txtPort.Text.Trim()));
            //将负责监听的套接字绑定到唯一的ip地址和port端口上
            socketWatch.Bind(endpoint);
            //设置监听队列的长度
            socketWatch.Listen(10);
            //开始监听客户端
            threadWatch = new Thread(Watch);//实例化这个线程,并传入监听的套接字的方法
            threadWatch.IsBackground = true;//设置为后台线程
            threadWatch.Start();//启动线程
            ShowMsg("服务器监听成功~");
 
 
        }

——客户端向服务端发送消息

同服务端想客户端发送消息一样:

代码如下:

//向服务器发送消息文本消息
        privatevoid btnSendMsg_Click(object sender, EventArgs e)
        {
            //将控件内的信息定义成字符串
            string strMsg =txtSendMsg.Text.Trim();
            //将字符串转换成方便网络传输的二进制数组
            byte[] arrMsg =System.Text.Encoding.UTF8.GetBytes(strMsg);
//传输数组
            socketClient.Send(arrMsgSend);
            showMsg("成功发送:" + strMsg);
        }

——客户端向服务器发送文件和消息

(一)客户端:

1.  发送文件:

a)  选择要发送发文件:

//选择要发送的文件
        privatevoidbtnChooseFile_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog()== System.Windows.Forms.DialogResult.OK)//如果在选择窗体上选择ok
            {
                txtChooseFile.Text =ofd.FileName;//在文本控件中显示文件的路径
            }
        }

b) 发送文件:

要点解析:

为了让传输数据中的第一个元素为我们规定的标示符,就需要定义一个新的数据数组,然后把原数组从下标为0的元素拷贝,新数组从下标为1的元素开始存储,然后设置新数组下标为0的元素的值

//向服务端发送文件
        privatevoid btnSendFile_Click(object sender, EventArgs e)
        {   
               //用文件流来打开用户选择的文件
            using (FileStream fs = new FileStream(txtChooseFile.Text,FileMode.Open))
            {
                byte[] arrFile = new byte[1024 * 1024 * 2];//定义一个2m的数组(缓存区)
                //将文件数据读到数组 arrFileSend中,并获得读取的真实数据长度length
                int length =fs.Read(arrFile, 0, arrFile.Length);
                byte[] arrFileSend = new byte[length + 1];
                arrFileSend[0] = 1;//代表发送的是文件数据
                //第一种方法
                //for (int i = 0; i< length; i++)
                //{
                //    arrFile[i] = arrFileSend[i + 1];
                //}
                //第二种方法:缺点,不能规定从第几个元素开始拷贝
                //arrFile.CopyTo(arrFileSend.length);
                //将arrFile数组中的元素从第0个开始拷贝,拷贝到arrFileSend数组里,从第一个位置开始存储,拷贝length个数据
                Buffer.BlockCopy(arrFile, 0, arrFileSend, 1, length);
                socketClient.Send(arrFileSend);
                showMsg("发送成功");
            }
        }

2. 发送消息:

修改后的发送消息的代码:

//向服务器发送消息文本消息
 privatevoid btnSendMsg_Click(object sender, EventArgs e)
        {
            //将控件内的信息定义成字符串
            string strMsg =txtSendMsg.Text.Trim();
            //将字符串转换成方便网络传输的二进制数组
            byte[] arrMsg =System.Text.Encoding.UTF8.GetBytes(strMsg);
            byte[] arrMsgSend = new byte[arrMsg.Length + 1];
            arrMsgSend[0] = 0;//数据的第一个元素标识为0
            Buffer.BlockCopy(arrMsg,0, arrMsgSend, 1, arrMsg.Length-1);//拷贝数据
            //传输数组
            socketClient.Send(arrMsgSend);
            showMsg("成功发送:" + strMsg);
        }


 

(二)服务端

要点解析:因为是根据接收的文件数据的第一个元素来判断接收的是文本还是文件,所以服务端接收消息的方法就需要用if来判断数据的第一个元素然后做出相应的操作

a)修改后的RecMsg方法:

//接收客户端的消息

        void RecMsg(object socketClientPara)

        {

            while (true)

            {

                Socket socketClient =socketClientPara asSocket;

                //定义一个接收的缓存区(2m字节组)

                byte[] arrMsgRec = new byte[1024 * 1024 * 2];

                //将接收到的数据存入arrMsgRec,并返回真正接收到的数据的长度

                int length =socketClient.Receive(arrMsgRec);

                

                //此时是将数组所有的元素都转成字符串,而真正接收到的只有服务端发来的几个字符

                if (arrMsgRec[0] == 0)//判断发送过来的数据的第一个元素是0,则代表发送过来的数据为文字数据

                {

                    string strArrMsgRec =System.Text.Encoding.UTF8.GetString(arrMsgRec,1, length - 1);

                    ShowMsg("客户端:"+strArrMsgRec);

                }

                elseif (arrMsgRec[0] == 1)//第一个元素是1,则代表发送过来的数据为文件数据(图片/视频/文件....)

                {

                    SaveFileDialog sfd = new SaveFileDialog();//保存文件选择框对象

                    if (sfd.ShowDialog()== System.Windows.Forms.DialogResult.OK)//用户选择文件路径后

                    {

                        string FileSavePath =sfd.FileName;//获得要保存的文件路径

                        //创建一个文件流,让文件流来根据文件路径来创建文件

                        using (FileStream fs = new FileStream(FileSavePath, FileMode.Create))

                        {

                            fs.Write(arrMsgRec,1, length - 1);

                            ShowMsg("文件接收成功:" + FileSavePath);

                        }

 

                    }

                }

            }

        }

——套接字的异常捕捉

要点解析:分别在负责连接,发送,接收等代码外侧用try-catch方法来捕捉异常并做出相应的处理

对启动服务的控件连接套接字做异常捕捉:

//客户端发送连接请求到服务端
        privatevoid btnConnect_Click(object sender, EventArgs e)
        {
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());//获得ip
            IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));//网络节点
            //创建客户端套接字
            socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //向指定的ip和端口(enpoint)发送连接请求
            try
            {
                socketClient.Connect(endpoint);
            }
            catch (SocketException ex)
            {
                showMsg("异常:"+ex.Message);
                return;
            }
            catch (Exception ex)
            {
                showMsg("异常:" + ex.Message);
                return;
            }
            threadClient = new Thread(RecMsg);
            threadClient.IsBackground = true;
            threadClient.Start();
            showMsg("与服务端连接成功");
 
        }

对接收消息的方法进行异常捕捉:

//接收服务端发来的消息
  void RecMsg()
        {
            while (true)
            {
                //定义一个接收的缓存区(2m字节组)
                byte[] arrMsgRec = new byte[1024 * 1024 * 2];
                //将接收到的数据存入arrMsgRec,并返回真正接收到的数据的长度
                int length = 0;
                try
                {
                    length =socketClient.Receive(arrMsgRec);
                }
                catch (SocketException ex)
                {
 
                    showMsg("异常;"+ex.Message);
                    break;
                }
                catch (Exception ex)
                {
 
                    showMsg("异常;" + ex.Message);
                    break;
                }
                if (arrMsgRec[0] == 0)
                {
                    //此时是将数组所有的元素都转成字符串,而真正接收到的只有服务端发来的几个字符
                    string strArrMsgRec =System.Text.Encoding.UTF8.GetString(arrMsgRec,1, length- 1);
                    showMsg("Admin:" + strArrMsgRec);
                }
                elseif (arrMsgRec[0] == 1)
                {
                    SaveFileDialog sfd = new SaveFileDialog();
                    if (sfd.ShowDialog()==System.Windows.Forms.DialogResult.OK)
                    {
                        using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create))
                        {
                            fs.Write(arrMsgRec,1, length - 1);
                            showMsg("文件接收成功"+sfd.FileName);
                        }
                    }
                }
            }
        }

——socket方法

————————类————————

IPAddress类:包含了一个IP地址

IPEndPoint:包含了一个对应的IP地址和端口号

————————方法———————

Socket();创建一个Socket

Bind();绑定一个本地的IP和端口号(IPEndPoint

Listen();Socket侦听传入的连接尝试,并指定侦听队列容量

Connect();初始化与另一个Socket的连接

Accept();接收连接病患会一个新的Socket

Send();输出数据到Socket

Receive();Socket中读取数据

Close();关闭Socket(销毁连接)

黑马程序员是一家IT培训机构,提供各种技术培训课程,包括网络通信相关的课程。在网络通信中,socket是一种编程接口,用于实现不同主机之间的通信。通过socket函数创建一个套接字,指定域名、类型和协议。域名可以是AF_INET、AF_INET6或AF_UNIX,类型可以是SOCK_STREAM(用于TCP通信)或SOCK_DGRAM(用于UDP通信),协议可以是0表示自动选择适合的协议。创建成功后,套接字会返回一个文件描述符,用于在后续的通信中进行读写操作。 在TCP通信中,服务器和客户端的流程大致相同。服务器首先使用socket函数创建套接字,然后使用bind函数绑定服务器地址结构,接着使用listen函数设置监听上限。服务器通过accept函数阻塞监听客户端连接,并使用read函数读取客户端传来的数据,进行相应的处理后,使用write函数将处理后的数据写回给客户端,最后使用close函数关闭套接字。客户端也是先使用socket函数创建套接字,然后使用connect函数与服务器建立连接,之后使用write函数将数据写入套接字,再使用read函数读取服务器返回的数据,最后使用close函数关闭套接字。 在UDP通信中,服务器和客户端的流程也有所不同。服务器使用socket函数创建套接字,指定类型为SOCK_DGRAM,然后使用bind函数绑定服务器地址结构。服务器通过recvfrom函数接收客户端传来的数据,并进行相应的处理,最后使用sendto函数将处理后的数据发送回给客户端。客户端同样使用socket函数创建套接字,然后通过sendto函数将数据发送给服务器,再使用recvfrom函数接收服务器返回的数据。 总之,socket网络通信是通过创建套接字实现不同主机之间的通信。根据使用的协议不同,可以选择TCP或UDP通信方式。服务器和客户端根据流程进行相应的操作,实现数据的传输和交互。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值