你也可以寫個聊天程式 – C# Socket學習1
- 2019 年 10 月 14 日
- 筆記
簡述
我們做軟體工作的雖然每天都離不開網路,可網路協議細節卻不是每個人都會接觸和深入了解。我今天就來和大家一起學習下Socket,並寫一個簡單的聊天程式。
一些基礎類
首先我們每天打開瀏覽器訪問網頁資訊都是使用的HTTP/HTTPS協議,而HTTP是通過的TCP建立的連接。TCP底層又是通過的Socket套接字進行的通訊。所以他們之間的抽象關係是:
我們在學習Socket編程的時候可能會需要用到IPEndPoint、Dns、IPAddress等類,再往上TCP相關有TcpListener、TcpClient、NetworkStream,再往上就是大家熟悉的HttpClient等。
IPEndPoint、Dns、IPAddress基礎作用如下:
//根據DNS獲取域名綁定的IP foreach (var address in Dns.GetHostEntry("www.baidu.com").AddressList) { Console.WriteLine($"百度IP:{address}"); } //字元串轉IP地址 IPAddress ipAddress = IPAddress.Parse("192.168.1.101"); //通過IP和埠構造IPEndPoint對象,用於遠程連接 //通過IP可以確定一台電腦,通過埠可以確定電腦上的一個程式 IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 80);
利用Socket編寫聊天程式
我們首先從Socket開始講起。
要實現Socket通訊,先得有個服務端的監聽,再有個客戶端的連接,然後客戶端和服務端就可以通訊了。如下:
服務端程式碼如下:
//1 創建Socket對象 socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2 綁定ip和埠 IPAddress ip = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001); socketServer.Bind(ipEndPoint); //3、開啟偵聽(等待客戶機發出的連接),並設置最大客戶端連接數為10 socketServer.Listen(10); //4、【阻塞】,等待客戶端連接 Socket newSocket = socketServer.Accept(); //5、【阻塞】,等待讀取客戶端發送過來的數據 byte[] data = new byte[1024 * 1024]; int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None); //6、讀取數據 var msg = Encoding.UTF8.GetString(data, 0, readLeng);
客戶端程式碼如下:
//1 創建Socket對象 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2 連接到服務端 IPAddress ip = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001); socketClient.Connect(ipEndPoint); //3 發送消息到服務端 socketClient.Send(Encoding.UTF8.GetBytes("你好,農碼一生"));
到此,我們就可以開啟服務端的服務,並接受客戶端的發來的消息了。
不過,這裡有個很大的問題,服務端只能建立一個客戶端連接和接受一次客戶端發來的消息。如果想要連接更多的客戶端和接受無數次的消息,服務端程式碼兩處阻塞的地方需要另外開一個執行緒然後包到循環裡面去。
修改後的服務端程式碼如下:
void .... () { //1 創建Socket對象 socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2 綁定ip和埠 IPAddress ip = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001); socketServer.Bind(ipEndPoint); //3、開啟偵聽(等待客戶機發出的連接),並設置最大客戶端連接數為10 socketServer.Listen(10); //開啟新的執行緒,循環等待新的客戶端連接 Task.Run(() => { Accept(socketServer); }); } void Accept(Socket socket) { while (true) { //4、【阻塞】,等待客戶端連接 Socket newSocket = socket.Accept(); //開啟新的執行緒,循環等待接收新的數據 Task.Run(() => { Receive(newSocket); }); } } void Receive(Socket newSocket) { while (true) { //5、【阻塞】,等待讀取客戶端發送過來的數據 byte[] data = new byte[1024 * 1024]; int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None); //6、讀取數據 var msg = Encoding.UTF8.GetString(data, 0, readLeng); } }
到此,服務端就可以接受多個客戶端的連接和接收多次客戶端發來的消息了。不過我們可能還需客服端能接收服務端發來的消息,這個你可以自己嘗試下。文末會提供完整的程式碼參考。
注意: 用Socket來編寫聊天軟體是長連接,有狀態的。不確定服務端什麼時候會發送消息過來,我們也可以連續發送消息而不響應。所以,對於消息的接收就需要開一個新的執行緒循環接收。 而對於HTTP來說,雖然它是也是通過TCP建立的通訊,但在數據請求完畢後會馬上關閉連接,這個過程很短。每次訪問都會建立一個新的連接,是無狀態的。 對於瀏覽器來說是一問一答的形式,先發送請求(Send),然後接收響應(Receive)所以就可以做到不開啟新的執行緒,直接有序的同步的完成。這個在下一篇《模擬瀏覽器的請求和服務端的響應》會具體分析。
利用TCP編寫聊天程式
雖然上面我們利用Socket類實現了一個簡單的聊天程式,但是微軟覺得Socket太複雜。為了讓你們早點幹完活,早點下班,於是又在Socket的基礎上有封裝了兩個相關的類TcpListener、TcpClient。
利用TcpListener、TcpClient來實現同上面相同的功能。
服務端程式碼:
void ...() { //1 開啟監聽 TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 9999); tcpListener.Start(10); //最多同時接收10個用戶連接 //開啟一個執行緒,循環等待客戶端的連接 Task.Run(() => { Accept(); }); } //等待客戶端的連接 void Accept() { while (true) { //2 【阻塞】等待客戶端的連接 TcpClient tcpClient = tcpListener.AcceptTcpClient(); NetworkStream networkStream = tcpClient.GetStream(); //開啟一個新的執行緒 等待新的消息 Task.Run(() => { Read(networkStream, tcpClient); }); } } //接收消息 void Read(NetworkStream networkStream) { while (true) { byte[] buffer = new byte[1024 * 1024]; //3 【阻塞】等待接收新的消息 var readLen = networkStream.Read(buffer, 0, buffer.Length); var msg = Encoding.UTF8.GetString(buffer, 0, readLen); } }
客戶端程式碼:
//1 連接服務端 TcpClient tcpClient = new TcpClient(); tcpClient.Connect(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text)); //2 發送消息到服務端 byte[] buffer = Encoding.UTF8.GetBytes("你好,農碼一生"); networkStream.Write(buffer, 0, buffer.Length);
用TcpListener、TcpClient的實現也算ok了,TcpListener程式碼寫的服務端和Socket通訊也是完成沒問題的,因為他們最後都是Socket。
對此你有覺得比Socket簡單和容易理解?其實我更習慣Socket。
注意:
// 1、斷開連接使用 socketClient?.Shutdown(SocketShutdown.Both); socketClient?.Close(); // 2、服務端需要判斷 int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None); if (readLeng == 0)//客戶端斷開連接 { //停止會話(禁用Socket上的發送和接收,該方法允許Socket對象一直等待,直到將內部緩衝區的數據發送完為止) newSocket.Shutdown(SocketShutdown.Both); //關閉連接 newSocket.Close(); //跳出循環 return; } 3、具體請參考文末提供的完整demo
效果圖
結束
- 完整Demo:https://github.com/zhaopeiym/BlogDemoCode/tree/master/Socket編程
- 本文已同步至索引目錄:《IoTClient開發系列》
- 關於demo本來打算用.net core3.0來寫的,可是winform的設計器不爭氣。所以demo還是用的.net fx,只有TcpListener是用的.net core控制台寫的。
- 為什麼要寫這個,一是學習下底層的協議通訊,二是為了實現一套物聯網設備通訊協議做準備。類似dathlin這位大佬的Hsl。
- 推薦閱讀