用UDP實現區域網內聊天程式

luckeryin發表於2009-06-10

花一天的時間,根據老大的指示,用UDP做了一個區域網內聊天的程式。以前從沒做過UDP通訊方面的程式,只做過一些比較管理的SOCKET TCP通訊,所以剛開始的時候,還是有點不知從何下手的味道,但是後來從網上找了幾個相關的例子臨時抱佛腳的學習了一下,畢竟,區域網內聊天程式並不是什麼高難道的專案,UDP也不是什麼新鮮玩意兒。網上相關例項多如牛毛。終於有了點門道,就開始著手規劃自己的聊天程式了。

這個程式的用途是用來讓多個管理員同時為多位使用者線上答疑的。根據老大的要求,要以服務程式為核心,所有通訊必須經過服務程式,服務程式常開,執行在伺服器上。要能夠顯示當前線上使用者,並且能實現示讀訊息快取,下次使用者登入時再送達。兩類使用者:管理員和普通使用者,前者要能接收到所有訊息,並能向指定普通使用者和全部線上使用者傳送訊息,而後者只能向管理員傳送訊息,並接收屬於自己的訊息或目標是全部使用者的訊息。還要能檢視歷史聊天記錄。所有使用者登入要經過儲存在資料庫裡的註冊使用者的認證。

先貼圖吧(本來想把UML圖一起貼上來的,但後來發現程式邏輯和結構都是再簡單不過的了,就省下時間來沒去畫了)

image
服務端介面

image
客戶端介面

image
歷史聊天記錄介面

image
軟體配置介面

以下是服務端實現的核心程式碼:
using System;
using System.Collections;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Lucker.LogManager;
//程式中使用到執行緒
using System.Text;
//程式中使用到編碼

namespace UDPServer
{
    public partial class Server : Form
    {
        private LogManager lm = new LogManager();
        private UdpClient server;
        private IPEndPoint receivePoint;
        private int port = 8080;
        //定義埠號
        private int ip = 127001;
        //設定本地IP地址
        private Thread startServer;

