.net 中socket的异步长连接实例

最近在熟悉socket通讯,写一个简单的聊天实例,希望对以后的初学者有帮助.....

服务器端的界面

 

 public partial class ClientSeverForm : Form
    {
       //private System.ComponentModel.Container components = null;
        //Clients数组保存当前在线用户的Client对象
        internal static Hashtable clients = new Hashtable();
        //该服务器默认的监听端口号
        private TcpListener listener;
        // 服务器可以支持的客户端的最大连接数
        static int MAX_NUM = 100;
        //服务器开始服务的标志
        internal static bool SocketServiceFlag = false;
    
        //构造函数
        public ClientSeverForm()
        {
            //windows窗口设计器支持所必须的
            InitializeComponent();
            //在 InitializeComponent();调用后添加任何构造函数代码
        }

        #region 获取地址
        获取本地局域网IP地址
        private string getIPAddress()
        {
            IPAddress[] AddressList = Dns.GetHostByName(Dns.GetHostName()).AddressList;// Dns.GetHostByName(Dns.GetHostName()).AddressList;
            if (AddressList.Length < 1)
            {
                return "";
            }
            return AddressList[0].ToString();
        }
        #endregion

        #region 获取服务器端口号,并判断端口号是否合法
        //获取服务器的端口号,判读端口号是否合法
        private int getValidPort(string port)
        {
            int lport;
            //测试端口号是否有效
            try
            {
                //是否为空
                if (port == "")
                {
                    Console.WriteLine("端口号为空,不启动服务器");
                }
                //将端口号转为int类型
                lport = System.Convert.ToInt32(port);
            }
            catch (Exception e)
            {
                Console.WriteLine("无效的端口号:"+e.ToString());
                this.rtbSocketMsg.AppendText("无效的端口号:"+e.ToString());
                return -1;
            }
            return lport;
        }
        #endregion

        #region 点击"启动Socket"按钮触发的事件,开始监听
        //启动"Socket",开始监听
        private void btnSocketStart_Click(object sender, EventArgs e)
        {
            //参数为端口号的值
            int port = getValidPort(tbSocketPort.Text);
            if (port < 0)
            {
                return;
            }
            string ip = this.getIPAddress();
            try
            {
                IPAddress ipAdd = IPAddress.Parse(ip);
                //创建服务器套接字
                listener = new TcpListener(ipAdd,port);
                //开始监听服务器端口
                listener.Start();
                this.rtbSocketMsg.AppendText("Socketr服务器已经启动,正在监听" + ip + "端口号:" + this.tbSocketPort.Text + "\n");

                //启动一个新的线程,执行方法this.StratSockListen
                //以便在一个独立的进程中执行确认与客户端Socket连接的操作
                ClientSeverForm.SocketServiceFlag = true;
                Thread thread = new Thread(new ThreadStart(this.StartSocketListen));
                thread.Start();
                this.btnSocketStart.Enabled = false;
                this.btnStop.Enabled = true;
            }
            catch (Exception ex)
            {
                this.rtbSocketMsg.AppendText(ex.Message.ToString() + "\n");
            }
        }
        #endregion

        #region 当有用户添加进来,将其添加到用户列表,更新在线人数
        public void addUser(string username)
        {
            //将刚链接的用户名加入到当前在线用户列表
            SetControlText(rtbSocketMsg, username + "已经加入\n");          
            //this.rtbSocketMsg.AppendText(username + "已经加入\n");
            //跟新在线人数加1
            SetControlList(lbSocketClients, username);
           // this.lbSocketClients.Items.Add(username);  
            //显示在线人数
            SetControlText(tbSocketClientsNum,clients.Count.ToString());
            //this.tbSocketClientsNum.Text = System.Convert.ToString(clients.Count);
        }
        #endregion       

        #region 启动新的进程来处理和该客户端的信息交互
        //在新的线程中的操作,它主要用于当接收到一个客户端请求时,确认与客户端的链接
        //并且立刻启动一个新的进程来处理和该客户端得信息交互
        private void StartSocketListen()
        {
            while (ClientSeverForm.SocketServiceFlag)
            {
                try
                {
                    //当接收到一个客户端请求时,确认与客户端的链接
                    if (listener.Pending())
                    {
                        //接收挂起的连接请求
                        Socket socket = listener.AcceptSocket();
                        if (clients.Count >= MAX_NUM)
                        {
                            this.rtbSocketMsg.AppendText("已经达到了最大连接数:" + MAX_NUM + ",拒绝接收新的链接");
                            socket.Close();
                        }
                        else
                        {
                            //启动一个新的线程
                            //执行方法this.ServiceClient,处理用户相应的请求
                            Client client = new Client(this, socket);
                            Thread clientService = new Thread(new ThreadStart(client.ServiceClient));
                            clientService.Start();
                        }
                    }
                    //是线程休息,提高系统性能
                    Thread.Sleep(200);
                }
                catch (Exception ex)
                {
                    this.rtbSocketMsg.AppendText(ex.Message.ToString() + "\n");
                }
            }
        }
        #endregion

        #region 输入端口号的文本框发生改变时
        private void tbSocketPort_TextChanged(object sender, EventArgs e)
        {
            //当端口号为空时,启动按钮亏掉不可用
            this.btnSocketStart.Enabled = (this.tbSocketPort.Text != "");
        }
        #endregion

        #region 点击“停止”按钮,触发的事件
        private void btnStop_Click(object sender, EventArgs e)
        {
            ClientSeverForm.SocketServiceFlag = false;
            this.btnSocketStart.Enabled = true;
            this.btnStop.Enabled = false;
        }
        #endregion    
 
        #region 当有用户离开,将其在列表中删除,在线人数跟新减少一个
        public void removeUser(string username)
        {
            SetControlText(rtbSocketMsg, username);
            // this.rtbSocketMsg.AppendText(username + "已经离开");
            //将其用户名移除用户表
            SetExit(lbSocketClients,username);
            //this.lbSocketClients.Items.Remove(username);
            //跟新在线人数
            //this.tbSocketClientsNum.Text =System.Convert.ToString(clients.Count);
            SetControlText(tbSocketClientsNum,clients.Count.ToString());
        }
        #endregion

        #region 用于在获取当前全部在线用户的列表,备用户名中间用“|”分割,该函数在LIST命令时调用
        public string GetUserList()
        {
            string Rtn = "";
            for (int i = 0; i < lbSocketClients.Items.Count; i++)
            {
                Rtn = Rtn + lbSocketClients.Items[i].ToString() + "|";
            }
            return Rtn;
        }
        #endregion
      
        #region 用于更新界面,添加系统或者聊天信息,参数是要显示的字符串
        public void updateUI(string msg)
        {
            SetControlText(rtbSocketMsg,msg+"\n");
            //this.rtbSocketMsg.AppendText(msg + "\n");
        }
        #endregion

        #region 当关闭窗口,会执行窗口的closing事件
        private void btn_Close_Click(object sender, System.ComponentModel.CancelEventArgs e)
        {
            ClientSeverForm.SocketServiceFlag = false;
        }
        #endregion    
  
        #region 定义一个委托
        private delegate void SetControlTextDelegate(Control control, string txt);
        //判断当前控件是否在此线程中,当是此线程时,调用该线程
        //如果不是同一线程,让其回调
        private void SetControlText(Control control, string txt)
        {
            if (control.InvokeRequired)
            {
                var d = new SetControlTextDelegate(SetControlText);
                BeginInvoke(d, new object[] { control, txt });
            }
            else
            {
                control.Text = txt;
            }
        }

        private delegate void SetControlListDelegate(ListBox listbox, string txt);
        //判断当前控件是否在此线程中,当是此线程时,调用该线程
        //如果不是同一线程,让其回调
        private void SetControlList(ListBox listbox, string item)
        {
            if (listbox.InvokeRequired)
            {
                var d = new SetControlListDelegate(SetControlList);
                BeginInvoke(d, new object[] { listbox, item });
            }
            else
            {
                listbox.Items.Add(item);
            }
        }

        private delegate void SetExitDelegate(ListBox listbox, string item);
        //判断当前控件是否在此线程中,当是此线程时,调用该线程
        //如果不是同一线程,让其回调
        private void SetExit(ListBox listbox, string item)
        {
            if (listbox.InvokeRequired)
            {
                var d = new SetExitDelegate(SetExit);
                BeginInvoke(d, new object[] { listbox, item });
            }
            else
            {
                listbox.Items.Remove(item);
            }
        }
        #endregion
    }

