聊天程序(基於Socket、Thread)


聊天程序簡述


1、目的:主要是為了闡述Socket,以及應用多線程,本文側重Socket相關網路編程的闡述。如果您對多線程不了解,大家可以看下我的上一篇博文淺解多線程

2、功能:此聊天程序功能實現了服務端跟多個客戶端之間的聊天,可以群發消息,選擇ip發消息,客戶端向服務端發送文件。 (例子為WinForm應用程序)


Socket,端口,Tcp,UDP。 概念


 1、Socket還被稱作“套接字”,應用程序通常通過套接字向網絡發送請求或者應答網絡請求。根據連接啟動的方式以及本地套接字要連接的目標,套接字之間的連接過程可以分為三個步驟:服務器監聽,客戶端請求,連接確認。

2、端口:可以認為是計算機與外界通訊交流的出口。

3、Tcp TCP是一種面向連接(連接導向)的、可靠的、基於字節流的運輸層通信協議。UDP是另一個重要的傳輸協議。

4、UDP:用戶數據報協議,是一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。


理解Socket,端口,Tcp,UDP


1、ip跟端口的作用:例如,你用QQ跟好友聊天,首先QQ要知根據好友所在電腦的IP地址發送信息,ip地址能確定好友的所在的電腦,但是不知道好友電腦上的QQ應用程序是哪一個,這就需要QQ提供一個端口號來確定你發過來的信息是QQ接受的數據。這樣就簡單的闡述了Ip跟端口的作用。

2、Tcp,Udp作用以及差異:首先要說的是,這是兩種網路協議,他們的差別就是TCP協議中包含了專門的傳遞保證機制,當數據接收方收到發送方傳來的信息時,會自動向發送方發出確認消息;發送方只有在接收到該確認消息之后才繼續傳送其它信息,否則將一直等待直到收到確認信息為止。與TCP不同,UDP協議並不提供數據傳送的保證機制。如果在從發送方到接收方的傳遞過程中出現數據報的丟失,協議本身並不能做出任何檢測或提示。我們.net程序員一般的應用程序用的都是Tcp協議。但是Tcp協議的執行速度,效率不及Udp快。看別人的博客感覺圖解這兩個協議,顯得更直觀點。上圖:


3、Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。出自同一篇博客的圖。

4、到這里如果你對Socket,還不是很清楚透徹,那么在接下來的聊天程序代碼中,我還會一點點的闡述。


 創建服務端監聽功能———聊天程序(Socket、Thread)


服務端監聽服務是創建一個Socket等待接收客戶端的信息。這個需要綁定服務端的Ip、端口號,以便於客戶端發送請求的時候找准確服務端聊天程序的具體位置。此外這個Socket還需要設置監聽序列的大小,告知應用程序一次性最多處理客戶端發來信息的多少。然后創建一個接收客戶端通信的Socket,等待客戶段發來的信息。

View Code
 Socket sck = null;
        //點擊開啟服務端監聽
        private void btn_StarServer_Click(object sender, EventArgs e)
        {
            //創建一個Socket實例
            //第一個參數表示使用ipv4
            //第二個參數表示發送的是數據流
            //第三個參數表示使用的協議是Tcp協議
            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //獲取ip地址
            IPAddress ip = IPAddress.Parse(txt_ip.Text);
            //創建一個網絡通信節點,這個通信節點包含了ip地址,跟端口號。
            //這里的端口我們設置為1029,這里設置大於1024,為什么自己查一下端口號范圍使用說明。
            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
            //Socket綁定網路通信節點
            sck.Bind(endPoint);
            //設置監聽隊列
            sck.Listen(10);
            ShowMsg("開啟監聽!");

            //創建一個接收客戶端通信的Socket
            Socket accSck = sck.Accept();
            //如果監聽到客戶端有鏈接,則運行到下一部,提示,鏈接成功!
            ShowMsg("鏈接成功!");
        }

 //消息框里面數據
        void ShowMsg(string str)
        {
            string Ystr="";
            if (txt_AccMsg.Text != "")
            {
                Ystr = txt_AccMsg.Text + "\r\n";
            }
            txt_AccMsg.Text = Ystr+str;
        }

 

