1. 程式人生 > >C# TCP/UDP 通訊程式碼講解

C# TCP/UDP 通訊程式碼講解

之前寫一個檔案傳輸軟體的時候不過的瞭解這 TCP和UDP的 一些概念和 知識,趁著目前還熱乎著,寫下來溫習一下。。第一次寫部落格 有不好的地方歡迎指出來。

首先介紹下我之前寫的思路。大家都知道 UDP是沒有連線是不可靠的,而TCP是面向連線可靠的連線。雖然TCP面向連線並且可靠但是同時效率也是相對沒有UDP好,但是檔案的傳輸是不允許丟失的就是必須可靠所以選擇用TCP,當然也可以用UDP將檔案打包傳送然後再檢驗。

TCP還有一個需要UDP的地方因為我們自己的電腦都不是固定的IP所以不能一直給TCP指定一個伺服器IP 一般自己寫的程式都是指定自己的電腦為伺服器這樣就導致每次IP都在改變所以這時候就需要 UDP的廣播,通過UDP的廣播將伺服器的IP傳送到客戶端然後客戶端接收到IP再建立TCP的連線。。差不多就是這樣一個思路。

接下來講解程式碼:

/// <summary>
        /// 傳送廣播資料包
        /// </summary>
        public virtual void UDPSendInfo(String SendMsg)
        {           
            UDPS = new Thread(UDPSendThread);
            UDPS.IsBackground = true;
            UDPS.Start(SendMsg);
        }

這個是UDP傳送資訊的函式通過 執行緒來發送 將要傳送的資訊 通過引數 SeendMsg 傳送給執行緒
/// <summary>
        /// UDP傳送資訊
        /// </summary>
        protected void UDPSendThread(Object SendMsg)
        {
            UDPSend = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一個Scoket實習,採用UDP傳輸
            while (true)
            {
                String IpMsg = (String)SendMsg;
                //IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport);//初始化一個傳送廣播和指定埠的網路埠例項
                IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport);
                UDPSend.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);//設定該scoket例項的傳送形式
                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(IpMsg + "-");
                UDPSend.SendTo(buffer, iep);
                Thread.Sleep(10);  //間隔一定時間傳送一次
            }
        }
根據上面講的思路我們要利用UDP傳送的是我們的IP地址所以 SendMsg 裡面裝的是我們的IP地址。  作為伺服器這邊就 隔一段時間就傳送 一次 IP地址資訊讓客戶端能夠接收到。IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, UDPport); 這句話說明 傳送的是廣播資訊。
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(IpMsg + "-");

這個是吧資訊轉換為位元組流資訊

/// <summary>
        /// 獲取本地IP
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        public static String GetLocalIP(int pos = 1)
        {
            try
            {
                // 獲得本計算機的名稱
                string hostName = Dns.GetHostName();

                // 通過名字獲得IP地址
                IPHostEntry hostInfo = Dns.GetHostByName(hostName);
                IPAddress[] address = hostInfo.AddressList;
                // 建立 ipAddress 陣列來存放字串形式的IP地址
                string[] ipAddress = new string[address.Length];
                for (int index = 0; index < address.Length; index++)
                {
                    ipAddress[index] = address[index].ToString();
                }
                pos = ipAddress.Length - 1;
                return Dns.Resolve(Dns.GetHostName()).AddressList[pos].ToString();             
            }
            catch (Exception exc)
            {
                //Error_Info = exc.Message;
                //WriteLogger(exc.ToString());
                return "";
            }

        }

通過這個函式獲取自己計算機的IP地址 以用於廣播發送給客戶端
pos = ipAddress.Length - 1;
                return Dns.Resolve(Dns.GetHostName()).AddressList[pos].ToString();    
這2句還是比較關鍵的 經過測試這獲取的就是自己的公網IP 如果是路由器連著就是 路由器分配給自己的IP 總之一般都是可以用的。
 /// <summary>
        /// UDP接受廣播資訊
        /// </summary>
        public virtual void UDPRecInfo()
        {           
            UDPR = new Thread(UDPRecThread);
            UDPR.IsBackground = true;
            UDPR.Start();          
        }