        string[] temp;
        byte[] recData;
        Hashtable UserList = new Hashtable();
        Hashtable AdminList = new Hashtable();       
        Queue SendToAdmin = new Queue();
        Queue SendToUser = new Queue();
        //接收資訊
        //接收資料的字串格式為:IP地址|埠號|使用者名稱|是否管理員(Y/N)|接收使用者名稱|資訊內容
        public void start_server()
        {
            MethodInvoker mi = new MethodInvoker(ThreadFun);
            while (true)
            {
                //接收從遠端主機傳送到本地8080埠的資料
                recData = server.Receive(ref receivePoint);
                ASCIIEncoding encode = new ASCIIEncoding();
                //獲得客戶端請求資料
                string Read_str = encode.GetString(recData);
                //提取客戶端的資訊,存放到定義為temp的字串陣列中
                temp = Read_str.Split("|".ToCharArray());
                //資訊長度不對,放棄
                if (temp.Length != 6)
                {
                    lm.WriteFileLog("Error data format", Read_str);
                    continue;
                }
                AddtoList();
                switch (temp[5].Substring(0,temp[5].IndexOf("&E")))
                {
                    case "GetOnLineAdmin":
                        string[] admin;
                        string adminstring="";
                        admin = new string[AdminList.Count];
                        AdminList.Keys.CopyTo(admin, 0);
                        if (admin.Length != 0)
                        {
                            foreach (string s in admin)
                            {
                                adminstring += s + ",";
                            }                           
                        }
                        SendMsg("@:GetOnLineAdmin&E" + adminstring);
                        continue;
                    case "GetOnLineUser":
                        string[] user;
                        string userstring = "";
                        user = new string[UserList.Count];
                        UserList.Keys.CopyTo(user, 0);
                        if (user.Length != 0)
                        {
                            foreach (string s in user)
                            {
                                userstring += s + ",";
                            }                           
                        }
                        userstring += "All,";
                        SendMsg("@:GetOnLineUser&E" + userstring);
                        continue;
                    default:
                        break;
                }
                //顯示資訊
                BeginInvoke(mi);//讓主執行緒去訪問自己建立的控制元件.
            }
        }
        private void SendMsg(string msg)
        {
            msg = string.Format("{0}|{1}|{2}|{3}|{4}|", "", "", "System", "N",temp[2]) + msg+"&E";
            Byte[] buffer = null;
            Encoding ASCII = Encoding.ASCII;
            buffer = new Byte[msg.Length + 1];
            int len = ASCII.GetBytes(msg.ToCharArray(), 0, msg.Length, buffer, 0);
            server.Send(buffer, len, temp[0], Int32.Parse(temp[1]));
        }
        //處理非同步呼叫
        private void ThreadFun()
        {           
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (recData != null && temp != null)
            {
                temp[5] = temp[5].Substring(0, temp[5].Length - 2);
                if (temp[4] == "")
                {
                    msg.AppendText(string.Format("{1}:/t[{0}] said/r/n/t{2}/r/n", temp[2], datetime, temp[5]));
                    lm.WriteFileLog(string.Format("[{0}]", temp[2]), temp[5]);
                }
                else
                {
                    msg.AppendText(string.Format("{1}:/t[{0}] said to [{3}]/r/n/t{2}/r/n", temp[2], datetime, temp[5], temp[4]));
                    lm.WriteFileLog(string.Format("[{0}] said to [{1}]", temp[2], temp[4]), temp[5]);
                }
                string[] ip = { temp[0], temp[1] };
                if (temp[3] == "Y")
                {
                    SendToUser.Enqueue(recData);
                    SendToAdmin.Enqueue(recData);//A 管理員的資訊要能轉發到B 管理員電腦上。
                }
                else
                {
                    SendToAdmin.Enqueue(recData);
                }

                recData = null;
                temp = null;
            }
        }
        //新增使用者到列表中
        private void AddtoList()
        {           
            string[] ip = { temp[0], temp[1] };
            if (temp[3] == "Y")
            {
                if (!AdminList.ContainsKey(temp[2]))
                {
                    AdminList.Add(temp[2], ip);
                }
            }
            else
            {
                if (!UserList.ContainsKey(temp[2]))
                {
                    UserList.Add(temp[2], ip);
                }
            }
        }
        public Server()
        {
            InitializeComponent();
        }

        //啟動服務
        private void Server_Load(object sender, EventArgs e)
        {
            timer1.Enabled = true;
            run();
        }
        public void run()
        {
            //利用本地8080埠號來初始化一個UDP網路服務
            server = new UdpClient(port);
            receivePoint = new IPEndPoint(new IPAddress(ip), port);
            //開一個執行緒
            startServer = new Thread(new ThreadStart(start_server));
            //啟動執行緒
            startServer.Start();
            toolStripStatusLabel_state.Text = "UDP Chat Server is running";
        }

        //清除伺服器端程式日誌
        private void button1_Click(object sender, EventArgs e)
        {
            msg.Text = "";
        }