問題1:代碼中的Socket accSck = sck.Accept();這個Socket是讓上一個綁定服務端ip端口號的Socket一直處於等待接受客戶端發送信息的狀態,所以一直占用應用程序一直默認開啟的Ui線程,致使點擊開啟服務監聽后,界面無響應。

解決辦法:使用多線程,我們在這里寫一個自己的線程讓這里的監聽服務,寫在自己的線程里面。修改代碼如下:

View Code
  Socket sck = null;
        Thread thread = null;
        //點擊開啟服務端監聽
        private void btn_StarServer_Click(object sender, EventArgs e)
        {
            //創建一個Socket實例
            //第一個參數表示使用ipv4
            //第二個參數表示發送的是數據流
            //第三個參數表示使用的協議是Tcp協議
            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //獲取ip地址
            IPAddress ip = IPAddress.Parse(txt_ip.Text);
            //創建一個網絡通信節點,這個通信節點包含了ip地址,跟端口號。
            //這里的端口我們設置為1029,這里設置大於1024,為什么自己查一下端口號范圍使用說明。
            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
            //Socket綁定網路通信節點
            sck.Bind(endPoint);
            //設置監聽隊列
            sck.Listen(10);
            ShowMsg("開啟監聽!");

            //開啟一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這里特別說下這樣是可以的。
            Thread thread = new Thread(JtSocket);
            //設置為后台線程
            thread.IsBackground = true;
            thread.Start();
        }

        //Socket服務監聽函數
        void JtSocket()
        {            
            //創建一個接收客戶端通信的Socket
            Socket accSck = sck.Accept();
            //如果監聽到客戶端有鏈接,則運行到下一部,提示,鏈接成功!
            ShowMsg("鏈接成功!");
        }

問題2:代碼中sck.Listen(10);設置監聽序列,這里設置為10是不是,服務端只能處理10個客戶段的請求呢。

答:不是的這里設置的是一次性只能處理10個,如果還有更多就在后面排隊,等待這10個處理完成,接下來在處理排着對的信息。

開啟服務監聽看一下我們的聊天界面:

然后我們再做一個客戶端,鏈接到服務端。


創建客戶端鏈接服務端的Socket———聊天程序(Socket、Thread)


如果鏈接服務端的聊天程序則需要知道服務端的Ip地址,端口號。

View Code
Socket clientSocket = null;
        Thread thread = null;
        //通過IP地址與端口號與服務端建立鏈接      
        private void btn_ConServer_Click(object sender, EventArgs e)
        {
            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //這里的ip地址,端口號都是服務端綁定的相關數據。
            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
            clientSocket.Connect(endpoint);//鏈接有端口號與IP地址確定服務端.
        }

然后點擊連接服務,查看我們的聊天界面。(首先先打開服務端應用程序,點擊開啟監聽,然后打開客戶端應用程序,點擊鏈接服務)

鏈接成功后,下一步,我們就開始我們的聊天信息接收發送了。


服務端向客戶端發送信息,客戶端接受信息———聊天程序(Socket、Thread)


1、這里我們發送消息是通過Tcp協議以 字節數組的類型形式發送,所以在發送之前我們需要把要發送,接收的數據做一個轉換為字節數組的類型。

2、客戶端通過創建的鏈接服務端的Socket的Receive方法接收消息,服務端通過創建的接受客戶端信息的Socket的Send方法發送消息。

服務端代碼:

