您现在的位置: 365建站网 > 365学习 > C#中Socket.Receive()的超时阻塞问题

C#中Socket.Receive()的超时阻塞问题

文章来源:365jz.com     点击数:371    更新时间:2018-06-08 22:38   参与评论

Socket.Receive 方法 (Byte(), Int32, Int32, SocketFlags)

使用指定的 SocketFlags,从绑定的 Socket 接收指定的字节数,存入接收缓冲区的指定偏移量位置。

public int Receive(  
    byte[] buffer,  
    int offset,  
    int size,  
    SocketFlags socketFlags  
)

参数
buffer
类型:System.Byte()
Byte 类型的数组,它是存储接收到的数据的位置。
offset
类型:System.Int32
buffer 中存储所接收数据的位置。
size
类型:System.Int32
要接收的字节数。
socketFlags
类型:System.Net.Sockets.SocketFlags
SocketFlags 值的按位组合。
返回值
类型:System.Int32
接收到的字节数。

(以上部分来自MSDN)
接下来我的经验总结:

1,Socket实际接收到的字节数与预期想要接收字节数

我们从上面的返回值可以清晰的知道,虽然我们给Buffer定义了一个长度和要接收到的size,但是Socket接收到的字节数(即返回值)并不一定等于Size

所以我们在做这个的时候,需要判断,实际接收到的字节数是否等于要接收的字节数!这个很重要。

//定义接收数据的缓存  
byte[] body = new byte[1024];  
//第一次接收的实际数据 flag  
int flag = socket.Receive(body, 0, body.Length, SocketFlags.None);  
//如果没有接收到定长的数据,循环接收  
while(flag != body.Length)  
{  
   flag += socket.Receive(body, flag, body.Length - flag, SocketFlags.None);  
}

因此,Socket接收数据的过程应该是这样的!

2,服务器发送多次,客户端一次接收的问题。

当然在实际的编程中很少这么干的,但是我这么写是想说明一个问题:假如客户端想一次接收服务器多次发送过来的数据,客户端定义缓存body的长度等于服务端多次发送的数据长度和,客户端在服务器发送完成后开始一次接收,客户端肯定能接收到服务器发送过来的所有数据,不会只接收服务器发送过来的部分数据。

我的叙述很烂!打个比方,假如服务器分三次分别发送“Hell”,“0”,“World”。这样客户端一定能接受到“HelloWorld”。而不是“Hell”或者其他什么的东西。

最近在调试程序的时候,希望1秒钟调用一次 Socket.Receive() 来接收数据。 
实际上,应该是说,如果没有数据到来,就是1秒钟一次,如果有数据到来,则收到数据后立即继续接收,然后继续是1秒钟接收一次。

C#的相关代码如下:

public bool ReadDataForMe(Socket sck, uint waitSec)
{    int rlen = 1;
    DateTime ttStart = DateTime.Now;    //sck.ReceiveTimeout = 5;
    sck.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, -300);    while (true)
    {      try
      {
        PrintLog("start read socket.\n");        if (BaseTools.GetEclpseTime(ttStart, 0.4) >= waitSec)
        {          break;
        }
        rlen = sck.Receive(m_bufRecv, 0, 1000, SocketFlags.None);        if (rlen == 0)
        {
           BaseTools.Sleep(100);           continue;
        }        return true;
      }      catch (IOException)
      {          // 读取失败,原因:基础 System.Net.Sockets.Socket 被关闭。
          return false;
      }      catch (SocketException e)
      {          if (e.ErrorCode == 10060)              // 超时的时候错误号码是10060
              continue;          break;
      }      catch (Exception e)
      {          break;
      }
    }return false;
}// 如下调用while(true)
{   if (ReadDataForMe(mySock, 1))      break;
}

