Socket 通訊(基礎原理、實時聊天系統雛形)

什麼是 Socket

Socket 英文直譯爲「孔或插座」,也稱爲套接字。用於描述 IP 地址和端口號,是一種進程間的通訊機制。你能夠理解爲 IP 地址肯定了網內的惟一計算機,而端口號則指定了將消息發送給哪個應用程序(大多應用程序啓動時會主動綁定一個端口,若是不主動綁定,操做系統自動爲其分配一個端口)。 數組

 

什麼是端口?

一臺主機通常運行了多個軟件並同時提供一些服務。每種服務都會打開一個 Socket,並綁定到一個端口號上,不一樣端口對應於不一樣的應用程序。例如 http 使用 80 端口;ftp 使用 21 端口;smtp 使用 23 端口安全

 

Socket 的類型

  • Stream:一種流式 Socket,針對於面向鏈接的 TCP 服務應用,安全,但效率低。(本文重點)
  • Datagram:數據報式的 Socket,針對於無鏈接的 UDP 服務應用,不安全(丟失、順序混亂,每每在接收端要分析完整性、重排、或要求重發),但效率高。

 

Socket 程序通常應用模式及運行流程

  1. 服務器端會啓動一個 Socket,開始監聽端口,監聽客戶端的鏈接信息,咱們稱之爲 Watch Socket。
  2. 客戶端 Socket 鏈接服務器端的監聽 Socket,一旦成功鏈接,服務器端會馬上建立一個新的 Socket 負責與客戶端進行通訊,以後,客戶端將再也不與 Watch Socket 通訊。
  3. Watch Socket 繼續監聽可能會來自其餘客戶端的鏈接。

上述過程就像是實現了一次三方會談。服務器端的 Socket 至少會有 2 個。一個是 Watch Socket,每成功接收到一個客戶端的鏈接,便在服務器端建立一個通訊 Socket。客戶端 Socket 指定要鏈接的服務器端地址和端口,建立一個 Socket 對象來初始化一個到服務器的 TCP 鏈接。 服務器

 

通訊的雛形

下面就看一個最簡單的 Socket 示例,實現了網絡聊天通訊的雛形。 網絡

服務器端socket