View Code
 Socket sck = null;
        Thread thread = null;
        //點擊開啟服務端監聽
        private void btn_StarServer_Click(object sender, EventArgs e)
        {
            //創建一個Socket實例
            //第一個參數表示使用ipv4
            //第二個參數表示發送的是數據流
            //第三個參數表示使用的協議是Tcp協議
            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //獲取ip地址
            IPAddress ip = IPAddress.Parse(txt_ip.Text);
            //創建一個網絡通信節點,這個通信節點包含了ip地址,跟端口號。
            //這里的端口我們設置為1029,這里設置大於1024,為什么自己查一下端口號范圍使用說明。
            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));
            //Socket綁定網路通信節點
            sck.Bind(endPoint);
            //設置監聽隊列
            sck.Listen(10);
            ShowMsg("開啟監聽!");

            //開啟一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這里特別說下這樣是可以的。
            Thread thread = new Thread(JtSocket);
            //設置為后台線程
            thread.IsBackground = true;
            thread.Start();
        }

        Socket accSck = null;
        //Socket服務監聽函數
        void JtSocket()
        {
            while (true)//注意該循環,服務端要持續監聽,要不然一個客戶端鏈接過后就無法鏈接第二個客戶端了。
            {
                //創建一個接收客戶端通信的Socket
                accSck = sck.Accept();
                //如果監聽到客戶端有鏈接,則運行到下一部,提示,鏈接成功!
                ShowMsg("鏈接成功!");
            }
        }

        //消息框里面數據
        void ShowMsg(string str)
        {
            string Ystr="";
            if (txt_AccMsg.Text != "")
            {
                Ystr = txt_AccMsg.Text + "\r\n";
            }
            txt_AccMsg.Text = Ystr+str;
        }

        //向客戶端發送數據
        private void btn_SendSingleMsg_Click(object sender, EventArgs e)
        {
            string SendMsg = txt_SendMsg.Text;
            if (SendMsg != "")
            {
                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(SendMsg); //將要發送的數據,生成字節數組。
                accSck.Send(buffer);
                ShowMsg("向客戶端發送了:" + SendMsg);
            }
        }

客戶端代碼:

View Code
  public Form1()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }
        Socket clientSocket = null;
        Thread thread = null;
        //通過IP地址與端口號與服務端建立鏈接      
        private void btn_ConServer_Click(object sender, EventArgs e)
        {
            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //這里的ip地址,端口號都是服務端綁定的相關數據。
            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
            clientSocket.Connect(endpoint);//鏈接有端口號與IP地址確定服務端.

            //客戶端在接受服務端發送過來的數據是通過Socket 中的Receive方法,
            //該方法會阻斷線程,所以我們自己為該方法創建了一個線程
            thread = new Thread(ReceMsg);            
            thread.IsBackground = true;//設置后台線程
            thread.Start();
        }
        //接收服務端數據
        public void ReceMsg()
        {
            while (true)
            {
                byte[] buffer = new byte[1024 * 1024 * 2];
                clientSocket.Receive(buffer);//接收服務端發送過來的數據
                string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字節數組轉成字符串顯示在文本框中。
                ShowMsg("接收到數據:" + ReceiveMsg);
            }
        }

        //消息框里面數據
        void ShowMsg(string str)
        {
            string Ystr = "";
            if (txt_ClientMsg.Text != "")
            {
                Ystr = txt_ClientMsg.Text + "\r\n";
            }
            txt_ClientMsg.Text = Ystr + str;
        }

啟動服務端應用程序,點擊啟動服務監聽,啟動客戶端應用程序,點擊連接服務,然后在消息框內輸入消息,點擊發送。運行效果如下。


 

 接下來做客戶端向服務端發送消息:


 客戶端向服務端發送信息(文件,字符串),客戶端接受信息———聊天程序(Socket、Thread)


1、這里我們發送不僅只有字符串還有文件。他們都是一字節數組的類型發送出去,區別字符串和文件的思想是:把字節數組的第一個值設置為0跟1,用來區分。

2、這里發送的文件接受的時候,重命名,還要為他寫上后綴名。沒有深入寫。

3、這里客戶端連接服務端的成功后,把客戶端的ip端口號,寫入list列表中,同時也存入Dictionary<string, Socket> socketDir集合中,便於服務端與多個客戶端連接時,選擇發送信息。同時也避免了,不知道發送給哪個客戶端數據。

客戶端代碼:

View Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;