        //轉發使用者資訊到每一位管理員電腦上:
        //轉發管理員資訊到指定的使用者電腦上:
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (SendToAdmin.Count > 0 && AdminList.Count > 0)
            {
                byte[] senddate;
                string[] data;
                for (int i = 0; i < SendToAdmin.Count; i++)
                {
                    ICollection keys = AdminList.Keys;
                    string[] value = new string[2];
                    senddate = (byte[])SendToAdmin.Dequeue();
                    ASCIIEncoding encode = new ASCIIEncoding();
                    //獲得客戶端請求資料
                    string Read_str = encode.GetString(senddate);
                    //提取客戶端的資訊,存放到定義為temp的字串陣列中
                    data = Read_str.Split("|".ToCharArray());
                    foreach (string admin in keys)
                    {
                        value = (string[])AdminList[admin];
                        if (data[2] == admin) continue;//自己發了的資訊,不應再回發給自己了。
                        server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                        Thread.Sleep(100);
                    }
                }
            }
            if (SendToUser.Count > 0 && UserList.Count > 0)
            {
                byte[] senddate;
                string[] data;
                for (int i = 0; i < SendToUser.Count; i++)
                {
                    ICollection keys = UserList.Keys;
                    string[] value = new string[2];
                    senddate = (byte[])SendToUser.Dequeue();
                    ASCIIEncoding encode = new ASCIIEncoding();
                    //獲得客戶端請求資料
                    string Read_str = encode.GetString(senddate);
                    //提取客戶端的資訊,存放到定義為temp的字串陣列中
                    data = Read_str.Split("|".ToCharArray());
                    foreach (string user in keys)
                    {
                        if (data[4] == "All")//發給所有人。
                        {
                            value = (string[])UserList[user];
                            server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                            senddate = null;
                            Thread.Sleep(100);
                        }
                        else
                            if (user == data[4])//查詢到接收使用者,傳送訊息
                            {
                                value = (string[])UserList[user];
                                server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                                senddate = null;
                                Thread.Sleep(100);
                                break;
                            }
                    }
                    if (senddate != null)//訊息沒有傳送出去,重新和隊。
                    {
                        SendToUser.Enqueue(senddate);
                    }
                }
            }
            //日誌太長,自動清空。
            if (msg.Text.Length > 32766)
            {
                msg.Text = "";
            }
            //更新線上列表,客戶端有資訊來時自動增加:
            for (int i = 0; i < admin_list.Items.Count;)
            {
                admin_list.Items.RemoveAt(i);
            }
            for (int i = 0; i < user_list.Items.Count;)
            {
                user_list.Items.RemoveAt(i);
            }
            ICollection keys1 = UserList.Keys;
            foreach (string user in keys1)
            {
                user_list.Items.Add(user);
            }
            ICollection keys2 = AdminList.Keys;
            foreach (string admin in keys2)
            {
                admin_list.Items.Add(admin);
            }
            UserList.Clear();
            AdminList.Clear();
        }

        private void tb_his_Click(object sender, EventArgs e)
        {
            msg His = new msg("History");
            His.Show();
        }

    }
}

以下是客戶端實現的核心程式碼:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Lucker.LogManager;

namespace UDPClient
{
    public partial class Client : Form
    {
        Register reg = new Register();
        private LogManager lm = new LogManager();
        string User;
        string IsAdmin = "N";
        private static UdpClient m_Client;
        private static string m_szHostName;
        string local_IP;
        private static IPHostEntry m_LocalHost;
        private static IPEndPoint m_RemoteEP;
        private Thread th;
        string[] data;
        String strData;
        string Sendto = "";
        string connectionstring;
        static int RemotePort;
        static int LocalPort;
        private static IPAddress m_GroupAddress_S;//要傳送到的計算機IP

        public Client()
        {
            InitializeComponent();
        }