public partial class ChatServer : Form
    {
        public ChatServer()
        {
            InitializeComponent();
            ListBox.CheckForIllegalCrossThreadCalls = false;
        }

        /// <summary>
        /// 監聽 Socket 運行的線程
        /// </summary>
        Thread threadWatch = null;

        /// <summary>
        /// 監聽 Socket
        /// </summary>
        Socket socketWatch = null;

        /// <summary>
        /// 服務器端通訊套接字集合
        /// 必須在每次客戶端鏈接成功以後,保存新建的通信套接字,這樣才能和後續的全部客戶端通訊
        /// </summary>
        Dictionary<string, Socket> dictCommunication = new Dictionary<string, Socket>();

        /// <summary>
        /// 通訊線程的集合,用來接收客戶端發送的信息
        /// </summary>
        Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();

        private void btnBeginListen_Click(object sender, EventArgs e)
        {
            // 建立服務器端監聽 Socket (IP4尋址協議,流式鏈接,TCP協議傳輸數據)
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 監聽套接字綁定指定端口
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
            socketWatch.Bind(endPoint);

            // 將監聽套接字置於偵聽狀態,並設置鏈接隊列的最大長度
            socketWatch.Listen(20);

            // 啓動監聽線程開始監聽客戶端請求
            threadWatch = new Thread(Watch);
            threadWatch.IsBackground = true;
            threadWatch.Start();
            ShowMsg("服務器啓動完成!");
        }

        Socket socketCommunication = null;
        private void Watch()
        {
            while (true)
            {
                // Accept() 會建立新的通訊 Socket,且會阻斷當前線程,所以應置於非主線程上使用
                // Accept() 與線程上接受的委託類型不符,所以需另建一方法作橋接
                socketCommunication = socketWatch.Accept();

                // 將新建的通訊套接字存入集合中,以便服務器隨時能夠向指定客戶端發送消息
                // 如不置於集合中,每次 new 出的通訊線程都是一個新的套接字,那麼原套接字將失去引用
                dictCommunication.Add(socketCommunication.RemoteEndPoint.ToString(), socketCommunication);
                lbSocketOnline.Items.Add(socketCommunication.RemoteEndPoint.ToString());

                // Receive 也是一個阻塞方法,不能直接運行在 Watch 中,不然監聽線程會阻塞
                // 另外,將每個通訊線程存入集合,方便從此的管理(如關閉、或掛起)
                Thread thread = new Thread(() =>
                {
                    while (true)
                    {
                        byte[] bytes = new byte[1024 * 1024 * 2];
                        int length =  socketCommunication.Receive(bytes);
                        string msg = Encoding.UTF8.GetString(bytes, 0, length);
                        ShowMsg("接收到來自" + socketCommunication.RemoteEndPoint.ToString() + "的數據:" + msg);
                    }
                });
                thread.IsBackground = true;
                thread.Start();
                dictThread.Add(socketCommunication.RemoteEndPoint.ToString(), thread);

                ShowMsg("客戶端鏈接成功!通訊地址爲:" + socketCommunication.RemoteEndPoint.ToString());
            }
        }

        delegate void ShowMsgCallback(string msg);
        private void ShowMsg(string msg)
        {
            if (this.InvokeRequired) // 也能夠啓動時修改控件的 CheckForIllegalCrossThreadCalls 屬性
            {
                this.Invoke(new ShowMsgCallback(ShowMsg), new object[] { msg });
            }
            else
            {
                this.txtMsg.AppendText(msg + "\r\n");
            }
        }

        private void btnSendMsg_Click(object sender, EventArgs e)
        {
            if (lbSocketOnline.Text.Length == 0)
                MessageBox.Show("至少選擇一個客戶端才能發送消息!");
            else
            {
                // Send() 只接受字節數組
                string msg = txtSendMsg.Text.Trim();
                dictCommunication[lbSocketOnline.Text].Send(Encoding.UTF8.GetBytes(msg));
                ShowMsg("發送數據:" + msg);
            }
        }

        private void btnSendToAll_Click(object sender, EventArgs e)
        {
            string msg = txtSendMsg.Text.Trim();
            foreach (var socket in dictCommunication.Values)
            {
                socket.Send(Encoding.UTF8.GetBytes(msg));
            }
            ShowMsg("羣發數據:" + msg);
        }
    }

客戶端ui

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

        /// <summary>
        /// 此線程用來接收服務器發送的數據
        /// </summary>
        Thread threadRecive = null;

        Socket socketClient = null;

        private void btnConnect_Click(object sender, EventArgs e)
        {
            // 客戶端建立通信套接字並鏈接服務器、開始接收服務器傳來的數據
            socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socketClient.Connect(IPAddress.Parse(txtIP.Text.Trim()), int.Parse(txtPort.Text.Trim()));
            ShowMsg(string.Format("鏈接服務器({0}:{1})成功!", txtIP.Text.Trim(), txtPort.Text.Trim()));

            threadRecive = new Thread(new ThreadStart(() =>
            {
                while (true)
                {
                    // Receive 方法從套接字中接收數據,並存入接收緩衝區
                    byte[] bytes = new byte[1024 * 1024 * 2];
                    int length = socketClient.Receive(bytes);
                    string msg = Encoding.UTF8.GetString(bytes, 0, length);
                    ShowMsg("接收到數據:" + msg);
                }
            }));
            threadRecive.IsBackground = true;
            threadRecive.Start();
        }

        delegate void ShowMsgCallback(string msg);
        private void ShowMsg(string msg)
        {
            if (this.InvokeRequired) // 也能夠啓動時修改控件的 CheckForIllegalCrossThreadCalls 屬性
            {
                this.Invoke(new ShowMsgCallback(ShowMsg), new object[] { msg });
            }
            else
            {
                this.txtMsg.AppendText(msg + "\r\n");
            }
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            string msg = txtSendMsg.Text.Trim();
            socketClient.Send(Encoding.UTF8.GetBytes(msg));
            ShowMsg("發送數據:" + msg);
        }
    }

如今全部客戶都能和服務器進行通訊,服務器也能和全部客戶進行通訊。那麼,客戶端之間互相通訊呢? this

顯然,在客戶端界面也應建立在線列表,每次有人登陸後,服務器端除了刷新自身在線列表外,還需將新客戶端的套接字信息發送給其餘在線客戶端,以便它們更新本身的在線列表。 spa

客戶端發送消息給服務器,服務器轉發此消息給另外一個客戶端。固然,這個消息須要進行一些處理,至少要包含目標套接字和發送內容。 操作系統

更爲完善的是,服務器必須定時按制定的規則檢測列表中套接字通訊的有效性,經過發送響應信號,並接收客戶端應答信號以確認客戶端的鏈接性是真實的(不然,需剔除無效客戶端)。 線程

 

客戶端上傳文件

客戶端

private void btnChooseFile_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                txtFilePath.Text = ofd.FileName;
            }
        }

        private void btnSendFile_Click(object sender, EventArgs e)
        {
            using (FileStream fs = new FileStream(txtFilePath.Text, FileMode.Open))
            {
                byte[] bytes = new byte[1024 * 1024 * 2];
                
                // 假設第一個字節爲標誌位:0 表示傳送文件

                // 方式一:總體向後偏移 1 個字節;但這樣有潛在缺點,
                // 有時在通訊時會很是準確的按照約定的字節長度來傳遞,
                // 那麼這種偏移方案顯然是不可靠的
                // bytes[0] = 0; 
                // int length = fs.Read(bytes, 1, bytes.Length);

                // 方式二:建立多出 1 個字節的數組發送
                int length = fs.Read(bytes, 0, bytes.Length);
                byte[] newBytes = new byte[length + 1];
                newBytes[0] = 0;
                // BlockCopy() 會比你本身寫for循環賦值更爲簡單合適
                Buffer.BlockCopy(bytes, 0, newBytes, 1, length);
                socketClient.Send(newBytes);
            }
        }

服務器端(Receive 方法中修改爲這樣)

Thread thread = new Thread(() =>
                {
                    while (true)
                    {
                        byte[] bytes = new byte[1024 * 1024 * 2];
                        int length =  socketCommunication.Receive(bytes);

                        if (bytes[0] == 0) // File
                        {
                            SaveFileDialog sfd = new SaveFileDialog();
                            if (sfd.ShowDialog() == DialogResult.OK)
                            {
                                using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create))
                                {
                                    fs.Write(bytes, 1, length - 1);
                                    fs.Flush();
                                    ShowMsg("文件保存成功,路徑爲:" + sfd.FileName);
                                }
                            }
                        }
                        else // Msg
                        {
                            string msg = Encoding.UTF8.GetString(bytes, 0, length);
                            ShowMsg("接收到來自" + socketCommunication.RemoteEndPoint.ToString() + "的數據:" + msg);
                        }
                    }
                });

 

異常捕捉

Socket 通訊屬於網絡通訊程序,會有許多的意外,必須進行異常處理以便程序不會被輕易的擊垮。不論是客戶端仍是服務器端,只要和網絡交互的環節(Connect、Accept、Send、Receive 等)都要作異常處理。

本例中對服務器端 Receive 方法環節作了一些異常處理,並移除了相應的資源,例以下面:

try
{
    length = socketCommunication.Receive(bytes);
}
catch (SocketException ex)
{
    ShowMsg("出現異常:" + ex.Message);
    string key = socketCommunication.RemoteEndPoint.ToString();
    lbSocketOnline.Items.Remove(key);
    dictCommunication.Remove(key);
    dictThread.Remove(key);
    break;
}

 

系統界面截圖

image

 

image image 9

相關文章
相關標籤/搜索