namespace CharClient
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }
        Socket clientSocket = null;
        Thread thread = null;
        //通過IP地址與端口號與服務端建立鏈接      
        private void btn_ConServer_Click(object sender, EventArgs e)
        {
            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //這里的ip地址,端口號都是服務端綁定的相關數據。
            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);
            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));
            clientSocket.Connect(endpoint);//鏈接有端口號與IP地址確定服務端.

            //客戶端在接受服務端發送過來的數據是通過Socket 中的Receive方法,
            //該方法會阻斷線程,所以我們自己為該方法創建了一個線程
            thread = new Thread(ReceMsg);            
            thread.IsBackground = true;//設置后台線程
            thread.Start();
        }
        //接收服務端數據
        public void ReceMsg()
        {
            while (true)
            {
                byte[] buffer = new byte[1024 * 1024 * 2];
                clientSocket.Receive(buffer);//接收服務端發送過來的數據
                string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字節數組轉成字符串顯示在文本框中。
                ShowMsg("接收到數據:" + ReceiveMsg);
            }
        }      
        //消息框里面數據
        void ShowMsg(string str)
        {
            string Ystr = "";
            if (txt_ClientMsg.Text != "")
            {
                Ystr = txt_ClientMsg.Text + "\r\n";
            }
            txt_ClientMsg.Text = Ystr + str;
        }
        //向服務端發送消息
        private void btn_ClientSendSingleMsg_Click(object sender, EventArgs e)
        {
            string txtMsg = txt_ClientSendMsg.Text;
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(txtMsg);
            byte[] newbuffer = new byte[buffer.Length + 1];//定義一個新數組
            newbuffer[0] = 0;//設置標識,表示發送的是字符串
            Buffer.BlockCopy(buffer, 0, newbuffer, 1, buffer.Length);//源數組中的數據拷貝到新數組中
            clientSocket.Send(newbuffer);//發送新數組中的數據
        }

        //向服務端發送文件
        private void btn_ClientSendfile_Click(object sender, EventArgs e)
        {
            using (FileStream fs = new FileStream(txt_ClientFile.Text, FileMode.Open))
            {
                byte[] buffer = new byte[1024 * 1024 * 2];
                int readLength = fs.Read(buffer, 0, buffer.Length);

                byte[] newbuffer = new byte[readLength + 1];//定義一個新的數組,然后將原有數組中的數據拷貝該數組中。
                newbuffer[0] = 1;//將第一單元設置為1,表示傳送的是文件.

                //將數據有一個數組拷貝到另一個數組.
                //第一參數:表示源數組
                //第二個:表示從源數組中的哪個位置開始拷貝
                //第三個:表示目標數組。
                //第四個:表示從目標數組的哪個位置開始填充.
                //五:表示:拷貝多少數據
                Buffer.BlockCopy(buffer, 0, newbuffer, 1, readLength);
                clientSocket.Send(newbuffer);
            }
        }
        //打開文件夾,選擇要發送的文件
        private void Btn_see_Click(object sender, EventArgs e)
        {
            OpenFileDialog openfile = new OpenFileDialog();
            if (openfile.ShowDialog() == DialogResult.OK)
            {
                txt_ClientFile.Text = openfile.FileName;
            }
        }

    }
}

服務端代碼:

View Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;

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

        Socket sck = null;
        Thread thread = null;
        //點擊開啟服務端監聽
        private void btn_StarServer_Click(object sender, EventArgs e)
        {
            //創建一個Socket實例
            //第一個參數表示使用ipv4
            //第二個參數表示發送的是數據流
            //第三個參數表示使用的協議是Tcp協議
            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //獲取ip地址
            IPAddress ip = IPAddress.Parse(txt_ip.Text);
            //創建一個網絡通信節點,這個通信節點包含了ip地址,跟端口號。
            //這里的端口我們設置為1029,這里設置大於1024,為什么自己查一下端口號范圍使用說明。
            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_port.Text));//創建一個網絡通信節點,該節點中包含了IP地址和端口號.
            //Socket綁定網路通信節點
            sck.Bind(endpoint);
            //設置監聽隊列
            sck.Listen(10);
            ShowMsg("開啟監聽!");

            //開啟一個線程,放入Socket服務監聽,上一篇博文中沒有介紹這樣的線程實例化方法。這里特別說下這樣是可以的。
            Thread thread = new Thread(ConnectAccept);
            //設置為后台線程
            thread.IsBackground = true;
            thread.Start();
        }

        //消息框里面數據
        void ShowMsg(string str)
        {
            string Ystr="";
            if (txt_AccMsg.Text != "")
            {
                Ystr = txt_AccMsg.Text + "\r\n";
            }
            txt_AccMsg.Text = Ystr+str;
        }

        //向客戶端發送數據
        private void btn_SendSingleMsg_Click(object sender, EventArgs e)
        {
            string sendMsg = this.txt_SendMsg.Text;//獲取要發送到客戶端的文本
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sendMsg);//生成字節數組
            if (!string.IsNullOrEmpty(this.lsb_Ips.Text))
            {
                string ipendpoint = this.lsb_Ips.Text;//在服務端,選擇與客戶端進行通信的IP地址與端口號
                socketDir[ipendpoint].Send(buffer);//向客戶端發送數據

                ShowMsg("向客戶端發送了:" + sendMsg);
            }
            else
            {
                MessageBox.Show("請選擇與哪個客戶端進行通信");
            }
        }

        //  Socket newSoket = null;//.:不能將與客戶端進行通信的Socket定義成全局的.
        Dictionary<string, Socket> socketDir = new Dictionary<string, Socket>();//將每一個與客戶端進行通信的Socket放到該集合中.
        public void ConnectAccept()
        {
            while (true)//注意該循環,服務端要持續監聽
            {
                Socket newSoket = sck.Accept();//接收客戶端發過來的數據,並且創建了一個新的Socket實例.
                socketDir.Add(newSoket.RemoteEndPoint.ToString(), newSoket);//將負責與客戶端進行通信的Socket實例添加到集合中。
                lsb_Ips.Items.Add(newSoket.RemoteEndPoint.ToString());
                ShowMsg("客戶端鏈接成功!");


                ParameterizedThreadStart par = new
                 ParameterizedThreadStart(RecevieMsg);
                Thread thread = new Thread(par);//由於服務端接收客戶端發送過來的數據是通過Recevie方法,該方法會阻斷線程,所以我們重新定義一個針對該方法的線程.
                // thread.SetApartmentState(ApartmentState.STA);
                thread.IsBackground = true;
                thread.Start(newSoket);//注意:不要忘記傳遞socket實例
            }
        }
        //該方法負責接收從客戶端發送過來的數據
        public void RecevieMsg(object socket)
        {
            Socket newSocket = socket as Socket;//轉成對應的Socket類型
            while (true)
            {
                byte[] buffer = new byte[1024 * 1024 * 2];
                int receiveLength = -1;
                try  //由於Socket中的Receive方法容易拋出異常,所以我們在這里要捕獲異常。
                {
                    receiveLength = newSocket.Receive(buffer);//接收從客戶端發送過來的數據
                }
                catch (SocketException ex)//注意:在捕獲異常時,先確定具體的異常類型。
                {
                    ShowMsg("出現了異常:" + ex.Message);
                    socketDir.Remove(newSocket.RemoteEndPoint.ToString());//如果出現了異常,將該Socket實例從集合中移除
                    lsb_Ips.Items.Remove(newSocket.RemoteEndPoint.ToString());
                    break;//出現異常以后,終止整個循環的執行
                }
                catch (Exception ex)
                {
                    ShowMsg("出現了異常:" + ex.Message);
                    break;
                }
                if (buffer[0] == 0)//表示字符串
                {
                    string str = System.Text.Encoding.UTF8.GetString(buffer, 1, receiveLength - 1);//注意,是從下標為1的開始轉成字符串,為0的是標識。
                    ShowMsg(str);
                }
                else if (buffer[0] == 1)//表示文件
                {
                    SaveFileDialog savafile = new SaveFileDialog();
                    if (savafile.ShowDialog() == DialogResult.OK)
                    {
                        using (FileStream fs = new FileStream(savafile.FileName, FileMode.Create))
                        {
                            fs.Write(buffer, 1, receiveLength - 1);//將文件寫到磁盤上,從1開始到receiveLength-1
                            ShowMsg("文件寫成功!");
                        }
                    }
                }
            }
        }

    }
}

啟動服務端應用程序,點擊啟動服務監聽,可以同時啟動多個客戶端應用程序,都要先點擊連接服務,然后在消息框內輸入消息,也可以選取文件,點擊發送。運行效果如下。



 總結:剩余一個群發,我沒寫上去,相信你如果看明白了上面我所寫的的話,這個群發,就so easy了。再次友情提醒一下,如果你不懂多線程,我的上一篇博客就是對他的淺解 。鏈接地址------->>淺解多線程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM