1.前言 在學習Socket之前,先來學習點網路相關的知識吧,自己學習過程中的一些總結,Socket是一門很高深的學問,本文只是Socket一些最基礎的東西,大神請自覺繞路。 傳輸協議 TCP:Transmission Control Protocol 傳輸控制協議TCP是一種面向連接(連接導向)的 ...
1.前言
在學習Socket之前,先來學習點網路相關的知識吧,自己學習過程中的一些總結,Socket是一門很高深的學問,本文只是Socket一些最基礎的東西,大神請自覺繞路。
傳輸協議
TCP:Transmission Control Protocol 傳輸控制協議TCP是一種面向連接(連接導向)的、可靠的、基於位元組流的運輸層(Transport layer)通信協議。 特點: 面向連接的協議,數據傳輸必須要建立連接,所以在TCP中需要連接時間。 傳輸數據大小限制,一旦連接建立,雙方可以按統一的格式傳輸大的數據。 一個可靠的協議,確保接收方完全正確地獲取發送方所發送的全部數據。 說到TCP就不得不說經典的三次握手。 在TCP/IP協議中,TCP協議通過三次握手建立一個可靠的連接第一次握手:客戶端嘗試連接伺服器,向伺服器發送syn包(同步序列編號Synchronize Sequence Numbers),syn=j,客戶端進入SYN_SEND狀態等待伺服器確認
第二次握手:伺服器接收客戶端syn包並確認(ack=j+1),同時向客戶端發送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態
第三次握手:第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手
UDP: User Datagram Protocol的簡稱, 中文名是用戶數據包協議,是 OSI 參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。 特點: 每個數據報中都給出了完整的地址信息,因此無需要建立發送方和接收方的連接。 UDP傳輸數據時是有大小限制的,每個被傳輸的數據報必須限定在64KB之內。 UDP是一個不可靠的協議,發送方所發送的數據報並不一定以相同的次序到達接收方。TCP協議:就好比兩個電話機 通過電話線進行數據交互的格式約定
HTTP協議:就好比兩個人 通過電話機 說話的語法。
(1)公認埠(WellKnownPorts):從0到1023,它們緊密綁定(binding)於一些服務。通常這些埠的通訊明確表明瞭某種服務的協議。例如:80埠實際上總是HTTP通訊。
(2)註冊埠(RegisteredPorts):從1024到49151。它們鬆散地綁定於一些服務。也就是說有許多服務綁定於這些埠,這些埠同樣用於許多其它目的。例如:許多系統處理動態埠從1024左右開始。
(3)動態和/或私有埠(Dynamicand/orPrivatePorts):從49152到65535。理論上,不應為服務分配這些埠。實際上,機器通常從1024起分配動態埠。
OSI網路7層模型
TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標準的協議集,它是為廣域網(WANs)設計的。 UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。 應用層 (Application):應用層是個很廣泛的概念,有一些基本相同的系統級 TCP/IP 應用以及應用協議,也有許多的企業商業應用和互聯網應用。 傳輸層 (Transport):傳輸層包括 UDP 和 TCP,UDP 幾乎不對報文進行檢查,而 TCP 提供傳輸保證。 網路層 (Network):網路層協議由一系列協議組成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。 鏈路層 (Link):又稱為物理數據網路介面層,負責報文傳輸。 IP地址 每台聯網的電腦都有一個唯一的IP地址。 長度32位,分為四段,每段8位,用十進位數字表示,每段範圍 0 ~ 255 特殊IP:127.0.0.1 用戶本地網卡測試 版本:V4(32位) 和 V6(128位,分為8段,每段16位) 埠 在網路上有很多電腦,這些電腦一般運行了多個網路程式。每種網路程式都打開一個Socket,並綁定到一個埠上,不同的埠對應於不同的網路程式。 常用埠:21 FTP ,25 SMTP ,110 POP3 ,80 HTTP , 443 HTTPS 有兩種常用Socket類型: 流式Socket(STREAM):是一種面向連接的Socket,針對於面向連接的TCP服務應用,安全,但是效率低 數據報式Socket(DATAGRAM):
是一種無連接的Socket,對應於無連接的UDP服務應用.不安全(丟失,順序混亂,在接收端要分析重排及要求重發),但效率高. 說了那麼多,讓我們來看看socket在網路7層協議中的位置。如下圖所示
2.聊天室原理
Socket 流式(伺服器端和客戶端 伺服器端的Socket(至少需要兩個) 一個負責接收客戶端連接請求(但不負責與客戶端通信) 每成功接收到一個客戶端的連接便在服務端產生一個對應的負責通信的Socket 在接收到客戶端連接時創建. 為每個連接成功的客戶端請求在服務端都創建一個對應的Socket(負責和客戶端通信). 客戶端的Socket 客戶端Socket 必須指定要連接的服務端IP地址和埠。 通過創建一個Socket對象來初始化一個到伺服器端的TCP連接 Socket的通訊過程 伺服器端: 申請一個socket 綁定到一個IP地址和一個埠上 開啟偵聽,等待接授連接 客戶端: 申請一個socket 連接伺服器(指明IP地址和埠號) l伺服器端接到連接請求後,產生一個新的socket(埠大於1024)與客戶端建立連接併進行通訊,原監聽socket繼續監聽。 Socket常用的一些類和方法 IPAddress類:包含了一個IP地址 IPEndPoint類:包含了一對IP地址和埠號 Socket (): 創建一個Socket Bind(): 綁定一個本地的IP和埠號(IPEndPoint) Listen(): 讓Socket偵聽傳入的連接嘗試,並指定偵聽隊列容量 Connect(): 初始化與另一個Socket的連接 Accept(): 接收連接並返回一個新的socket Send(): 輸出數據到Socket Receive(): 從Socket中讀取數據 Close(): 關閉Socket (銷毀連接)3.聊天室代碼
伺服器端代碼:
using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Server { using System.Net.Sockets; using System.Net; using System.Threading; public partial class Form1 : Form {
public Form1() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false; }
//服務端 監聽套接字 Socket socketWatch = null; //服務端 監聽線程 Thread threadWatch = null; //字典集合:保存 通信套接字 Dictionary<string, Socket> dictCon = new Dictionary<string, Socket>();
private void btnStartListen_Click(object sender, EventArgs e) { try { //1.創建監聽套接字 使用 ip4協議,流式傳輸,TCP連接 socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.綁定埠 //2.1獲取網路節點對象 IPAddress address = IPAddress.Parse(txtIP.Text); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text)); //2.2綁定埠(其實內部 就向系統的 埠表中 註冊 了一個埠,並指定了當前程式句柄) socketWatch.Bind(endPoint); //2.3設置監聽隊列 socketWatch.Listen(10); //2.4開始監聽,調用監聽線程 執行 監聽套接字的 監聽方法 threadWatch = new Thread(WatchConnecting); threadWatch.IsBackground = true; threadWatch.Start(); ShowMsg("楓伶憶,伺服器啟動啦!"); } catch (Exception ex) { MessageBox.Show(ex.Message); throw; } }
void WatchConnecting() { //2.4開始監聽:此方法會阻斷當前線程,直到有 其它程式 連接過來,才執行完畢 Socket sokMsg = socketWatch.Accept(); //將當前連接成功的 【與客戶端通信的套接字】 的 標識 保存起來,並顯示到 列表中 //將 遠程客戶端的 ip和埠 字元串 存入 列表 this.lbOnline.Items.Add(sokMsg.RemoteEndPoint.ToString()); //將 服務端的通信套接字 存入 字典集合 dictCon.Add(sokMsg.RemoteEndPoint.ToString(), sokMsg); ShowMsg("有客戶端連接了!"); //2.5創建 通信線程 Thread thrMsg = new Thread(ReceiveMsg); thrMsg.IsBackground = true; thrMsg.Start(sokMsg); }
void ReceiveMsg(object obj) { try { Socket sokMsg = obj as Socket; //3.通信套接字 監聽 客戶端的 消息 //3.1創建 消息緩存區 byte[] arrMsg = new byte[1024 * 1024 * 1]; while (isReceive) { //3.2接收客戶端的消息 並存入 緩存區,註意:Receive方法也會阻斷當前的線程 sokMsg.Receive(arrMsg); //3.3將接收到的消息 轉成 字元串 string strMsg = System.Text.Encoding.UTF8.GetString(arrMsg); //3.4將消息 顯示到 文本框 ShowMsg(strMsg); } } catch (Exception ex) { MessageBox.Show(ex.Message); throw; } }
void ShowMsg(string strmsg) { this.txtShow.AppendText(strmsg + "\r\n"); }
private void btnSend_Click_1(object sender, EventArgs e) { string strClient = this.lbOnline.Text; if (string.IsNullOrEmpty(strClient)) { MessageBox.Show("請選擇你要發送消息的客戶端"); return; } if (dictCon.ContainsKey(strClient)) { string strMsg = this.txtInput.Text.Trim(); ShowMsg("\r\n向客戶端【" + strClient + "】說:" + strMsg); //使用 指定的 通信套接字 將 字元串 發送到 指定的客戶端 byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg); dictCon[strClient].Send(arrMsg); } this.txtInput.Text = ""; }
}
}
客戶端代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Client { using System.Net.Sockets; using System.Net; using System.Threading; public partial class Form1 : Form {
public Form1() { InitializeComponent(); TextBox.CheckForIllegalCrossThreadCalls = false; }
//客戶端 通信套接字 Socket socketMsg = null; //客戶端 通信線程 Thread threadMsg = null; bool isRec = true;//標記任務
private void btnConnect_Click(object sender, EventArgs e) { try { //1.創建監聽套接字 使用 ip4協議,流式傳輸,TCP連接 socketMsg = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.獲取要連接的服務端 節點 //2.1獲取網路節點對象 IPAddress address = IPAddress.Parse(txtIP.Text); IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text)); //3.向服務端 發送鏈接請求 socketMsg.Connect(endPoint); ShowMsg("連接伺服器成功~~!"); //4.開啟通信線程 threadMsg = new Thread(RecevieMsg); threadMsg.IsBackground = true; threadMsg.Start(); } catch (Exception ex) { MessageBox.Show(ex.Message); throw; } }
void RecevieMsg() { try { //3.1創建 消息緩存區 byte[] arrMsg = new byte[1024 * 1024 * 1]; while (isRec) { socketMsg.Receive(arrMsg); string strMsg = System.Text.Encoding.UTF8.GetString(arrMsg); ShowMsg("\r\n伺服器說:" + strMsg); } } catch (Exception ex) { MessageBox.Show(ex.Message); throw; } }
private void btnSend_Click_1(object sender, EventArgs e) { string strMsg = this.txtInput.Text.Trim(); byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(strMsg); socketMsg.Send(arrMsg); this.txtInput.Text = ""; }
void ShowMsg(string strmsg) { this.txtShow.AppendText(strmsg + "\r\n"); }
}
}
最終的效果圖如下:
4.註意
至少要定義一個要連接的遠程主機的IP和埠號。
埠號必須在 1 和 65535之間,最好在1024以後。 要連接的遠程主機必須正在監聽指定埠,也就是說你無法隨意連接遠程主機。 如: IPAddress addr = IPAddress.Parse("127.0.0.1"); IPEndPoint endp = new IPEndPoint(addr, 8989);服務端先綁定:serverWelcomeSocket.Bind(endp)
客戶端再連接:clientSocket.Connect(endp)
一個Socket一次只能連接一臺主機。 Socket關閉後無法再次使用。 每個Socket對象只能一臺遠程主機連接. 如果你想連接到多台遠程主機, 你必須創建多個Socket對象5.擴展
l實現傳送文件 如果接收數據是文件還是文字? 設計"協議": 把要傳遞的位元組數組前面都加上一個位元組做為標識。0:表示文字 1:表示文件 即:文字: 0+文字(位元組數組表示) 文件:1+文件的二進位信息比如Socket的分包,黏包問題,非同步編程在後續的文章繼續討論