接下來是 負責接收的這邊。同樣利用執行緒來負責接收 以免阻塞主執行緒卡死 並且設定為後臺執行緒。這裡也沒有其他好說的
 /// <summary>
        /// UDP接受執行緒
        /// </summary>
        protected void UDPRecThread()
        {
            UDPRec = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);//初始化一個Scoket協議           
            IPEndPoint iep = new IPEndPoint(IPAddress.Any, UDPport);
            //IPEndPoint iep = new IPEndPoint(IPAddress.Any, UDPport);//初始化一個偵聽區域網內部所有IP和指定埠
            EndPoint ep = (EndPoint)iep;
            UDPRec.Bind(iep);//繫結這個例項
            byte[] buffer = new byte[2048];//設定緩衝資料流
            UDPRec.ReceiveFrom(buffer, ref ep);//接收資料,並確把資料設定到緩衝流裡
            serveripaddress = System.Text.Encoding.UTF8.GetString(buffer); //獲取ip地址
            serveripaddress = serveripaddress.Split('-')[0];
            UDPRec.Close();
            Start();   //啟動
            UDPR.Abort();  //釋放資源
        }
這裡也是 基本上 註釋 講的差不多了 
serveripaddress = serveripaddress.Split('-')[0];
這個地方要注意一下前面我封裝的時候就是 在IP最後加了一個 “-”作為 結束標識。可以自己定義
UDPport

