封裝Socket.BeginReceive/EndReceive支援Timeout簡介

liuxixi發表於2016-06-24

.NET中的Socket類提供了網路通訊常用的方法,分別提供了同步和非同步兩個版本,其中非同步的實現是基於APM非同步模式實現,即BeginXXX/EndXXX的方式。非同步方法由於其非阻塞的特性,在需考慮程式效能和伸縮性的情況下,一般會選擇使用非同步方法。但使用過Socket提供的非同步方法的同學,應該都會注意到了Socket的非同步方法是無法設定Timeout的。以Receive操作為例,Socket提供了一個ReceiveTimeout屬性,但該屬性設定的是同步版本的Socket.Receive()方法的Timeout值,該設定對非同步的Socket.BeginReceive()無效:如果對方沒有返回任何訊息,則BeginReceive操作將無法完成,其中提供的回撥函式也將不會呼叫。如下示例程式碼所示:

private static void TestSocketBeginReceive()


{


    Socket socket = new Socket(AddressFamily.InterNetwork,


        SocketType.Dgram, ProtocolType.Udp);


    byte[] content = Encoding.ASCII.GetBytes("Hello world");




    IPAddress ip = Dns.Resolve("www.google.com").AddressList[0];


    IPEndPoint receiver = new IPEndPoint(ip, 80);




    socket.BeginSendTo(content, 0, content.Length, SocketFlags.None,


        receiver, SendToCb, socket);


    Console.WriteLine("Sent bytes: " + content.Length);


}




private static void SendToCb(IAsyncResult ar)


{


    var socket = ar.AsyncState as Socket;


    socket.EndSendTo(ar);


    byte[] buffer = new byte[1024];




    IAsyncResult receiveAr = socket.BeginReceive(buffer, 0, buffer.Length,


        SocketFlags.None, null, null);


    int received = socket.EndReceive(receiveAr);


    Console.WriteLine("Received bytes: " + received);


}

由於接收方不會返回任何訊息,Socket.BeginReceive將永遠不會完成,SentToCb方法中的socket.EndReceive()呼叫將永遠阻塞,應用程式也無法得知操作的狀態。

支援Timeout

在個別的應用場景下,我們希望既能使用Socket的非同步通訊方法,保證程式的效能,同時又希望能指定Timeout值,當操作沒有在指定的時間內完成時,應用程式能得到通知,以進行下一步的操作,如retry等。以下介紹的就是一種支援Timeout的Socket非同步Receive操作的實現,方式如下:

1.基於APM非同步模式封裝Socket.BeginReceive/EndReceive方法。

2.使用ThreadPool提供的RegisterWaitForSingleObject()方法註冊一個WaitOrTimerCallback,如果指定時間內操作未完成,則結束操作,並設定狀態為Timeout。

3.將上述封裝實現為Socket的擴充套件方法方便呼叫。

以下程式碼簡化了所有的引數檢查和異常處理,實際使用中需新增相關邏輯。

AsyncResultWithTimeout

首先看一下IAsyncResult介面的實現:

public class AsyncResultWithTimeout : IAsyncResult


{


    private ManualResetEvent m_waitHandle = new ManualResetEvent(false);


    public AsyncResultWithTimeout(AsyncCallback cb, object state)


    {


        this.AsyncState = state;


        this.Callback = cb;


    }




    #region IAsyncResult




    public object AsyncState { get; private set; }


    public WaitHandle AsyncWaitHandle { get { return m_waitHandle; } }


    public bool CompletedSynchronously { get { return false; } }


    public bool IsCompleted { get; private set; }




    #endregion




    public AsyncCallback Callback { get; private set; }


    public int ReceivedCount { get; private set; }


    public bool TimedOut { get; private set; }


    public void SetResult(int count)


    {


        this.IsCompleted = true;


        this.ReceivedCount = count;


        this.m_waitHandle.Set();




        if (Callback != null) Callback(this);


    }




    public void SetTimeout()


    {


        this.TimedOut = true;


        this.IsCompleted = true;


        this.m_waitHandle.Set();


    }


}

AsyncResultWithTimeOut類中包含了IAsyncResult介面中4個屬性的實現、使用者傳入的AsyncCallback委託、接收到的位元組數ReceivedCount以及兩個額外的方法:

1.SetResult(): 用於正常接收到訊息時設定結果,標記操作完成以及執行回撥。

2.SetTimeout():當超時時,標記操作完成以及設定超時狀態。

StateInfo

