C#中使用async和await實現(xiàn)異步Udp通訊的示例代碼
在之前的C#版本中, 如果我們想要進行異步的Udp, 需要單開線程接收消息, C#7.1開始, 我們可以使用async/await關(guān)鍵字來編寫異步代碼, 我們今天一起來探索怎么實現(xiàn).
C/S架構(gòu)
我們要實現(xiàn)兩個app, 一個客戶端和一個服務(wù)器, 各自都可以發(fā)消息和收消息.
發(fā)消息很簡單, 收消息的話需要一直在端口上監(jiān)聽.
udp相比tcp來說簡單了很多, 不需要一直保持連接, 也不需要處理發(fā)送回調(diào), 因為udp不可靠, 只要發(fā)了就不管, 丟了也與我無關(guān). 而且因為不需要保證順序, 所以沒有發(fā)送緩存, 只要請求發(fā)送, 立馬就發(fā), 收到的包也不會堆積, 肯定是整包, 所以我們也不需要處理粘包問題.
整個實現(xiàn)的關(guān)鍵點有:
- Sockets.socket: socket類, tcp和udp共用.
- System.Net.IPEndPoint: 端口類, tcp和udp共用.
- Sockets.socket.Bind: 綁定本地端口方法, 主要是服務(wù)器使用.
- Sockets.socket.Create: 綁定遠端端口方法, 主要是客戶端使用.
- Sockets.socket.SendTo: 向指定端口發(fā)送數(shù)據(jù), 主要是服務(wù)器使用.
- Sockets.socket.ReceiveFrom: 從指定端口接收數(shù)據(jù), 主要是服務(wù)器使用.
- Sockets.socket.Send: 從綁定的端口發(fā)送數(shù)據(jù), 主要是客戶端使用.
- Sockets.socket.Receive: 從綁定的端口接收數(shù)據(jù), 主要是客戶端使用.
- async 關(guān)鍵字: 標識方法為異步方法.
- await 關(guān)鍵字: 標識異步執(zhí)行方法, 等待返回.
- System.Threading.Tasks.Task: 異步任務(wù)類
客戶端實現(xiàn)
我們先來研究客戶端, 服務(wù)器的實現(xiàn)大致相同, 只是有細微的差別.
客戶端主流程和實現(xiàn)
// 構(gòu)建socket對象 Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // 連接遠端口, 用于向遠端發(fā)送消息, 這里是自己的機器同時當服務(wù)器和客戶端, 所以都是127... // 注意這里的連接只是將`socket`對象與ip和端口綁定, 不是tcp中的連接概念. // 內(nèi)部會分配新的本地端口, 發(fā)送給遠端, 供遠端使用 var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8060); udpSocket.Connect(endPoint); // 發(fā)送消息 SendMessageToServer("客戶端說:Hello Server!"); // 監(jiān)聽消息 StartRecvMessage(); Console.ReadKey();
客戶端發(fā)送消息實現(xiàn)
static void SendMessageToServer(string message) { udpSocket.Send(Encoding.UTF8.GetBytes(message)); }
因為之前已經(jīng)和遠端口綁定了, 所以客戶端可以直接發(fā)送消息, 在內(nèi)部會自動分配一個客戶端自己的本地端口, 服務(wù)器端使用這個端口來向本客戶端發(fā)送消息, 我們會在服務(wù)器實現(xiàn)中看到.
客戶端監(jiān)聽消息實現(xiàn)
// 從byte中轉(zhuǎn)換string static string ConverBytesToString(Decoder decoder, byte[] bytes, int len) { ? ? var nchar = decoder.GetCharCount(bytes, 0, len); ? ? var bytesChar = new char[nchar]; ? ? nchar = decoder.GetChars(bytes, 0, len, bytesChar, 0); ? ? var result = new string(bytesChar, 0, nchar); ? ? return result; } // 從連接的端口接收消息, 返回讀取到的字節(jié)數(shù) static int SocketRecvMessage() { ? ? var nread = udpSocket.Receive(buffer); ? ? return nread; } // 開始異步接收消息 static async void StartRecvMessage() { ? ? Console.WriteLine("客戶端開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? while (true) ? ? { ? ? ? ? var nread = await Task.Run<int>(SocketRecvMessage); ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? Console.WriteLine($"收到來自服務(wù)器的消息: {message}"); ? ? } }
上面的代碼中, 主要的部分是:
async/await/Task.Run<int>(xxx):
- async:標識方法StartRecvMessage將采用異步方式執(zhí)行
- await: 標識要等待的操作, 而這種操作是需要耗時的, 比如socket, io等, 也可以是單純就是要等待多久(Task.Delay(500); // 等待500ms).
- Task.Run<int>(xxx): 將耗時的操作包裝為異步任務(wù)(類似開了一個線程來執(zhí)行該操作).
udpSocket.Receive(buffer): 從連接好的遠端口接收消息, 這是一個阻斷性的操作, 在消息回來之前會停留在這里不動.
上面的異步還能寫成下面的形式, 只是將耗時操作推遲到了更具體的操作而已:
// 從連接的端口接收消息, 返回讀取到的字節(jié)數(shù) static async Task<int> SocketRecvMessage() { ? ? var nread = await Task.Run<int>(() => udpSocket.Receive(buffer)); ? ? return nread; } // 開始異步接收消息 static async void StartRecvMessage() { ? ? Console.WriteLine("客戶端開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? while (true) ? ? { ? ? ? ? var nread = await SocketRecvMessage(); ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? Console.WriteLine($"收到來自服務(wù)器的消息: {message}"); ? ? } }
我們還能進一步簡化代碼:
// 開始異步接收消息 static async void StartRecvMessage() { ? ? Console.WriteLine("客戶端開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? while (true) ? ? { ? ? ? ? var nread = await Task.Run<int>(() => udpSocket.Receive(buffer)); ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? Console.WriteLine($"收到來自服務(wù)器的消息: {message}"); ? ? } }
服務(wù)器實現(xiàn)
服務(wù)器和客戶端的實現(xiàn)差別很小.
主要區(qū)別在于服務(wù)器針對的是很多客戶端, 所以在收發(fā)消息上對于端口的處理不一樣.
服務(wù)器主流程和實現(xiàn)
// 構(gòu)建socket對象 Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // 綁定本地端口, 監(jiān)聽來自于各個客戶端的消息 var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8060); udpSocket.Bind(endPoint); // 監(jiān)聽消息 StartRecvMessage(); Console.ReadKey();
服務(wù)器發(fā)送消息實現(xiàn)
// 向指定的客戶端端口發(fā)送消息 // 注意這里和客戶端的實現(xiàn)不一樣, 還是因為服務(wù)器會對應(yīng)多個客戶端, 所以每次發(fā)送都需要指明目的地 static void SendMessageToClient(EndPoint endPoint, string message) { ? ? udpSocket.SendTo(Encoding.UTF8.GetBytes(message), endPoint); }
服務(wù)器監(jiān)聽消息實現(xiàn)
static (int, EndPoint) SocketRecvMessage() { ? ? EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); ? ? var nread = udpSocket.ReceiveFrom(buffer, ref endPoint); ? ? return (nread, endPoint); } static async void StartRecvMessage() { ? ? Console.WriteLine("服務(wù)器開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? while(true) ? ? { ? ? ? ? var (nread, endPoint) = await Task.Run<(int, EndPoint)>(SocketRecvMessage); ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? Console.WriteLine($"收到來自客戶端[{endPoint}]的消息: {message}"); ? ? ? ? SendMessageToClient(endPoint, "服務(wù)器對你說Hi!"); ? ? } }
上面的代碼中, 主要的差別在對于端口的處理上:
- SocketRecvMessage返回的是一個元組(int, EndPoint): 即讀取到的字節(jié)數(shù), 還有客戶端的端口信息.
- ReceiveFrom: 接收消息指定了端口, 服務(wù)器每次接收消息都要使用端口信息用來標識發(fā)送消息的客戶端.
優(yōu)化過后的代碼為:
static async void StartRecvMessage() { ? ? Console.WriteLine("服務(wù)器開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? while(true) ? ? { ? ? ? ? EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); ? ? ? ? var nread = await Task.Run<int>(() => udpSocket.ReceiveFrom(buffer, ref endPoint)); ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? Console.WriteLine($"收到來自客戶端[{endPoint}]的消息: {message}"); ? ? ? ? SendMessageToClient(endPoint, "服務(wù)器對你說Hi!"); ? ? } }
下面是完整的代碼:
// --- AsyncUdpClient.cs using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace test { ? ? class AsyncUdpClient ? ? { ? ? ? ? static Socket udpSocket; ? ? ? ? static byte[] buffer = new byte[4096]; ? ? ? ? public static void Main() ? ? ? ? { ? ? ? ? ? ? udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); ? ? ? ? ? ? var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8060); ? ? ? ? ? ? //udpSocket.Bind(endPoint); ? ? ? ? ? ? udpSocket.Connect(endPoint); ? ? ? ? ? ? SendMessageToServer("客戶端說:Hello Server!"); ? ? ? ? ? ? StartRecvMessage(); ? ? ? ? ? ? Console.ReadKey(); ? ? ? ? } ? ? ? ? static void SendMessageToServer(string message) ? ? ? ? { ? ? ? ? ? ? udpSocket.Send(Encoding.UTF8.GetBytes(message)); ? ? ? ? } ? ? ? ? static async void StartRecvMessage() ? ? ? ? { ? ? ? ? ? ? Console.WriteLine("客戶端開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? ? ? ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? ? ? ? ? while (true) ? ? ? ? ? ? { ? ? ? ? ? ? ? ? var nread = await Task.Run<int>(() => udpSocket.Receive(buffer)); ? ? ? ? ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? ? ? ? ? Console.WriteLine($"收到來自服務(wù)器的消息: {message}"); ? ? ? ? ? ? ? ? #region 交互 ? ? ? ? ? ? ? ? Console.WriteLine("是否繼續(xù)監(jiān)聽?[yes|no]"); ? ? ? ? ? ? ? ? var str = await Task.Run<string>(() => Console.ReadLine()); ? ? ? ? ? ? ? ? if (str == "yes") ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? Console.WriteLine("繼續(xù)監(jiān)聽..."); ? ? ? ? ? ? ? ? ? ? continue; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? Console.WriteLine("客戶端停止監(jiān)聽."); ? ? ? ? ? ? ? ? return; ? ? ? ? ? ? ? ? #endregion ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? static string ConverBytesToString(Decoder decoder, byte[] bytes, int len) ? ? ? ? { ? ? ? ? ? ? var nchar = decoder.GetCharCount(bytes, 0, len); ? ? ? ? ? ? var bytesChar = new char[nchar]; ? ? ? ? ? ? nchar = decoder.GetChars(bytes, 0, len, bytesChar, 0); ? ? ? ? ? ? var result = new string(bytesChar, 0, nchar); ? ? ? ? ? ? return result; ? ? ? ? } ? ? } } // --- AsyncUdpServer.cs using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace test { ? ? static class AsyncUdpServer ? ? { ? ? ? ? static Socket udpSocket; ? ? ? ? static byte[] buffer = new byte[4096]; ? ? ? ? public static void Main() ? ? ? ? { ? ? ? ? ? ? udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); ? ? ? ? ? ? var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8060); ? ? ? ? ? ? udpSocket.Bind(endPoint); ? ? ? ? ? ? //udpSocket.Connect(endPoint); ? ? ? ? ? ? StartRecvMessage(); ? ? ? ? ? ? Console.ReadKey(); ? ? ? ? } ? ? ? ? static void SendMessageToClient(EndPoint endPoint, string message) ? ? ? ? { ? ? ? ? ? ? udpSocket.SendTo(Encoding.UTF8.GetBytes(message), endPoint); ? ? ? ? } ? ? ? ? static async void StartRecvMessage() ? ? ? ? { ? ? ? ? ? ? Console.WriteLine("服務(wù)器開始監(jiān)聽: " + udpSocket.LocalEndPoint); ? ? ? ? ? ? var decoder8 = Encoding.UTF8.GetDecoder(); ? ? ? ? ? ? while(true) ? ? ? ? ? ? { ? ? ? ? ? ? ? ? EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); ? ? ? ? ? ? ? ? var nread = await Task.Run<int>(() => udpSocket.ReceiveFrom(buffer, ref endPoint)); ? ? ? ? ? ? ? ? var message = ConverBytesToString(decoder8, buffer, nread); ? ? ? ? ? ? ? ? Console.WriteLine($"收到來自客戶端[{endPoint}]的消息: {message}"); ? ? ? ? ? ? ? ? SendMessageToClient(endPoint, "服務(wù)器對你說Hi!"); #region 交互 ? ? ? ? ? ? ? ? Console.WriteLine("是否繼續(xù)監(jiān)聽?[yes|no]"); ? ? ? ? ? ? ? ? var str = await Task.Run<string>(()=> Console.ReadLine()); ? ? ? ? ? ? ? ? if (str == "yes") ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? Console.WriteLine("繼續(xù)監(jiān)聽..."); ? ? ? ? ? ? ? ? ? ? continue; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? Console.WriteLine("服務(wù)器停止監(jiān)聽."); ? ? ? ? ? ? ? ? return; #endregion ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? static string ConverBytesToString(Decoder decoder, byte[] bytes, int len) ? ? ? ? { ? ? ? ? ? ? var nchar = decoder.GetCharCount(bytes, 0, len); ? ? ? ? ? ? var bytesChar = new char[nchar]; ? ? ? ? ? ? nchar = decoder.GetChars(bytes, 0, len, bytesChar, 0); ? ? ? ? ? ? var result = new string(bytesChar, 0, nchar); ? ? ? ? ? ? return result; ? ? ? ? } ? ? } }
總結(jié)
今天我們使用aync/await關(guān)鍵字實現(xiàn)了異步的udp通訊.
主要是了解和實踐異步關(guān)鍵字的知識和使用, 同時對傳統(tǒng)的單開線程來進行udp通訊方式進行了優(yōu)化, 這
樣的好處是不需要自己維護多線程環(huán)境, 不需要保證線程安全, 各種鎖之類的操作.
udp通訊本身很簡單, 只要搞清楚Bind, Connect還有端口的概念即可.
aync/await對于長期寫同步代碼或者使用異步callback形式回調(diào)的同學(xué)來說, 可能會有一定的理解困難,
但是其實也就那么回事, 我們簡單理解為協(xié)程即可(只是比協(xié)程使用起來更方便).
到此這篇關(guān)于C#中使用async和await實現(xiàn)異步Udp通訊的示例代碼的文章就介紹到這了,更多相關(guān)C# 異步Udp通訊內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#中HttpWebRequest、WebClient、HttpClient的使用詳解
這篇文章主要介紹了C#中HttpWebRequest、WebClient、HttpClient的使用詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12C#動態(tài)創(chuàng)建Access數(shù)據(jù)庫及密碼的方法
同為微軟的產(chǎn)品,本文將討論的是C#如何創(chuàng)建Access數(shù)據(jù)庫,同時創(chuàng)建數(shù)據(jù)庫密碼與相關(guān)操作,希望對大家有所幫助。2015-09-09C#給picturebox控件加圖片選中狀態(tài)的2個方法
C#給picturebox控件加圖片選中狀態(tài)的2個方法,需要的朋友可以參考一下2013-03-03