        //登入
        private void button3_Click(object sender, EventArgs e)
        {
            if(button3.Text=="&Login")
            {
                SqlConnection con = new SqlConnection(connectionstring);
                SqlCommand com = new SqlCommand();
                com.Connection = con;
                com.CommandText = string.Format("select count(*) from [UserInfor] where [UserName]='{0}' and [Password]='{1}'", tb_user.Text.Trim(), tb_pwd.Text.Trim());
                try
                {
                    if (con.State != ConnectionState.Open) con.Open();
                    //ReceivedMsg.AppendText("[System]:/tConnected!/r/n");
                    if (int.Parse(com.ExecuteScalar().ToString()) != 1)//登入不成功
                    {
                        ReceivedMsg.AppendText("[System]:/tUser name or password is wrong.Please try again./r/n");
                        return;
                    }
                    User = tb_user.Text.Trim();
                    //是否是管理員
                    com.CommandText = string.Format("select [Class] from [UserInfor] where [UserName]='{0}'", User);
                    object oIsAdmin=com.ExecuteScalar();
                    if (oIsAdmin != null)
                    {
                        if (Convert.ToInt32(oIsAdmin) == 0)
                        {
                            IsAdmin = "Y";
                            Sendto = "All";
                            button1.Text = "&Send to All";
                            ReceivedMsg.AppendText("[System]:/tAdmin <" + User + "> login./r/n");
                        }
                        else
                        {
                            IsAdmin = "N";
                            ReceivedMsg.AppendText("[System]:/tUser <" + User + "> login./r/n");
                        }
                    }
                    else
                    {
                        IsAdmin = "N";
                        ReceivedMsg.AppendText("[System]:/tUser <" + User + "> login./r/n");
                    }
                    SendMsg("I login.");
                    Text = User + " is on line";
                    button1.Enabled = true;
                    tb_SendMsg.Focus();
                    button3.Text = "&Logout";
                    tb_user.Enabled = false;
                    tb_pwd.Enabled = false;
                    cbx_remb.Enabled = false;
                    //GetOnLine();
                    //Thread.Sleep(500);
                    timer1.Enabled = true;

                    //儲存登入記錄
                    if (cbx_remb.Checked)
                    {
                        string id = tb_user.Text.Trim();
                        string pwd = tb_pwd.Text.Trim();
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "Last user", id, Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "User pwd", pwd, Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "Remember", "Y", Microsoft.Win32.RegistryValueKind.String);
                    }
                    else
                    {
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "Last user", "", Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "User pwd", "", Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software/LuckerSoft/UDPChat/", "Remember", "N", Microsoft.Win32.RegistryValueKind.String);
                    }
                }
                catch (SqlException sqlex)
                {
                    ReceivedMsg.AppendText("[System]:/t"+sqlex.Message+"/r/n");
                }
            }
            else
            {
                Sendto = "All";
                SendMsg("I logout.");
                timer1.Enabled = false;
                button3.Text = "&Login";
                tb_user.Enabled = true;
                tb_pwd.Enabled = true;
                cbx_remb.Enabled = true;
                User = "";
                button1.Enabled = false;
                button1.Text = "&Send to";
                Sendto = "";
                Text = "Chat";
                for (int i = 0; i < AdminList.Items.Count;)
                {
                    AdminList.Items.RemoveAt(i);
                }
                for (int i = 0; i < UserList.Items.Count;)
                {
                    UserList.Items.RemoveAt(i);
                }
            }
        }