StateInfo類用於儲存相關的狀態資訊,該物件會作為Socket.BeginReceive()的最後一個引數傳入。當接收到訊息時,接收到的位元組數會儲存到AsyncResult屬性中,並設定操作完成。當超時時,WatchTimeOut方法會將AsyncResult設定為TimeOut狀態,並通過RegisteredWaitHandle屬性取消註冊的WaitOrTimerCallback.

public class StateInfo


{


    public StateInfo(AsyncResultWithTimeout result, Socket socket)


    {


        this.AsycResult = result;


        this.Socket = socket;


    }




    public Socket Socket { get; private set; }


    public AsyncResultWithTimeout AsycResult { get; private set; }


    public RegisteredWaitHandle RegisteredWaitHandle { get; set; }


}

封裝Socket.BeginReceive

與Socket.BeginReceive方法相比,BeginReceive2新增了一個引數timeout,可以設定該操作的超時時間,單位為毫秒。BeginReceive2中呼叫Socket.BeginReceive()方法,其中指定的ReceiveCb回撥將在正常接收到訊息後將結果儲存在stateInfo物件的AsyncResult屬性中,該屬性中的值就是BeginReceive2()方法返回的IAsyncResult。BeginReceive2呼叫Socket.BeginReceive後,在ThreadPool中註冊了一個WaitOrTimerCallback委託。ThreadPool將在Receive操作完成或者Timeout時呼叫該委託。

public static class SocketExtension


{




    public static int EndReceive2(IAsyncResult ar)


    {


        var result = ar as AsyncResultWithTimeout;


        result.AsyncWaitHandle.WaitOne();




        return result.ReceivedCount;


    }




    public static AsyncResultWithTimeout BeginReceive2


    (


        this Socket socket,


        int timeout,


        byte[] buffer,


        int offset,


        int size,


        SocketFlags flags,


        AsyncCallback callback,


        object state


    )


    {


        var result = new AsyncResultWithTimeout(callback, state);




        var stateInfo = new StateInfo(result, socket);




        socket.BeginReceive(buffer, offset, size, flags, ReceiveCb, state);




        var registeredWaitHandle =


            ThreadPool.RegisterWaitForSingleObject(


                result.AsyncWaitHandle,


                WatchTimeOut,


                stateInfo, // 作為state傳遞給WatchTimeOut


                timeout,


                true);




        // stateInfo中儲存RegisteredWaitHandle,以方便在úWatchTimeOut


        // 中unregister.


        stateInfo.RegisteredWaitHandle = registeredWaitHandle;




        return result;


    }




    private static void WatchTimeOut(object state, bool timeout)


    {


        var stateInfo = state as StateInfo;


        // 設定的timeout前,操作未完成,則設定為操作Timeout


        if (timeout)


        {


            stateInfo.AsycResult.SetTimeout();


        }




        // 取消之前註冊的WaitOrTimerCallback


        stateInfo.RegisteredWaitHandle.Unregister(


            stateInfo.AsycResult.AsyncWaitHandle);


    }




    private static void ReceiveCb(IAsyncResult result)


    {


        var state = result.AsyncState as StateInfo;


        var asyncResultWithTimeOut = state.AsycResult;


        var count = state.Socket.EndReceive(result);


        state.AsycResult.SetResult(count);


    }


}

試一下

以下程式碼演示瞭如何使用BeginReceive2:

private static void TestSocketBeginReceive2()


{


    Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);


    byte[] content = Encoding.ASCII.GetBytes("Hello world");




    IPAddress ip = Dns.Resolve("www.google.com").AddressList[0];


    IPEndPoint receiver = new IPEndPoint(ip, 80);




    socket.BeginSendTo(content, 0, content.Length, SocketFlags.None, receiver, SendToCb2, socket);


    Console.WriteLine("Sent bytes: " + content.Length);


}




private static void SendToCb2(IAsyncResult ar)


{


    var socket = ar.AsyncState as Socket;


    socket.EndSendTo(ar);


    byte[] buffer = new byte[1024];




    AsyncResultWithTimeout receiveAr = socket.BeginReceive2(2000, buffer, 0, buffer.Length, SocketFlags.None, null, null);


    receiveAr.AsyncWaitHandle.WaitOne();


    if (receiveAr.TimedOut)


    {


        Console.WriteLine("Operation timed out.");


    }


    else


    {


        int received = socket.EndReceive(ar);


        Console.WriteLine("Received bytes: " + received);


    }


}

輸出結果如下:

                       

 

上述實現是針對BeginReceive的封裝,還可以以相同的方式將Send/Receive封裝以支援Timeout, 或者更進一步支援retry操作。

附示例程式碼:files.cnblogs.com/dytes/SocketAsyncOpWithTimeOut.zip

本文轉自:http://www.csharpwin.com/csharpspace/13263r8436.shtml

 

相關文章