其中BaseTools为一个基本工具库,自己封装的,这里用到了两个功能,一个是Sleep(),用于休息多长时间,另外一个是 GetEclpseTime() 用于获取指定的一个时间到当前时间的秒数,这样用于计算时间差。 
另外还有一个函数 PrintLog() 用于在控制台打印字符串,其中添加了时间信息,代码如下:

public static void PrintLog(string s)
{
   Console.WriteLine($"[{ DateTime.Now.ToString("HH:mm:ss.fff")}] {s}");
}

在每一次打印都添加了时间,精确到毫秒,这样方便对照打印时的时间。 
测试方法就是连上服务器之后,服务器不发送数据,此客户端一直调用Receive()接口来收取数据。在每一次接收数据之前都加一行打印:start read socket.

以上为测试的环境情况。 
下面测试主要是对函数开始的前几行的socket超时设置以及对超时时间的观察。 
如上面的代码那样,设置超时时间为 -300,则打印间隔为500ms 
如果改成3,则打印间隔为503ms


前几日碰到一问题,当CSocket的Receive阻塞时,如何进行超时处理。由于程序是在多线程中使用Socket通信,开始时是在主线程中用定时监测Receive函数,当超时后,结束通信。但问题是CSocket对象无法释放。因此从网上搜索解决办法,直接在线程中对Receive进行超时处理。

不错,搜到以下内容,很多网站转载。

 

为CSocket配置Time-Out功能

    CSocket操作,如Send(),Receive(),Connect()都属阻塞操作,即它们在成功完成或错误发生之前是不会返回的。
    在某些情况下,某项操作可能永远不能成功完成,程序为了等待其完成就得永远循环下去。在程序中为某项操作限定一个成功完成的时间是个好主意。本文就是讨论此问题的。
    一个办法是设计一个计时器,当操作费时过长时就触发。这个办法的关键是怎样处理计时器。虽然操作是"阻塞"的,但仍具处理传回的消息的能力。如果用SetTimer来设置计时器,就可截获WM_TIMER消息,当它产生时就终止操作。涉及到这个过程的主要函数是:Windows API ::SetTimer(),MFC函数CSocket::OnMessagePending()和CSocket:: CancelBlockingCall()。这些功能可包装到你的CSocket类中得以简化。
    类中用到三个重要函数:
    BOOL SetTimeOut(UINT uTimeOut) 它应在CSocket函数调用前被调用。uTimeOut以千分秒为单位。下面的实现只是简单的设置计时器。当设置计时器失败时返回False。参见Windows API中关于SetTimer的说明。
    BOOL KillTimeOut() 此函数应在操作未完成被阻塞时被调用。它删除SetTimeOut所设置的计时器。如果调用KillTimer失败则返回False。参见Windows API中关于KillTimer的说明。
    BOOL OnMessagePending() 它是一个虚拟回调函数,当等待操作完成时被CSocket类调用。它给你机会来处理传回的消息。这次我们用它来检查SetTimeOut所设置的计时器,如果超时(Time-Out),则它调用CancelBlockingCall()。参见MFC文档关于OnMessagePending()和CancelBlockingCall()的说明。注意调用CancelBlockingCall()将使当前操作失败,GetLastError()函数返回WSAEINTR(指出是中断操作)。
    下面就是使用这个类的例子:   ...

  CTimeOutSocket sockServer;
   CAcceptedSocket sockAccept;
   sockServer.Create(777);
   sockServer.Listen();
   // Note the following sequence:
   //  SetTimeOut
   //  <operation which might block>
   //  KillTimeOut
   if(!sockServer.SetTimeOut(10000))
   {
     ASSERT(FALSE);
     // Error Handling...for some reason, we could not setup
     // the timer.
   }
   if(!sockServer.Accept(sockAccept))
   {
     int nError = GetLastError();
     if(nError==WSAEINTR)
       AfxMessageBox("No Connections Arrived For 10 Seconds");
      else
        ; // Do other error processing.
   }
   if(!sockServer.KillTimeOut())
   {
     ASSERT(FALSE);
     // Error Handling...for some reason the timer could not
     // be destroyed...perhaps a memory overwrite has changed
     // m_nTimerID?
     // 
   }
   ...

    下面是示例代码:

  // 
   // HEADER FILE
   // 
   class CTimeOutSocket : public CSocket
   {
   public:
     BOOL SetTimeOut(UINT uTimeOut);
     BOOL KillTimeOut();
   protected:
     virtual BOOL OnMessagePending();
   private:
     int m_nTimerID;
   };
   // 
   // END OF FILE
   // 
   // 
   // IMPLEMENTATION FILE
   // 
   BOOL CTimeOutSocket::OnMessagePending()
   {
     MSG msg;
     if(::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE))
     {
       if (msg.wParam == (UINT) m_nTimerID)
       {
         // Remove the message and call CancelBlockingCall.
         ::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_REMOVE);
         CancelBlockingCall();
         return FALSE;  // No need for idle time processing.
       };
     };
     return CSocket::OnMessagePending();
   }
   BOOL CTimeOutSocket::SetTimeOut(UINT uTimeOut)
   {
     m_nTimerID = SetTimer(NULL,0,uTimeOut,NULL);
     return m_nTimerID;
   }
   BOOL CTimeOutSocket::KillTimeOut()
   {
     return KillTimer(NULL,m_nTimerID);
   }