        //退出
        private void button2_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (AdminList.Items.Count == 0 && UserList.Items.Count == 0)
            {
                ReceivedMsg.AppendText("[System]:/tThere is no Admin or User online, your message may not be read!/r/n");
                return;
            }
            if (button1.Text == "&Send to" &&IsAdmin=="Y")
            {
                ReceivedMsg.AppendText("[System]:/tPlease choose a user to chat./r/n");
                return;
            }
            string msg = tb_SendMsg.Text.Trim();
            if (msg.Length == 0) return;
            SendMsg(msg);
            tb_SendMsg.Text = "";
            tb_SendMsg.Focus();
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (IsAdmin == "Y")
            {
                ReceivedMsg.AppendText(string.Format("{1}:/t[{0}] said to [{3}]/r/n/t{2}/r/n", User, datetime, msg, Sendto));
                lm.WriteFileLog(string.Format("[{0}] said to [{1}]", User, Sendto), msg);
            }
            else
            {
                ReceivedMsg.AppendText(string.Format("{1}:/t[{0}] said/r/n/t{2}/r/n", User, datetime, msg));
                lm.WriteFileLog(string.Format("[{0}] said", User), msg);
            }
        }
        private void Client_Load(object sender, EventArgs e)
        {           
            string ip= (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "IP", "127.0.0.1", true, Microsoft.Win32.RegistryValueKind.String);
            string port = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "Port", "8080", true, Microsoft.Win32.RegistryValueKind.String);
            string server = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "Server", "192.168.3.19", true, Microsoft.Win32.RegistryValueKind.String);
            string db = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "DataBase", "Stock", true, Microsoft.Win32.RegistryValueKind.String);
            string id = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "UserID", "sa", true, Microsoft.Win32.RegistryValueKind.String);
            string pwd = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "Passwork", "ati1234", true, Microsoft.Win32.RegistryValueKind.String);
            connectionstring=string.Format("Data Source={0};Initial Catalog={1};User ID={2};Password={3};",server,db,id,pwd);

            tb_user.Text = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "Last user", "", true, Microsoft.Win32.RegistryValueKind.String);
            tb_pwd.Text = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "User pwd", "", true, Microsoft.Win32.RegistryValueKind.String);
            string remb = (string)reg.ReadReg(@"Software/LuckerSoft/UDPChat/", "Remember", "N", true, Microsoft.Win32.RegistryValueKind.String);
            if (remb == "Y")
                cbx_remb.Checked = true;
            else
                cbx_remb.Checked = false;

            RemotePort = int.Parse(port);
            LocalPort = RemotePort;
            m_GroupAddress_S = IPAddress.Parse(ip);//要傳送到的計算機IP

            System.Net.IPHostEntry localhost = Dns.GetHostEntry(Dns.GetHostName());
            local_IP = localhost.AddressList[0].ToString();//接收訊息的本地IP,用於監聽
            //m_Address_C = IPAddress.Parse(local_IP);

            m_szHostName = Dns.GetHostName();
            m_LocalHost = Dns.GetHostEntry(m_szHostName);
            //例項化UdpCLient
            m_Client = new UdpClient(LocalPort);
            //建立對方主機的終結點
            m_RemoteEP = new IPEndPoint(m_GroupAddress_S, RemotePort);

            th = new Thread(new ThreadStart(Listener));
            th.Start();
        }

        public void Listener()
        {
            MethodInvoker mi = new MethodInvoker(ThreadFun);
            Encoding ASCII = Encoding.ASCII;
            while (true)
            {
                Byte[] recdata = m_Client.Receive(ref m_RemoteEP);
                strData = ASCII.GetString(recdata);
                //提取資訊,存放到定義為data的字串陣列中
                data = strData.Split("|".ToCharArray());
                //資訊長度不對,放棄
                if (data.Length != 6)
                {
                    lm.WriteFileLog("Error data format", strData);
                    continue;
                }
                //顯示資訊
                BeginInvoke(mi);//讓主執行緒去訪問自己建立的控制元件.
            }
        }

        private void ThreadFun()
        {
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (strData != null && data != null)
            {
                switch (data[5].Substring(0,data[5].IndexOf("&E")))
                {
                    case "GetOnLineAdmin"://客戶端不響應獲取管理員列表的請求
                    case "GetOnLineUser":
                        break;
                    case "@:GetOnLineAdmin"://管理員列表資訊 "@:"表示響應請求.
                        string admins=data[5].Substring(data[5].IndexOf("&E")+2);
                        if (admins.Length > 3)
                        {
                            admins = admins.Substring(0, admins.Length - 3);
                            string[] adminlist = admins.Split(",".ToCharArray());
                            for (int i = 0; i < AdminList.Items.Count;)
                                AdminList.Items.RemoveAt(i);
                            AdminList.Items.AddRange(adminlist);
                        }
                        break;
                    case "@:GetOnLineUser"://管理員列表資訊 "@:"表示響應請求.
                        string users = data[5].Substring(data[5].IndexOf("&E") + 2);
                        if (users.Length > 3)
                        {
                            users = users.Substring(0, users.Length - 3);
                            string[] userlist = users.Split(",".ToCharArray());
                            for (int i = 0; i < UserList.Items.Count; )
                                UserList.Items.RemoveAt(i);
                            UserList.Items.AddRange(userlist);
                        }
                        break;
                    default://普通會話
                        data[5] = data[5].Substring(0, data[5].Length - 2);
                        if (data[4] == "")
                        {
                            ReceivedMsg.AppendText(string.Format("{1}:/t[{0}] said/r/n/t{2}/r/n", data[2], datetime, data[5]));
                            lm.WriteFileLog(string.Format("[{0}]", data[2]), data[5]);
                        }
                        else
                        {
                            ReceivedMsg.AppendText(string.Format("{1}:/t[{0}] said to [{3}]/r/n/t{2}/r/n", data[2], datetime, data[5], data[4]));
                            lm.WriteFileLog(string.Format("[{0}] said to [{1}]", data[2], data[4]), data[5]);
                        }
                        break;
                }
                strData = null;
                data = null;
            }
        }
        private void SendMsg(string msg)
        {
            msg = string.Format("{0}|{1}|{2}|{3}|{4}|", local_IP, LocalPort, User, IsAdmin, Sendto) + msg + "&E";
            Byte[] buffer = null;
            Encoding ASCII = Encoding.ASCII;
            buffer = new Byte[msg.Length + 1];
            int len = ASCII.GetBytes(msg.ToCharArray(), 0, msg.Length, buffer, 0);
            m_Client.Send(buffer, len, m_RemoteEP);
        }
        //獲得線上管理員列表:
        private void GetOnLine()
        {
            SendMsg("GetOnLineAdmin");
            Thread.Sleep(100);
            SendMsg("GetOnLineUser");
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            GetOnLine();
        }

        private void UserList_MouseClick(object sender, MouseEventArgs e)
        {
            if (IsAdmin == "N") return;
            if (UserList.SelectedItems.Count == 0)
            {
                //ReceivedMsg.AppendText("[System]:/tPlease choose a user to chat./r/n");
                return;
            }
            Sendto = UserList.SelectedItem.ToString();
            button1.Text = "&Send to " + Sendto;
        }

        private void history_Click(object sender, EventArgs e)
        {
            msg His = new msg("History");
            His.Show();
        }

        private void Config_Click(object sender, EventArgs e)
        {
            Config config = new Config();
            config.ShowDialog();
        }
    }
}

