聊天程序簡述
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,等待客戶段發來的信息。

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線程,致使點擊開啟服務監聽后,界面無響應。
解決辦法:使用多線程,我們在這里寫一個自己的線程讓這里的監聽服務,寫在自己的線程里面。修改代碼如下:

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地址,端口號。

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方法發送消息。
服務端代碼:

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); } }
客戶端代碼:

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集合中,便於服務端與多個客戶端連接時,選擇發送信息。同時也避免了,不知道發送給哪個客戶端數據。
客戶端代碼:

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; } } } }
服務端代碼:

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了。再次友情提醒一下,如果你不懂多線程,我的上一篇博客就是對他的淺解 。鏈接地址------->>“ 淺解多線程 ” 。