Socket編程 (異步通訊,解決Udp丟包)
對於基於socket的udp協議通訊,丟包問題大家應該都見怪不怪了,但我們仍然希望在通訊方面使用Udp協議通訊,因為它即時,消耗資源少,響應迅速,靈活性強無需向Tcp那樣建立連接消耗很長的時間等等很有優勢的理由讓我們對Udp通訊寄予了厚望。但它也存在一個不好的特點,經常丟包是時常發生的事。可能各位大俠已經有了很好的解決方案,本人在這也只是本着大家共同學習的目的,提供自己的解決方式。
解決思路:模擬tcp三次握手協議,通過使用Timer定時器監視發送請求后接受數據的時間,如果一段時間內沒有接受到數據包則判定丟包,並重新發送本次請求。
下面我們將通過在客戶端向服務器端發送文件以及請求下載文件...將該例子是如何解決Udp丟包的問題。
個人優化:基於上章項目的Tcp協議下通訊,模擬聊天的同時發送文件。我們開辟兩個端口,一個基於Tcp協議下進行聊天通訊,一個基於Udp協議下傳送文件通訊。
項目大致思路如下:
發送文件:1.tcp客戶端(文件發送端)先在本端初始化Udp服務端,並綁定好相應的端口,並將Ip地址以及端口號發送到tcp服務器端(文件接收端)。
2.tcp服務端(文件接收端)接收到發送文件請求后,初始化Udp客戶端,並向指定的Udp服務端發送已准備完畢信息。
3.Udp服務器端接收到Udp客戶端已准備好的信息后,初始化要發送文件對象,獲取文件的基本參數(文件名、文件大小、數據大小、數據包總數)。
4.Udp客戶端收到文件的基本參數后想Udp服務端發送開始發送文件,以及此時接受到文件之后的偏移量,告訴Udp服務端應該從文件的哪個部分發送數據。此時啟動Timer定時器。
5.Udp服務器端收到文件繼續發送的請求后,設置文件的偏移量,然后發送對應的數據包。此時關閉Timer定時器。
6.循環4-5過程直到文件發送完畢后,Udp客戶端向Udp服務端發送文件接受完畢消息,並與3秒后關閉Udp客戶端套接字。
7.Udp接受到文件接受完畢的消息后,關閉該套接字。
8.Udp客戶端一段時間沒接受到消息則出發Timer的定時觸發事件,重新發送本次請求。
接收文件:1.tcp客戶端(文件發送端)先在本端初始化Udp服務端,並綁定好相應的端口,並將Ip地址以及端口號發送到tcp服務器端(文件接收端)。
2.tcp服務端(文件接收端)接收到發送文件請求后,初始化Udp客戶端,並根據文件名初始化要發送文件對象,獲取文件的基本參數(文件名、文件大小、數據大小、數據包總數),並將此信息轉成協議信息發送到Udp服務端。
后面的過程與上類似,我就不再馬字了......
下面的過程主要針對發送文件進行講解:
對於文件信息以及各種其他確認請求通過自定義協議發送后並解析的情況,在上一章都已經展示給大家看了。
Udp服務端以及Udp客戶端繼承UdpCommon公用抽象類,主要存放兩端共同所需對象及方法:
/// <summary> /// Udp異步通訊公有類 /// 實現server與client相同方法 /// </summary> public abstract class UdpCommon : IDisposable { #region 全局成員 protected Socket worksocket;//當前套接字 protected EndPoint sendEP;//發送端remote protected EndPoint reciveEP;//接受端remote protected int buffersize = 1024;//數據緩沖區大小 protected RequestFile requestFile;//文件請求 protected FileSendManager sendmanager;//文件發送管理 protected FileReciveManager recivemanager;//文件接收管理 protected HandlerMessage handlerMsg = new HandlerMessage();//消息協議處理類 protected delegate void ReciveCallbackDelegate(int length, byte[] buffer); protected event ReciveCallbackDelegate ReciveCallbackEvent;//消息接收處理事件 protected delegate void TimeOutHandlerDelegate(MutualMode mode, byte[] buffer); protected event TimeOutHandlerDelegate TimeOutHandlerEvent;//超時處理 #endregion #region 構造函數 /// <summary> /// 構造函數 /// </summary> /// <param name="_requestFile">請求信息</param> public UdpCommon(RequestFile _requestFile) { requestFile = _requestFile; } #endregion #region 抽象方法 /// <summary> /// 接受buffer處理 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> public abstract void ReciveBufferHandler(int length, byte[] buffer); #endregion #region 異步接受/發送buffer /// <summary> /// 異步接受buffer /// </summary> protected void AsynRecive() { byte[] buffer = new byte[buffersize]; worksocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref reciveEP, asyncResult => { if (asyncResult.IsCompleted)//信息接收完成 { int length = worksocket.EndReceiveFrom(asyncResult, ref sendEP); if (TimeOutHandlerEvent != null) { TimeOutHandlerEvent.Invoke(MutualMode.recive, null); } ReciveCallbackEvent.Invoke(length, buffer); } }, null); } /// <summary> /// 異步發送buffer /// </summary> /// <param name="buffer"></param> protected void AsynSend(byte[] buffer) { worksocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, sendEP, asyncResult => { if (asyncResult.IsCompleted)//消息發送完成 { worksocket.EndSendTo(asyncResult); if (TimeOutHandlerEvent != null) { TimeOutHandlerEvent.Invoke(MutualMode.send, buffer); } } }, null); } #endregion }
Udp服務端初始化Socket套接字,並初始化相應的消息返回處理委托事件:
using System; using System.Collections.Generic; using System.Text; #region 命名空間 using System.Net; using System.Net.Sockets; using System.IO; using SocketCommon; #endregion namespace ClientConsole { public class UdpServer : UdpCommon { public UdpServer(RequestFile _requestFile) : base(_requestFile) { //初始化套接字 IPEndPoint ipep = new IPEndPoint(IPAddress.Parse("192.168.1.108"), 0); base.worksocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); base.worksocket.Bind(ipep); base.reciveEP = base.sendEP = (EndPoint)(new IPEndPoint(IPAddress.Any, 0)); //返回Ip地址及端口信息 IPEndPoint localEP = (IPEndPoint)base.worksocket.LocalEndPoint; requestFile.Address = localEP.Address.ToString(); requestFile.Port = localEP.Port; //根據不同的請求初始化相應的事件 if (requestFile.Mode == RequestMode.send)//發送准備 { base.ReciveCallbackEvent += new ReciveCallbackDelegate(ReadySendBuffer); } else //接受准備 { base.ReciveCallbackEvent += new ReciveCallbackDelegate(ReadyReciveBuffer); } base.AsynRecive(); } /// <summary> /// 准備發送文件信息 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> public void ReadySendBuffer(int length, byte[] buffer) { MessageProtocol msgPro = handlerMsg.HandlerObject(Encoding.UTF8.GetString(buffer, 0, length)); if (msgPro.MessageType == MessageType.text && msgPro.MessageInfo.Content == "ready") { ReciveCallbackEvent -= new ReciveCallbackDelegate(ReadySendBuffer); ReciveCallbackEvent += new ReciveCallbackDelegate(ReciveBufferHandler); if (sendmanager == null) { sendmanager = new FileSendManager(requestFile.FileObject); Console.WriteLine("發送文件:{0}", sendmanager.fileobject.FileName); } msgPro = new MessageProtocol(RequestMode.send, sendmanager.fileobject); AsynSend(msgPro.ToBytes()); AsynRecive(); } } /// <summary> /// 接受下一個包請求或者發送下一個數據包 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> public override void ReciveBufferHandler(int length, byte[] buffer) { if (requestFile.Mode == RequestMode.send) //發送文件 { SendFile(length, buffer); } else//寫入文件 { ReciveFile(length, buffer); } } } }
因為兩端都存在SendFile以及ReciveFile的方法,我們在UdpCommon實現兩個方法方便兩端共同調用。
#region 准備發送/接收文件 /// <summary> /// 准備接收 /// </summary> protected void ReciveReady() { recivemanager = new FileReciveManager(requestFile.FileObject); Console.WriteLine("接受文件:{0}", recivemanager.fileobject.FileName); RequestAction(); } /// <summary> /// 准備接收文件Buffer /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> protected void ReadyReciveBuffer(int length, byte[] buffer) { MessageProtocol msgPro = handlerMsg.HandlerObject(Encoding.UTF8.GetString(buffer, 0, length)); ReciveCallbackEvent -= new ReciveCallbackDelegate(ReadyReciveBuffer); ReciveCallbackEvent += new ReciveCallbackDelegate(ReciveBufferHandler); requestFile = msgPro.RequestFile; ReciveReady(); } #endregion #region 發送/接收文件 /// <summary> /// 發送文件 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> protected void SendFile(int length, byte[] buffer) { string protocol = Encoding.UTF8.GetString(buffer, 0, length); MessageProtocol msgPro = handlerMsg.HandlerObject(protocol); if (msgPro.MessageType == MessageType.text) { string msg = msgPro.MessageInfo.Content; string[] strArr = msg.Split('|'); Status status = (Status)Enum.Parse(typeof(Status), strArr[0]); if (status == Status.keepon) { sendmanager.offset = Convert.ToInt32(strArr[1]);//文件偏移量 AsynSend(sendmanager.Read());//發送下一個包 Console.WriteLine("已發送:{0}%", sendmanager.GetPercent()); AsynRecive(); } else Dispose(); } } /// <summary> /// 接受文件 /// 1.未接收完之前將文件保存為臨時文件 /// 2.完畢后通過moveTo重命名 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> protected void ReciveFile(int length, byte[] buffer) { recivemanager.Write(buffer); Console.WriteLine("已接受:{0}%", recivemanager.GetPercent()); RequestAction(); } #endregion #region 發送請求下一個包 /// <summary> /// 發送請求下一個包 /// </summary> public void RequestAction() { //根據狀態處理 MessageProtocol msgPro = new MessageProtocol( String.Format("{0}|{1}", (int)recivemanager.status, recivemanager.offset)); if (recivemanager.status == Status.keepon) { AsynSend(msgPro.ToBytes()); long tempsize = recivemanager.fileobject.FileLength - recivemanager.offset; if (tempsize < recivemanager.fileobject.PacketSize) { buffersize = Convert.ToInt32(tempsize); } else buffersize = recivemanager.fileobject.PacketSize; AsynRecive(); } else { TimeOutHandlerEvent = null; AsynSend(msgPro.ToBytes()); } } #endregion #region 釋放資源 /// <summary> /// 釋放資源 /// </summary> public void Dispose() { if (worksocket != null) { Thread.Sleep(3); worksocket.Close(3); } } #endregion
下面我來看看Udp客戶端是怎樣工作的......
using System; using System.Collections.Generic; using System.Text; #region 命名空間 using System.Net; using System.Net.Sockets; using System.IO; using SocketCommon; using System.Timers; #endregion namespace ServerConsole { public class UdpClient : UdpCommon { /// <summary> /// 定時器輔助類 /// </summary> TimerManager timermanager;//定時(用於超時重發) //當前發送請求數據緩存 byte[] temp_buffer; public UdpClient(RequestFile _requestFile) : base(_requestFile) { //初始化套接字 base.worksocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); base.sendEP = new IPEndPoint(IPAddress.Parse(_requestFile.Address), requestFile.Port); base.reciveEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); //根據不同的請求返回對應的消息 MessageProtocol msgPro; if (_requestFile.Mode == RequestMode.send)//接受准備 { base.ReciveCallbackEvent += new ReciveCallbackDelegate(ReadyReciveBuffer); msgPro = new MessageProtocol("ready"); } else //發送准備 { base.ReciveCallbackEvent += new ReciveCallbackDelegate(ReciveBufferHandler); sendmanager = new FileSendManager(requestFile.FileObject); msgPro = new MessageProtocol(RequestMode.recive, sendmanager.fileobject); Console.WriteLine("發送文件:{0}", sendmanager.fileobject.FileName); } //初始化定時器 timermanager = new TimerManager(3000); timermanager.ElapsedEvent += new TimerManager.ElapsedDelegate(ElapsedEvent); base.TimeOutHandlerEvent += new TimeOutHandlerDelegate(TimeOutHandler); base.AsynSend(msgPro.ToBytes()); base.AsynRecive(); } /// <summary> /// 請求下一個數據包或者發送下一個數據包 /// </summary> /// <param name="length"></param> /// <param name="buffer"></param> public override void ReciveBufferHandler(int length, byte[] buffer) { if (requestFile.Mode == RequestMode.send)//接受來自客戶端的文件 { ReciveFile(length, buffer); } else //向客戶端發送文件 { SendFile(length, buffer); } } private void TimeOutHandler(MutualMode mode, byte[] buffer) { this.temp_buffer = buffer; if (mode == MutualMode.send) { if (!timermanager.IsRuning) { timermanager.Start(); } } else { if (timermanager.IsRuning) { timermanager.Stop(); } } } /// <summary> /// 超時后重發當前請求 /// </summary> private void ElapsedEvent() { if (temp_buffer != null) { Console.WriteLine("發生丟包,重新請求..."); base.AsynSend(temp_buffer); } } } }
我們來看看運行效果:
發送文件:
接收文件:
由此對於文件使用Udp傳送的過程我們完成了,至於在上面的測試過程中丟包問題,我在同學的電腦上測試過丟包問題,由於不方便截圖,就不放到上面了,上面測試過程中如果出現丟包會重新發送本次請求,並接受數據。
附上源碼:SocketProQuests.zip
作者:曾慶雷
出處:http://www.cnblogs.com/zengqinglei
本頁版權歸作者和博客園所有,歡迎轉載,但未經作者同意必須保留此段聲明, 且在文章頁面明顯位置給出原文鏈接,否則保留追究法律責任的權利