貼完程式碼,實現自己都感覺程式碼很亂,只能夠說把功能實現了,設計模式,程式設計思想什麼的就沒去考慮了。希望大家多給我提些改進的意見或建議,逐步完善這個程式。

有需要原始碼的朋友請繼續關注本文,我會再過幾天將原始碼奉上,在文章結尾處貼出下載連結。

後語

程式做完了,卻始終沒有弄明白這個程式為什麼老大非要我用UDP實現,我想:同樣的功能,用TCP的話要簡單多了。UDP最大的特點就是無連線通訊,實現簡單。而本例中UDP無連線的特性恰恰是確實使用者線上狀態的最大的不方便之處,在TCP程式中,對方的上線和下線,哪怕裡網路中斷都可以即時反映出來,無需太多的程式碼就可以實現,雖然本例中通過客戶端不斷的向伺服器輪詢基本上確保了在可以接受的短時間內獲得線上使用者列表,但實現它的代價是十分昂貴的,不僅支出了相當大量的網路流量(絕大部分通訊流量是輪詢)和系統時間,相比TCP來說,實在不合算。如果硬要用UDP做通訊的話,其實,之前還有一個相對“經濟”一點的方案,那就是:客戶端開啟後,通過UDP廣播獲得服務端的IP地址和埠,從而和服務端取得聯絡。每個客戶端執行後都在服務端儲存它的IP地址和埠,這樣,當一個終端要和另一個終端通訊時,可以從服務端獲得對方的IP地址和埠,然後就可以實現兩個終端之間的直接通訊。服務端只在其中扮演“查詢字典”的角色。這個方案和上面實現的方案各有特色,讀者朋友們可以自己研究研究。

不知不覺,已近午夜。呵呵,該睡覺了。

相關文章