我按照以上方式建类并完成代码。运行测试后发现并没有实现效果!OnMessagePending没有监测到WM_TIMER消息。

然后我对类进行了调整,一来使得封装更好,二来外部调用基本不用做任何变化,三来希望能使OnMessagePending能够起作用。

其实修改很简单,就是将SetTimeOut和KillTimeOut都修改为私有函数,并重载CSocket基类的Receive和Send函数,在收发前后启动和关闭定时器。

class CTimeOutSocket : public CSocket{// Attributespublic:
// Operationspublic: CTimeOutSocket(); virtual ~CTimeOutSocket();
// Overridespublic: // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CTimeOutSocket) public: virtual BOOL OnMessagePending(); virtual int Receive(void* lpBuf, int nBufLen, int nFlags = 0); virtual int Send(const void* lpBuf, int nBufLen, int nFlags = 0); //}}AFX_VIRTUAL
 // Generated message map functions //{{AFX_MSG(CTimeOutSocket)  // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG
// Implementationprotected: int m_nTimerID;private: BOOL KillTimeOut(); BOOL SetTimeOut(int nTimeOut);};
BOOL CTimeOutSocket::OnMessagePending() { MSG msg; if(::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE)) {  if (msg.wParam == (UINT) m_nTimerID)  {   // Remove the message and call CancelBlockingCall.   ::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_REMOVE);   CancelBlockingCall();   return FALSE;  // No need for idle time processing.  }; }; return CSocket::OnMessagePending();}
int CTimeOutSocket::Receive(void* lpBuf, int nBufLen, int nFlags) { SetTimeOut(10000); int nRecv = CSocket::Receive(lpBuf, nBufLen, nFlags); KillTimeOut(); return nRecv;}
int CTimeOutSocket::Send(const void* lpBuf, int nBufLen, int nFlags) { SetTimeOut(10000); int nSend = CSocket::Send(lpBuf, nBufLen, nFlags); KillTimeOut(); return nSend; }
BOOL CTimeOutSocket::SetTimeOut(int nTimeOut){ m_nTimerID = SetTimer(NULL,0,nTimeOut,NULL); return m_nTimerID;}
BOOL CTimeOutSocket::KillTimeOut(){ return KillTimer(NULL,m_nTimerID);}

经过修改的代码,运行后OnMessagePending得到了WM_TIMER消息,正确解决了Receive的阻塞问题,同时我只需将原来的CSocket对象改为CTimeOutSocket对象就可以了,其它代码不用变化。


如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛


发表评论 (371人查看0条评论)
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
用户名: 验证码: 点击我更换图片
最新评论
------分隔线----------------------------