這個就是一個埠 基本上TCP和UDP 都是樣的通過埠來識別是哪個應用程式來接受這段資訊,基本模式也是埠 + IP地址的組合 就可以通訊。
(IPAddress.Any
這個則是偵聽所有的
/// <summary>
        /// 開啟前傳送UDP電報將本身的serveripadress傳送給客戶端,和SearchStart 選取一個就好
        /// </summary>
        /// <returns></returns>
        public override bool SearchStart()
        {
            UDPSendInfo(serveripaddress);
            // 建立負責監聽的套接字,注意其中的引數;  
            socketWatch = new Socket(IPAddress.Parse(serveripaddress).AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            // 獲得文字框中的IP物件;  
            IPAddress address = IPAddress.Parse(serveripaddress);
            // 建立包含ip和埠號的網路節點物件;  
            IPEndPoint endPoint = new IPEndPoint(address, port);
            try
            {
                // 將負責監聽的套接字繫結到唯一的ip和埠上;  
                socketWatch.Bind(endPoint);
            }
            catch (SocketException se)
            {
                Error_Info = "異常:" + se.Message;
                return false;
            }
            // 設定監聽佇列的長度;  
            socketWatch.Listen(clent_lenght);
            // 建立負責監聽的執行緒;  
            threadWatch = new Thread(WatchConnecting);
            threadWatch.IsBackground = true;
            threadWatch.Start();
            File_Rec();        //處理流執行緒
            _is_start = true;
            return true;
        }
這個是啟動TCP 的執行緒 
serveripaddress
這個變數就是之前我們獲取到的本機的IP地址,將它與我們開啟的TCP進行繫結,然後
threadWatch = new Thread(WatchConnecting);
            threadWatch.IsBackground = true;
            threadWatch.Start();
用這個建立負責監聽是否有客戶端連線 的執行緒
 /// <summary>
        /// 伺服器監聽函式,進行對子客戶端的監聽
        /// </summary>
		private void WatchConnecting()
        {
            while (true)  // 持續不斷的監聽客戶端的連線請求;  
            {
                try {
                    // 開始監聽客戶端連線請求,Accept方法會阻斷當前的執行緒;  
                    Socket sokConnection = socketWatch.Accept(); // 一旦監聽到一個客戶端的請求,就返回一個與該客戶端通訊的 套接字;  
                                                                 // 想列表控制元件中新增客戶端的IP資訊;  
                                                                 //lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString());
                                                                 // 將與客戶端連線的 套接字 物件新增到集合中;
                    String skey = sokConnection.RemoteEndPoint.ToString();
                    AddNewClient(skey, sokConnection);  //新增一個新的客戶端的資訊
                    SendInitNameInfo(sokConnection, skey);    //給新連線的客戶端 初始化其他客戶端的資訊和給其他客戶端新增新客戶端的資訊                             
                    Thread thr = new Thread(RecMsg); // 為每一個接收訊息的連結單獨建立一個執行緒
                    thr.IsBackground = true;
                    thr.Start(sokConnection);
                    dictThread.Add(skey, thr);  //  將新建的執行緒 新增 到執行緒的集合中去。  
                                                //sendInitInfo();//傳送初始化資訊;
                }catch(Exception ex)
                {
                    Error_Info = ex.Message;
                    WriteLogger(ex.ToString());
                }
            }
            
        }
建立一個死迴圈來不斷 監聽是否有客戶端連線進來如果有客戶端連線進來則 給每個 連線客戶端建立一個訊息執行緒,然後將這個這個客戶端的埠資訊和物件資訊存入Dictionary字典 一般都是用這個來儲存的當然 選擇自己最擅長用的 自己覺得最好用的就好。
 AddNewClient(skey, sokConnection);  //新增一個新的客戶端的資訊

我這裡則是封裝為一個函數了將這個過程。

SendInitNameInfo(sokConnection, skey);    //給新連線的客戶端 初始化其他客戶端的資訊和給其他客戶端新增新客戶端的資訊  
這個是我自己的處理邏輯 就不解釋了。
 /// <summary>
        /// 監聽到後資訊的檢測函式
        /// </summary>
        /// <param name="sokConnectionparn"></param>
		void RecMsg(object sokConnectionparn) // 負責接收訊息的執行緒
        {
            Socket sokClient = sokConnectionparn as Socket;     //套接字物件
            String skey = sokClient.RemoteEndPoint.ToString();   //套接字資訊
            while (true)
            {
                // 定義一個2M的快取區;  
                byte[] arrMsgRec = new byte[Msg_send_size];
                // 將接受到的資料存入到輸入  arrMsgRec中;  
                int length = -1;
                try
                {
                    length = sokClient.Receive(arrMsgRec,0, Msg_send_size,SocketFlags.None); // 接收資料,並返回資料的長度;
                      
                }
                catch (SocketException se)
                {
                    Error_Info = "異常:" + se.Message;
                   /* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;  
                    dict.Remove(sokClient.RemoteEndPoint.ToString());
                    //從名字列表刪除被中斷的套接字的名字
                    ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
                    ///刪除一箇中斷的客戶端資訊
                    DelClient(skey); 
                    // 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;  
                    dictThread.Remove(skey);
                    // 從列表中移除被中斷的連線IP  
                    //lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                    break;
                }
                catch (Exception ex)
                {
                    Error_Info = "異常:" + ex.Message;
                    /* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;  
                    dict.Remove(sokClient.RemoteEndPoint.ToString());
                    //從名字列表刪除被中斷的套接字的名字
                    ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
                    ///刪除一箇中斷的客戶端資訊
                    DelClient(skey);
                    // 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;
                    dictThread[skey].Abort();
                    dictThread.Remove(skey);
                    // 從列表中移除被中斷的連線IP  
                    //lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                    break;
                }

                try {
                    //Infomation_Select(arrMsgRec, length);   //進行處理資訊篩選                   
                    AddRecInfo(arrMsgRec);
                    SendByteToAll(arrMsgRec, sokClient);   //轉發
                    //SendFileByte(arrMsgRec);            //轉發                                 
                } catch (Exception ex)
                {
                    Error_Info = ex.Message;
                }
            }
        }        
這個是每個客戶端對應 的一個接受訊息的執行緒 負責接收一個客戶端傳送的訊息同時負責將訊息 轉發給其他同時線上的客戶端 如果客戶端連線斷開則
Error_Info = "異常:" + se.Message;
                   /* // 從 通訊套接字 集合中刪除被中斷連線的通訊套接字;  
                    dict.Remove(sokClient.RemoteEndPoint.ToString());
                    //從名字列表刪除被中斷的套接字的名字
                    ClientNameList.Remove(sokClient.RemoteEndPoint.ToString());*/
                    ///刪除一箇中斷的客戶端資訊
                    DelClient(skey); 
                    // 從通訊執行緒集合中刪除被中斷連線的通訊執行緒物件;  
                    dictThread.Remove(skey);
                    // 從列表中移除被中斷的連線IP  
                    //lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                    break;
刪除這個客戶端的訊息同時break跳出死迴圈結束這個執行緒
length = sokClient.Receive(arrMsgRec,0, Msg_send_size,SocketFlags.None); // 接收資料,並返回資料的長度;
這裡表示要衝 Socket 接收快取中  一次提取多少長度的內容出來
Msg_send_size

代表要一次提取的內容長度,所以請在傳送的時候也 一次傳送同樣大小的資料包,因為本人制作的是檔案傳輸 一般檔案都不是一下子能傳完的所以必須將檔案分包傳送,然後自己封裝每個檔案包的 頭和尾加上判斷是否接受完畢和傳送完畢。當全部接受的時候 然後寫回到磁碟。訊息傳送的邏輯和封裝都是自己自定義

/// <summary>
        /// 傳送檔案流
        /// </summary>
        /// <param name="msg">檔案流資訊</param>
        public override void SendFileByte(byte [] msg)
        {
            try
            {
                foreach (Socket s in dict.Values)
                {
                    if (s.Connected)
                        s.Send(msg, Msg_send_size, SocketFlags.None);
                }
            }catch(Exception ex)
            {
                Error_Info = ex.Message;
            }
        }
伺服器傳送流訊息, 傳送的資訊都是 以位元組流的形式傳送。提取每個線上的客戶端都給他們傳送一遍。注意傳送的資訊量的大小
Msg_send_size
<strong><span style="font-size:32px;">客戶端:</span></strong>
/// <summary>
        /// 啟動客戶端
        /// </summary>
        /// <returns></returns>
        public override Boolean Start()
        {
            IPAddress ip = IPAddress.Parse(serveripaddress);
            IPEndPoint endPoint = new IPEndPoint(ip, port);
            sockClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                Error_Info += "與伺服器連線中……\r\n";
                //MessageBox.Show("與伺服器連線中……");
                sockClient.Connect(endPoint);

            }
            catch (SocketException se)
            {
                Error_Info = "連線錯誤:" + se.Message;
                WriteLogger(se.ToString());
                return false;
                //this.Close();  
            }
            Error_Info = "與伺服器連線成功!!!\r\n";
            //MessageBox.Show("與伺服器連線成功!!!");
            threadClient = new Thread(RecMsg);    //客戶端接收資訊
            threadClient.IsBackground = true;
            threadClient.Priority = ThreadPriority.Highest;
            threadClient.Start();                 //開啟執行緒           
            Thread intname = new Thread(InitThread);
            intname.IsBackground = true;
            intname.Start();
            File_Rec();                     //處理留資訊 執行緒
            _is_start = true;
            return true;
        }
當客戶端接收到伺服器傳送的UDP資料包的時候 上面我們已經看到客戶端 接收UDP資料包並且解析出來的伺服器的IP地址這個時候就可以使用解析出來的IP地址
serveripaddress
與客戶端這邊的TCPSocket進行綁定了然後開啟接收伺服器傳送的訊息的執行緒
/// <summary>
        /// 接受伺服器傳送來的訊息
        /// </summary>
        protected void RecMsg()
        {
            while (true)
            {
                if (!sockClient.Connected)
                {
                    Error_Info = "和伺服器連線斷開.....\r\n";
                    WriteLogger(Error_Info);
                    break;
                }
                // 定義一個Msg_send_size的快取區;  
                byte[] arrMsgRec = new byte[Msg_send_size];
                // 將接受到的資料存入到輸入  arrMsgRec中;  
                int length = -1;
                try
                {
                    length = sockClient.Receive(arrMsgRec , 0, Msg_send_size, SocketFlags.None); // 接收資料,並返回資料的長度;  
                }
                catch (SocketException se)
                {
                    Error_Info += "異常;" + se.Message;
                    WriteLogger(se.ToString());
                    return;
                }
                catch (Exception se)
                {
                    Error_Info += "異常:" + se.Message;
                    WriteLogger(se.ToString());
                    return;
                }
                try
                {
                    //Infomation_Select(arrMsgRec, length);  //資訊篩選                   
                    AddRecInfo(arrMsgRec);                   
                    
                }
                catch (Exception ex)
                {
                    Error_Info = ex.Message;
                    WriteLogger(ex.ToString());
                }
            }
        }
這邊接收的基本和伺服器一個樣沒什麼好說的同樣要注意
Msg_send_size

/// <summary>
        /// 傳送檔案流
        /// </summary>
        /// <param name="msg">檔案流資訊</param>
        public override void SendFileByte(byte []msg)
        {
            try {
                sockClient.Send(msg);//傳送位元組流
            }catch(Exception ex)
            {
                Error_Info = ex.Message;
            }
        }
接下來是客戶端傳送 位元組流資訊基本上看下就明白了。

目前就先這樣 有問題歡迎大家提出來