-----------------------------------------------------------------------------------------------------------------------------------------------------

______________________________________________________________________________________________--

Client类——一个中间类的逻辑处理

public class Client
    {
        private  string name;
        private Socket currentSocket ;//保存与当前用户连接的Socket对象
        private string ipAddress;
        private ClientSeverForm server;

        public ClientSeverForm Server
        {
            get { return server; }
            set { server = value; }
        }
        //保存当前链接的状态:closed      
        private string state="closed";

        public Client(ClientSeverForm server, Socket clientSocket)
        {
            this.server = server;
            this.currentSocket = clientSocket;
            ipAddress = getRemoteIPAddress();
        }      

        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
            }
        }

        public Socket CurrentSocket
        {
            get
            {
                return currentSocket;
            }
            set
            {
                currentSocket = value;
            }
        }

        public string IpAddress
        {
            get
            {
                return ipAddress;
            }           
        }

        //获取ip地址
        private string getRemoteIPAddress()
        {
            return((IPEndPoint)currentSocket.RemoteEndPoint).Address.ToString();
        }

        //服务器处理聊天命令时,需要向客户端发送命令或者发送响应信息,这些命令的发送都是通过SendToClient()函数实现的
        //SendToClient()方法实现了向客户端发送命令请求的功能
        private void SendToClient(Client client, string msg)
        {

            System.Byte[] message = System.Text.Encoding.Default.GetBytes(msg.ToCharArray());
            client.CurrentSocket.Send(message, message.Length, 0);
        }

        /// <summary>
        /// Client类中提供了一个ServiceClient()函数,用于与客户端进行通信,接受客户端请求
        /// 根据不同命令执行不同操作,将结果返回给客户端
        /// </summary>
        public void ServiceClient()
        {
            string[] tokens = null;
            byte[] buff = new byte[1024];
            bool keepConnect = true;//退出循环,关闭连接
            //循环的与客户端交互,直到客户端发送exit
            //讲keepConnect跟新为false ,推出循环,关闭连
            while (keepConnect && ClientSeverForm.SocketServiceFlag)
            {
                try
                {
                    if (this.CurrentSocket == null || this.CurrentSocket.Available < 1)
                    {
                        Thread.Sleep(300);
                        continue;
                    }
                    //接收数据并保存入buff数组中
                    int len = this.CurrentSocket.Receive(buff);
                    //讲字符数组转为字符串,
                    string clientCommand = System.Text.Encoding.Default.GetString(buff, 0, len);
                    //tokens[0]中保存了命令标志符(CONN,CHAT,PRIV,LIST,EXIT)
                    tokens = clientCommand.Split(new Char[] { '|' });
                    if (tokens == null)
                    {
                        Thread.Sleep(200);
                        continue;
                    }
                }
                catch (Exception e)
                {
                   Server.updateUI("发生异常");
                }

                //以上代码是用于服务器初始化和接收客户端发来的请求,对数据解析后,把命令转为数组形式
                //下面讲服务器根据不同的命令进行相应的处理
                if (tokens[0] == "CONN")
                {
                    //此时接收到的命令格式为
                    //命令标识符(CONN)发送者的用户名
                    //tokens[1]中保存发送者的用户名
                    this.Name = tokens[1];
                    if (ClientSeverForm.clients.Contains(this.name))
                    {
                        SendToClient(this,"ERR|User"+this.name+"已经存在");
                    }
                    else
                    {
                        Hashtable synclients = Hashtable.Synchronized(ClientSeverForm.clients);
                        synclients.Add(this.name, this);
                        // 更新界面
                        server.addUser(this.name);

                        //对每一个当前在线的用户发送JOIN消息命令和LIST息命令
                        //以此来更新客户端的当前在线用户列表
                        System.Collections.IEnumerator myEnumerator = ClientSeverForm.clients.Values.GetEnumerator();
                        while (myEnumerator.MoveNext())
                        {
                            Client client = (Client)myEnumerator.Current;
                            SendToClient(client, "JOIN|" + tokens[1] + "|");
                            Thread.Sleep(100);
                        }
                        //更新状态
                        state = "connected";
                        SendToClient(this, "ok");

                        //向客户端发t送list命令,以此跟新客户端的当前在线用户列表
                        string msgUsers = "LIST|" + server.GetUserList();
                        SendToClient(this, msgUsers);
                    }
                }
                else if (tokens[0] == "LIST")
                {
                    if (state == "connected")
                    {
                        //向客户端发送list命令,以此跟新用户列表
                        string msgUsers = "LIST|" + server.GetUserList();
                        SendToClient(this, msgUsers);
                    }
                    else
                    {
                        SendToClient(this, "ERR|state error,Please login first");
                    }
                }
                else if (tokens[0] == "CHAT")
                {
                    if (state == "connected")
                    {
                        //此时接收到的命令的格式为:
                        //命令标识符,发送者的用户名:发送内容|向所有当前在线的用户转发此消息
                        System.Collections.IEnumerator myEnumerator = ClientSeverForm.clients.Values.GetEnumerator();//ChatClientForm.clients.Values.GetEnumerator();
                        while (myEnumerator.MoveNext())
                        {
                            Client client = (Client)myEnumerator.Current;
                            //讲“发送者的用户名:发送内容“转发给用户””
                            SendToClient(client, tokens[1]);
                        }
                        server.updateUI(tokens[1]);
                    }
                    else
                    {
                        SendToClient(this, "ERR|state error,Please login first");
                    }
                }
                //如果是PRIV命令,就把信息转发给PRIV命令中指定的接受者和发送者
                else if (tokens[0] == "PRIV")
                {
                    if (state == "connected")
                    {
                        //此时接收到的命令格式为
                        //命令标识符(PRIV)发送呢者用户名(接受者用户名|发送内容|tokens[1]中保存了发送者的用户名)ro
                        string sender = tokens[1];
                        //tokens[2] 中保存了接受者 的名字
                        string receiver = tokens[2];
                        //tokens[3]保存了发送的内容
                        string content = tokens[3];
                        string message = sender + "--->" + receiver + ":" + content;
                        //仅将信息转发给发送者和接受者
                        if (ClientSeverForm.clients.Contains(sender))
                        {
                            SendToClient((Client)ClientSeverForm.clients[sender], message);

                        }
                        if (ClientSeverForm.clients.Contains(receiver))
                        {
                            SendToClient((Client)ClientSeverForm.clients[receiver], message);
                        }
                        server.updateUI(message);
                    }
                    else
                    {
                        SendToClient(this, "ERR|state error,Please login first");
                    }
                }
                else if (tokens[0] == "EXIT")
                {
                    //此时接收到的命令的格式:命令标石符|发送者的名字|
                    //向所有当前用户发送已经离线的信息
                    if (ClientSeverForm.clients.Contains(tokens[1]))
                    {
                        Client client = (Client)ClientSeverForm.clients[tokens[1]];

                        //将该用户对应的Client对象从clients中删除
                        Hashtable syncClients = Hashtable.Synchronized(ClientSeverForm.clients);
                        syncClients.Remove(client.Name);
                        server.removeUser(client.Name);

                        //向客户端发送QUIT命令
                        string message = "QUIT|" + tokens[1];
                        System.Collections.IEnumerator myEnumberator = ClientSeverForm.clients.Values.GetEnumerator();
                        while (myEnumberator.MoveNext())
                        {
                            Client c = (Client)myEnumberator.Current;
                            SendToClient(c, message);
                        }
                        server.updateUI("QUIT");
                    }
                    break;
                }
                Thread.Sleep(200);
            }
        }  
    }

_______________________________________________________________________

——————————————————————————————————————————————————————————

客户端的界面

 public partial class ChatClientForm : Form
    {
        public ChatClientForm()
        {
            InitializeComponent();
        }

        //与服务器的连接
        TcpClient tcpClient;
        //与服务器数据交互的流通道
        private NetworkStream Stream;
        //客户端的状态
        private static string CLOSED = "closed";
        private static string CONNECTED = "connected";
        //定义当前客户的状态
        private string state = CLOSED;

        private bool stopFlag;
        private Color color;

        #region 点击“登陆”
        private void btnLogin_Click(object sender,System.EventArgs e)
        {
            if(state==CONNECTED)
            {
                return;
            }
            if(this.tbUserName.Text.Length==0)
            {
                MessageBox.Show("请输入您的昵称","提示信息");
                this.tbUserName.Focus();
                return;
            }
            try
            {
               // Client clients = new Client();
                //创建一个客户端套接字,他是logion的一个套接字,将被传递给ChatClient窗体
                tcpClient=new TcpClient();
                //向指定的IP地址服务器发送连接请求
                tcpClient.Connect(IPAddress.Parse(txtHost.Text),Int32.Parse(txtport.Text));
                //获得与服务器数据交互的流通道
                Stream=tcpClient.GetStream();
                //启动一个新的线程,执行方法this.ServerResponse(),
                //以便来响应从服务器发回的信息
                Thread thread=new Thread(new ThreadStart(this.ServeerResponse));
                thread.Start();
                //向服务器发送"CONN"请求命令,此命令的格式与服务器的定义的格式一致
                //命令格式为:命令标示符CONN| 发送者的名字
                string cmd ="CONN|"+this.tbUserName.Text+"|";
                //将字符串转化为字符数组
                Byte[] outbytes=System.Text.Encoding.Default.GetBytes(cmd.ToCharArray());
                Stream.Write(outbytes,0,outbytes.Length);          
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        #endregion
       
        #region 点击“发送”按钮
        private void btnSend_Click(object sender, System.EventArgs e)
        {
            try
            {
                if (!this.cbPrivate.Checked)
                {
                    //此时命令的格式是:
                    //命令标示符(CHAT)|发送者的名字|发送内容
                    string message = "CHAT|" + this.tbUserName.Text + tbSendContent.Text + "|";
                    tbSendContent.Text = "";
                    tbSendContent.Focus();
                    //将字符串转为字符数组
                    Byte[] outbytes = System.Text.Encoding.Default.GetBytes(message.ToCharArray());
                    Stream.Write(outbytes, 0, outbytes.Length);
                }
                else
                {
                    if (lstUsers.SelectedIndex == -1)
                    {
                        MessageBox.Show("请在一个列表中选择一个用户","提示信息");
                        return;
                    }
                    string receiver = lstUsers.SelectedItem.ToString();
                    //命令标示符(PRIV|)|发送者的名字|接收者的用户名|发送内容
                    string message = "PRIV|" + this.tbUserName.Text + "|" + receiver + "|" + tbSendContent.Text + "|";
                    tbSendContent.Text="";
                    tbSendContent.Focus();
                    //将字符串转化为字符数组
                    byte[] outbytes = System.Text.Encoding.ASCII.GetBytes(message.ToCharArray());
                    Stream.Write(outbytes,0,outbytes.Length);
                }
            }
            catch(Exception ex)
            {
                this.rtbMsg.AppendText("网络发生错误");
            }
        }
        #endregion

        #region 用于接收服务器发回的信息
        //根据不同的命令,执行相应的操作
        private void ServeerResponse()
        {
            //定义一个 byte数组,用于接收从服务器发回的信息
            //每次所能接收的数据包的最大长度为1024个字符
            byte[] buff=new byte[1024];
            string msg="";
            int len;
            try
            {
                if (!Stream.CanRead)
                {
                    return;
                }
                stopFlag = false;
                while (!stopFlag)
                {
                    //从流中得到数据,并存入到buff字符数组中
                    len = Stream.Read(buff, 0, buff.Length);
                    if (len < 1)
                    {
                        Thread.Sleep(200);
                        continue;
                    }
                    //将字符数组转化为字符串
                    msg = System.Text.Encoding.Default.GetString(buff, 0, len);
                    msg.Trim();
                    string[] tokens = msg.Split(new Char[]{'|'});
                    //tokens = msg.Split(new Char[] { '|' });//takens[0]中保存了命令标识符(LIST或JOIN或QUIT)
                    if (tokens[0].ToUpper() == "OK")
                    {
                        //处理响应
                        add("命令执行成功");
                    }
                    else if (tokens[0].ToUpper() == "ERR")
                    {
                        //命令执行错误
                        add("命令执行错误:" + tokens[1]);
                    }
                    else if (tokens[0] == "LIST")
                    {
                        //此时从服务器返回的消息格式:
                        //命令标志符(LIST|用户名|用户名2|用户名3...
                        //跟新在线用户列表
                        lstUsers.Items.Clear();
                        for (int i = 1; i < tokens.Length - 1; i++)
                        {
                            lstUsers.Items.Add(tokens[i].Trim());
                        }
                    }
                    else if (tokens[0] == "JOIN")
                    {
                        //此时从服务器返回的消息格式
                        add(tokens[1] + "" + "已经进入了聊天室");
                        SetControlList(lstUsers,tokens[1]);
                       // this.lstUsers.Items.Add(tokens[1]);
                        if (this.tbUserName.Text == tokens[1])
                        {
                            this.state = CONNECTED;
                        }
                    }
                    else if (tokens[0] == "QUIT")
                    {
                        if (this.lstUsers.Items.IndexOf(tokens[1]) > -1)
                        {
                            //this.lstUsers.Items.Remove(tokens[1]);
                            SetControlList(lstUsers,tokens[1]);
                        }
                        add("用户:" + tokens[1] + "已经离开");
                    }
                    else
                    {
                        //如果从服务器返回的其他消息格式
                        //则在listBox控件中直接现实
                        add(msg);
                    }

                }
                //关闭连接
                tcpClient.Close();
            }
            catch (Exception ex)
            {
                add("网络发生错误");
            }
     }  
        #endregion

        #region 点击“离开”按钮
        private void btnExit_Click(object sender, System.EventArgs e)
        {
            if (state == CONNECTED)
            {
                string message = "EXIT|"+this.tbUserName.Text+"|";
                //将字符串转化为字符数组
                Byte[] outbytes = System.Text.Encoding.Default.GetBytes(message.ToCharArray());
                Stream.Write(outbytes,0,outbytes.Length);

                this.state = CLOSED;
                this.stopFlag = true;              
                this.lstUsers.Items.Clear();
            }
        }
        #endregion

        #region 点击"关闭"
        private void btnClose_Click(object sender, System.ComponentModel.CancelEventArgs e)
        {
            btnExit_Click(sender, e);
        }
        #endregion

        #region 获取颜色的方法
        private void btnColor_Click(object sender, EventArgs e)
        {
            ColorDialog colorDialog1 = new ColorDialog();
            colorDialog1.Color = this.rtbMsg.SelectionColor;
            if (colorDialog1.ShowDialog() == DialogResult.OK && colorDialog1.Color != this.rtbMsg.SelectionColor)
            {
                this.rtbMsg.SelectionColor = colorDialog1.Color;
                color = colorDialog1.Color;
            }
        }
        #endregion

        #region 添加信息
        private void add(string msg)
        {
            if(!color.IsEmpty)
            {
                SetControlText(rtbMsg,color.ToString());
               // this.rtbMsg.SelectionColor = color;
            }
            SetControlText(rtbMsg,msg+"\n");
            //this.rtbMsg.SelectedText = msg + "\n";
        }
        #endregion

        #region 定义一个委托
        private delegate void SetControlTextDelegate(Control control, string txt);
        //判断当前控件是否在此线程中,当是此线程时,调用该线程
        //如果不是同一线程,让其回调
        private void SetControlText(Control control, string txt)
        {
            if (control.InvokeRequired)
            {
                var d = new SetControlTextDelegate(SetControlText);
                BeginInvoke(d, new object[] { control, txt });
            }
            else
            {
                control.Text = txt;
            }
        }   

        private delegate void SetControlListDelegate(ListBox listbox, string txt);
        //判断当前控件是否在此线程中,当是此线程时,调用该线程
        //如果不是同一线程,让其回调
        private void SetControlList(ListBox listbox, string item)
        {
            if (listbox.InvokeRequired)
            {
                var d = new SetControlListDelegate(SetControlList);
                BeginInvoke(d, new object[] { listbox, item });
            }
            else
            {
                listbox.Items.Add(item);
            }
        }

        #endregion
    }    

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值