說明:本隨筆主要演示自己給自己發送消息例子,分別使用了TCP協議、UDP協議以及socket套接字通信。使用socket套接字了模擬TCP、UDP通信實現原理。其中有些源碼都來自《C#高級編程 第7版》,並附加了自己的理解,有的也進行了一些簡單的拓展。
第一次原創隨筆,很多地方可能考慮不周或理解有誤,希望大家留言指正,與大家共同進步。也希望大家不喜勿噴,給點鼓勵還是比較好的~~ 閑話少說,進入主題!
一、TCP 類
TCP是基於連接的一種通信模式,我們需要創建客戶端與服務器端來進行通信。在客戶端讀取文件,並將文件內容發送到服務器端進行顯示。
客戶端:
1 using System; 2 using System.Windows.Forms; 3 using System.Net; 4 using System.Net.Sockets; 5 using System.IO; 6 7 namespace TcpSender 8 { 9 public partial class Form1 : Form 10 { 11 public Form1() 12 { 13 InitializeComponent(); 14 } 15 16 private void btnSend_Click(object sender, EventArgs e) 17 { 18 TcpClient tcpClient = new TcpClient("127.0.0.1",2112); // 配置主機號及端口 127.0.0.1 2112 19 NetworkStream nws = tcpClient.GetStream(); // 獲取連接流 20 string path = txbFile.Text; // 配置文件路徑 21 FileStream fs = new FileStream(path, FileMode.Open); 22 int data = fs.ReadByte(); 23 24 while (data != -1) 25 { 26 nws.WriteByte((byte)data); 27 data = fs.ReadByte(); 28 } 29 fs.Close(); 30 nws.Close(); 31 tcpClient.Close(); 32 } 33 } 34 }
服務器:
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.IO; 5 using System.Threading; 6 using System.Windows.Forms; 7 8 namespace TcpReceiver 9 { 10 public partial class Form1 : Form 11 { 12 public Form1() 13 { 14 InitializeComponent(); 15 Thread thread = new Thread(new ThreadStart(Listen)); // 防止主線程在監聽到請求之前處於卡死狀態 16 thread.Start(); 17 } 18 19 // 監聽功能 20 public void Listen() 21 { 22 IPAddress localAddr = IPAddress.Parse("127.0.0.1"); 23 Int32 port = 2112; 24 TcpListener tcpListener = new TcpListener(localAddr, port); // 也可以僅綁定端口,因為發送者與接收者均是本地 25 tcpListener.Start(); 26 27 while (true) // 持續監聽 28 { 29 TcpClient tcpClient = tcpListener.AcceptTcpClient(); // 監聽到請求前處於阻塞狀態 30 NetworkStream nws = tcpClient.GetStream(); // 獲取監聽流 31 32 StreamReader sr = new StreamReader(nws, System.Text.Encoding.UTF8); 33 string content = sr.ReadToEnd(); 34 35 // ★★★ very important ★★★ 36 Invoke(new Action<string>(UpdateDisplay), new object[] { content }); // 該線程擁有用戶界面的句柄,因此無需從后台線程直接訪問對話框 37 tcpClient.Close(); 38 } 39 // tcpListener.Stop(); 40 } 41 42 // 將接收到內容顯示到控件中 43 public void UpdateDisplay(string text) 44 { 45 txbContent.Clear(); 46 txbContent.Text = text; 47 } 48 } 49 }
運行效果圖:
客戶端
服務器
二、UDP
UDP是一個無連接的協議,因此可以在一個程序中實現收發消息。
情形1:使用同一個 UdpClient 實例實現收發消息
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.Text; 5 using System.Threading; 6 using System.IO; 7 8 namespace UDPSender 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 #region 模擬自己發送自己 使用UDPClient(通過) 15 UdpClient client = new UdpClient(11000); //相當於給socket實例指定端口,參見socket模擬UDP實現原理(后續代碼) 16 IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000); // 服務器網址及端口 17 18 Console.WriteLine("Type in what you want to send :"); 19 string sendMsg = Console.ReadLine(); 20 byte[] sendBytes = Encoding.ASCII.GetBytes(sendMsg); 21 client.Send(sendBytes, sendBytes.Length, iep); //將消息推送到 127.0.0.1:11000 22 23 24 byte[] rcvBytes = client.Receive(ref iep); // 接收127.0.0.1:11000 端口消息,非連接的消息需要緩存存放 25 Console.WriteLine("Received the following message:"); 26 string rcvMessage = Encoding.ASCII.GetString(rcvBytes, 0, rcvBytes.Length); 27 Console.WriteLine(rcvMessage); 28 Console.ReadKey(); 29 #endregion 30 } 31 } 32 }
情形2:使用一個 UdpClient 實例發送消息, 使用另一個 UdpClient 實例接收消息
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.Text; 5 using System.Threading; 6 using System.IO; 7 8 namespace UDPSender 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 #region 模擬自己發送自己 使用UDPClient(通過) 15 UdpClient client = new UdpClient(); // 沒有為socket指定端口,則在client.Send()時,系統默認為socket分配一個端口,參見Socket模擬UDP(后續代碼) 16 // IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0); 17 UdpClient server = new UdpClient(11000); // 在客戶端發送之前確保服務器已經進行端口綁定,為客戶端發送的消息創建載體 18 19 IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000); // 192.168.106.231 服務器網址及端口 20 21 Console.WriteLine("Type in what you want to send :"); 22 string sendMsg = Console.ReadLine(); 23 byte[] sendBytes = Encoding.ASCII.GetBytes(sendMsg); 24 client.Send(sendBytes, sendBytes.Length, iep); // 將消息推送到 127.0.0.1:11000 25 26 27 byte[] rcvBytes = server.Receive(ref iep); // 接收127.0.0.1:11000 端口消息,也可以使用 remote 端點,表示接收所有端口消息 28 Console.WriteLine("Received the following message:"); 29 string rcvMessage = Encoding.ASCII.GetString(rcvBytes, 0, rcvBytes.Length); 30 Console.WriteLine(rcvMessage); 31 Console.ReadKey(); 32 #endregion 33 } 34 } 35 }
運行效果圖:
三、Socket 通信
TCP、UDP 底層通信都是通過 socket 套接字實現。
1、模擬 TCP 通信實現
客戶端:
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.Text; 5 6 namespace SocketSender 7 { 8 // 使用socket類模擬TCP發送原理 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 byte[] ReceiveBytes = new byte[1024]; 14 IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2112); // 創建遠程端點 15 16 Console.WriteLine("Starting: Creating Socket Object "); 17 while (true) 18 { 19 // 創建socket對象 20 Socket SocketSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 21 // 連接遠程端點 22 SocketSender.Connect(ipEndPoint); // 連接到遠程端點,如果服務器端沒有響應,則此處會掛起。若超時,則拋出遠程服務器拒絕的異常。 23 24 Console.WriteLine("Successfully Connected to {0}",SocketSender.RemoteEndPoint); 25 26 Console.WriteLine("Input the Message you want to send :"); 27 string SendingMessage = Console.ReadLine(); 28 byte[] ForwardMessage = Encoding.ASCII.GetBytes(SendingMessage + "[FINAL]"); 29 30 // 發送消息 31 SocketSender.Send(ForwardMessage); 32 33 // 接收從服務器傳回相應消息,因為是基於連接的,因此使用同一個socket實例直接接收即可 34 int TotalBytesReceived = SocketSender.Receive(ReceiveBytes); 35 36 // 打印輸出接收到的消息 37 Console.WriteLine("Message Provided By Server: {0}", Encoding.ASCII.GetString(ReceiveBytes, 0, TotalBytesReceived)); 38 Console.WriteLine(Environment.NewLine); 39 SocketSender.Shutdown(SocketShutdown.Both); // 關閉該socket的發送和接收功能 40 SocketSender.Close(); 41 } 42 //Console.ReadKey(); 43 } 44 } 45 } 46
服務器端:
1 using System; 2 using System.Text; 3 using System.Net; 4 using System.Net.Sockets; 5 6 namespace SocketReceiver 7 { 8 // 使用socket模擬TCP接收原理 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 // 本實例是一個基於連接的socket通信,模擬 TCPClient 進行通信 14 Console.WriteLine("Starting: Creating Socket object"); 15 Socket Listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 16 17 Listener.Bind(new IPEndPoint(IPAddress.Any, 2112)); // 創建了基於本機IP地址的,端口號為任意值的,可用於所有網絡接口(網線、電話線及其他形式網絡接口)的綁定 18 Listener.Listen(10); // 掛起連接隊列的最大長度 19 20 while (true) 21 { 22 Console.WriteLine("Waiting for connetion on port 2112 "); 23 Socket socket = Listener.Accept(); // 若無socket,則處於阻塞狀態,等待有效的socket實例 24 string ReceiveValue = string.Empty; 25 26 while (true) 27 { 28 byte[] ReceivedByte = new byte[1024]; // 緩存區 29 int NumBytes = socket.Receive(ReceivedByte); // 接收到的是二進制數據,將接收到的數據放入到緩存中。也可以直接讀到流中,使用 Stream stream = new networkstream(socket) 30 Console.WriteLine("Receiving . . ."); 31 ReceiveValue += Encoding.ASCII.GetString(ReceivedByte, 0, NumBytes); 32 if (ReceiveValue.IndexOf("[FINAL]") > -1) 33 { 34 break; 35 } 36 } 37 Console.WriteLine("Received Value: {0}", ReceiveValue); 38 Console.WriteLine(Environment.NewLine); 39 string ReplyValue = "Message successfully Received."; 40 byte[] ReplyByte = Encoding.ASCII.GetBytes(ReplyValue); 41 socket.Send(ReplyByte); // 發送的是二進制數據 42 43 // 對於面向連接的協議,建議先調用 Shutdown,然后再調用 Close。 這可以確保在已連接的套接字關閉之前,已發送和接收該套接字上的所有數據。 44 socket.Shutdown(SocketShutdown.Both); // 關閉該socket的關閉和接收功能 45 socket.Close(); 46 } 47 // Listener.Close(); 48 } 49 } 50 }
運行效果圖:
客戶端
服務器端
說明:基於鏈接的通信,應先啟動服務器端程序進行監聽,后啟動客戶端程序。否則客戶端在嘗試連接時,沒有服務器進行相應,嘗試一定的時間后,若一直無響應,則拋出遠程主機拒絕的異常。
2、模擬 UDP 通信實現
不基於連接的通信,只需要一個程序端即可。
1 using System; 2 using System.Net; 3 using System.Net.Sockets; 4 using System.Text; 5 using System.Threading; 6 using System.IO; 7 8 namespace UDPSender 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 #region 模擬自己發送給自己,使用socket模擬UDP實現原理(通過) 15 16 // Server 17 Thread server = new Thread(new ThreadStart(ReceiveServer)); 18 server.Start(); 19 20 // ★★★確保服務端已建立網絡監聽★★★ 21 Thread.Sleep(50); 22 23 // Client 24 IPHostEntry ipHostEntry = Dns.GetHostEntry("127.0.0.1"); 25 IPAddress ipAddress = ipHostEntry.AddressList[0]; 26 IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 2112); 27 28 while (true) 29 { 30 // UDP 的socket類型不支持stream 31 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); 32 //socket.Bind(ipEndPoint); 33 socket.Connect(ipEndPoint); // 若在此之前不給socket手動綁定一個端口,則程序會在此時默認給socket隨機綁定一個端口 34 Console.WriteLine("Type in what you want to send:"); 35 string SendString = Console.ReadLine(); 36 byte[] SendBytes = Encoding.Default.GetBytes(SendString); 37 //Thread.Sleep(3000); 38 socket.Send(SendBytes); 39 // 網絡接收有延時,因此需要等待服務器進行接收 40 socket.Close(100);//等待 timeout 秒以發送所有剩余數據,然后關閉該套接字。 41 // 等同於socket.Close(100) 的效果 42 //Thread.Sleep(100); 43 //socket.Close(); 44 45 } 46 #endregion 47 } 48 49 public static void ReceiveServer() 50 { 51 IPHostEntry ipHostEntry = Dns.GetHostEntry("127.0.0.1"); 52 IPAddress ipAddress = ipHostEntry.AddressList[0]; 53 IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 2112); 54 55 #region 使用緩存接收數據 56 // Receive Message 57 Socket socketListen = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // set the protocol 58 socketListen.Bind(ipEndPoint); // binding to the local point which need to be listened 59 60 try 61 { 62 // UDP 協議不需要監聽,直接接收消息即可 63 //socketListen.Listen(5); // 最多監聽到5個隊列的請求 64 while (true) 65 { 66 string result = string.Empty; 67 if (socketListen.Available < 1) // 判斷是否有數據,若沒有數據而直接進行receive,則會使線程阻塞等待數據的到來。return 無:0 有:>0 68 { 69 Thread.Sleep(20); 70 } 71 else 72 { 73 byte[] data = new byte[1024]; 74 int NumBytes = socketListen.Receive(data); // 如果網絡監聽不到數據,則該語句會使線程處於阻塞狀態而等待消息,因此要添加判斷語句判斷數據是否有效 75 Console.WriteLine("Receiving.."); 76 if (NumBytes > 0) 77 { 78 result = Encoding.Default.GetString(data, 0, NumBytes); 79 Console.WriteLine("Server Received the follow message:"); 80 Console.WriteLine(result); 81 Console.WriteLine(Environment.NewLine); 82 //Thread.Sleep(50000); 83 } 84 } 85 } 86 } 87 catch (SocketException e) 88 { 89 string a = e.ErrorCode.ToString(); 90 Console.WriteLine(e.Message + e.ErrorCode); 91 } 92 #endregion 93 } 94 } 95 }
運行效果圖:
注意:對於非連接的通信,在服務器端socket綁定端口(Bind()方法,而非Receive()方法,注意區分)之前,若客戶端發送了消息A,則在服務器端口綁定后收不到該消息A,只能收到在綁定端口后客戶端發送來的消息!