學習任何東西,我們只要搞清楚其原理,就會觸類旁通。現在結和我所學,我想總結一下客戶端到伺服器端的通訊過程。只有明白了原理,我們才會明白當我們程式開發過程中錯誤的問題會出現在那,才會更好的解決問題。
我們首先要了解一個概念性的詞彙:Socket
socket的英文原義是“孔”或“插座”。作為程式通訊機制,取後一種意思。通常也稱作“套接字”,用於描述IP地址和埠,是一個通訊鏈的控制程式碼。(其實就是兩個程式通訊用的。)socket非常類似於電話的插座。以一個電話網為例。電話的通話雙方相當於相互通訊的2個程式,電話號碼可以當作是IP地址。任何使用者在通話之前,首先要佔有一部電話機,相當於申請一個socket;同時要知道對方的號碼(IP地址),相當於對方有一個固定的socket。然後向對方撥號呼叫,相當於發出連線請求。對方假如在場並空閒,拿起電話話筒,雙方就可以正式通話,相當於連線成功。雙方通話的過程,是一方向電話機發出訊號和對方從電話機接收訊號的過程,相當於向socket傳送資料和從socket接收資料。通話結束後,一方掛起電話機相當於關閉socket,撤消連線,通訊完成。
以上通訊是以兩個人通話做為事例來在概的說明了下通訊,但是現在假如通訊中的一個人是外國人(說英語),一個人是中國人(說普通話),他們倆相互通訊的話,都不能聽明白對方說的是什麼,那麼他們的溝通就不能夠完成。但是如果我們給一個規定,給通話雙方,只能講普通話,那麼雙方溝通就沒有障礙了。這就引出來了通訊協議。
有兩種型別:(Tcp協議與Udp協議):
Tcp協議與Udp協議是在兩硬體裝置上進行通訊傳輸的一種資料語法。
– 流式Socket(STREAM):
是一種面向連線的Socket,針對於面向連線的TCP服務應用,安全,但是效率低;Tcp:是以流的形式來傳的。
– 資料包式Socket(DATAGRAM):
是一種無連線的Socket,對應於無連線的UDP服務應用.不安全(丟失,順序混亂,在接收端要分析重排及要求重發),但效率高.Udp:將資料包拆開為若干份編號後來傳輸。在傳輸的過程中容易出現資料的丟失。但是傳輸速度要比TCP的快。
Socket的通訊流程
- Demo:
- 伺服器端:
– 申請一個socket (socketWatch)用來監聽的
– 繫結到一個IP地址和一個埠上
– 開啟偵聽,等待接授客戶端的連線
– 當有連線時建立一個用於和連線進來的客戶端進行通訊的socket(socketConnection)
– 即續監聽,等侍下一個客戶的連線
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
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;//IPAdress,IPEndPoint(ip和埠)類 using System.Net.Sockets; using System.Threading; using System.IO; namespace MyChatRoomServer { public partial class FChatServer : Form { public FChatServer() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false;//關閉 對 文字框 的跨執行緒操作檢查 } Thread threadWatch = null;//負責監聽 客戶端 連線請求的 執行緒 Socket socketWatch = null;//負責監聽的 套接字 private void btnBeginListen_Click(object sender, EventArgs e) { //建立 服務端 負責監聽的 套接字,引數(使用IP4定址協議,使用流式連線,使用TCP協議傳輸資料) socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //獲得文字框中的 IP地址物件 IPAddress address = IPAddress.Parse(txtIP.Text.Trim()); //建立 包含 ip 和 port 的網路節點物件 IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim())); //將 負責監聽 的套接字 繫結到 唯一的IP和埠上 socketWatch.Bind(endpoint); //設定監聽佇列的長度 socketWatch.Listen(10); //建立 負責監聽的執行緒,並傳入監聽方法 threadWatch = new Thread(WatchConnecting); threadWatch.IsBackground = true;//設定為後臺執行緒 threadWatch.Start();//開啟執行緒 ShowMsg("伺服器啟動監聽成功~"); //IPEndPoint //socketWatch.Bind( } //儲存了伺服器端 所有負責和客戶端通訊的套接字 Dictionary<string, Socket> dict = new Dictionary<string, Socket>(); //儲存了伺服器端 所有負責呼叫 通訊套接字.Receive方法 的執行緒 Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>(); //Socket sokConnection = null; /// <summary> /// 監聽客戶端請求的方法 /// </summary> void WatchConnecting() { while (true)//持續不斷的監聽新的客戶端的連線請求 { //開始監聽 客戶端 連線請求,注意:Accept方法,會阻斷當前的執行緒! Socket sokConnection = socketWatch.Accept();//一旦監聽到客戶端的請求,就返回一個負責和該客戶端通訊的套接字 sokConnection //sokConnection.Receive //向 列表控制元件中 新增一個 客戶端的ip埠字串,作為客戶端的唯一標識 lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString()); //將 與客戶端通訊的 套接字物件 sokConnection 新增到 鍵值對集合中,並以客戶端IP埠作為鍵 dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection); //建立 通訊執行緒 ParameterizedThreadStart pts = new ParameterizedThreadStart(RecMsg); Thread thr = new Thread(pts); thr.IsBackground = true;//設定為 thr.Start(sokConnection);//啟動執行緒 併為執行緒要呼叫的方法RecMsg 傳入引數sokConnection dictThread.Add(sokConnection.RemoteEndPoint.ToString(), thr);//將執行緒 儲存在 字典裡,方便大家以後做“踢人”功能的時候用 ShowMsg("客戶端連線成功!" + sokConnection.RemoteEndPoint.ToString()); //sokConnection.RemoteEndPoint 中儲存的是 當前連線客戶端的 Ip和埠 } } /// <summary> /// 服務端 負責監聽 客戶端 傳送來的資料的 方法 /// </summary> void RecMsg(object socketClientPara) { Socket socketClient = socketClientPara as Socket; while (true) { //定義一個 接收用的 快取區(2M位元組陣列) byte[] arrMsgRec = new byte[1024 * 1024 * 2]; //將接收到的資料 存入 arrMsgRec 陣列,並返回 真正接收到的資料 的長度 int length=-1; try { length = socketClient.Receive(arrMsgRec); } catch (SocketException ex) { ShowMsg("異常:" + ex.Message); //從 通訊套接字 集合中 刪除 被中斷連線的 通訊套接字物件 dict.Remove(socketClient.RemoteEndPoint.ToString()); //從 通訊執行緒 結合中 刪除 被終端連線的 通訊執行緒物件 dictThread.Remove(socketClient.RemoteEndPoint.ToString()); //從 列表中 移除 被中斷的連線 ip:Port lbOnline.Items.Remove(socketClient.RemoteEndPoint.ToString()); break; } catch (Exception ex) { ShowMsg("異常:" + ex.Message); break; } if (arrMsgRec[0] == 0)//判斷 傳送過來的資料 的第一個元素是 0,則代表傳送來的是 文字資料 { //此時 是將 陣列 所有的元素 都轉成字串,而真正接收到的 只有服務端發來的幾個字元 string strMsgRec = System.Text.Encoding.UTF8.GetString(arrMsgRec,1, length-1); ShowMsg(strMsgRec); } else if (arrMsgRec[0] == 1)//如果是1 ,則代表傳送過來的是 檔案資料(圖片/視訊/檔案....) { SaveFileDialog sfd = new SaveFileDialog();//儲存檔案選擇框物件 if (sfd.ShowDialog() == System.Windows.Forms.DialogResult.OK)//使用者選擇檔案路徑後 { string fileSavePath = sfd.FileName;//獲得要儲存的檔案路徑 //建立檔案流,然後 讓檔案流來 根據路徑 建立一個檔案 using (FileStream fs = new FileStream(fileSavePath, FileMode.Create)) { fs.Write(arrMsgRec, 1, length-1); ShowMsg("檔案儲存成功:" + fileSavePath); } } } } } //傳送訊息到客戶端 private void btnSend_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(lbOnline.Text)) { MessageBox.Show("請選擇要傳送的好友"); } else { string strMsg = txtMsgSend.Text.Trim(); //將要傳送的字串 轉成 utf8對應的位元組陣列 byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg); //獲得列表中 選中的KEY string strClientKey = lbOnline.Text; //通過key,找到 字典集合中對應的 與某個客戶端通訊的 套接字的 send方法,傳送資料給對方 try { dict[strClientKey].Send(arrMsg); //sokConnection.Send(arrMsg); ShowMsg("傳送了資料出去:" + strMsg); } catch (SocketException ex) { ShowMsg("傳送時異常:"+ex.Message); } catch (Exception ex) { ShowMsg("傳送時異常:" + ex.Message); } } } //服務端群發訊息 private void btnSendToAll_Click(object sender, EventArgs e) { string strMsg = txtMsgSend.Text.Trim(); //將要傳送的字串 轉成 utf8對應的位元組陣列 byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg); foreach (Socket s in dict.Values) { s.Send(arrMsg); } ShowMsg("群發完畢!:)"); } #region 顯示訊息 /// <summary> /// 顯示訊息 /// </summary> /// <param name="msg"></param> void ShowMsg(string msg) { |
- 客戶端:
– 申請一個socket(socketClient)
– 連線伺服器(指明IP地址和埠號)
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
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; namespace MyChatRoomClient { public partial class FChatClient : Form { public FChatClient() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false; } Thread threadClient = null; //客戶端 負責 接收 服務端發來的資料訊息的執行緒 Socket socketClient = null;//客戶端套接字 //客戶端傳送連線請求到伺服器 private void btnConnect_Click(object sender, EventArgs e) { IPAddress address = IPAddress.Parse(txtIP.Text.Trim());//獲得IP IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));//網路節點 //建立客戶端套接字 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //向 指定的IP和埠 傳送連線請求 socketClient.Connect(endpoint); //客戶端 建立執行緒 監聽服務端 發來的訊息 threadClient = new Thread(RecMsg); threadClient.IsBackground = true; threadClient.Start(); } /// <summary> /// 監聽服務端 發來的訊息 /// </summary> void RecMsg() { while (true) { //定義一個 接收用的 快取區(2M位元組陣列) byte[] arrMsgRec = new byte[1024 * 1024 * 2]; //將接收到的資料 存入 arrMsgRec 陣列,並返回 真正接收到的資料 的長度 int length= socketClient.Receive(arrMsgRec); //此時 是將 陣列 所有的元素 都轉成字串,而真正接收到的 只有服務端發來的幾個字元 string strMsgRec = System.Text.Encoding.UTF8.GetString(arrMsgRec, 0, length); ShowMsg(strMsgRec); } } void ShowMsg(string msg) { txtMsg.AppendText(msg + "\r\n"); } } } |
通訊過程圖
通過以上流程圖我們可以看出,客戶端與伺服器端之間的一個基本通訊流程,概括一下Socket 一般應用模式(客戶端和伺服器端)的作用:
伺服器端:最少有兩個socket,一個是服務端負責監聽客戶端發來連線請求,但不負責與請求的客戶端通訊,另一個是每當伺服器端成功接收到客戶端時,但在伺服器端建立一個用與請求的客戶端進行通訊的socket.
客戶端:指定要連線的伺服器端地址和埠,通過建立一個socket物件來初始化一個到伺服器端的TCP連線。
其實很早就想寫下這篇總結了,但是由於工作較忙,一直推遲到現在。這篇總結也是為我接下來要寫的瀏覽器與Iis伺服器的通訊過程和ASP.Net頁面生命週期做一個鋪墊,現在終於寫完了,來和大家一起分享一下,不完善的地方,我將在以後的工作和學習過程中慢慢補充。