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

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

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

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

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

</>code

  1. public int Receive(  
  2.     byte[] buffer,  
  3.     int offset,  
  4.     int size,  
  5.     SocketFlags socketFlags  
  6. )

参数
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

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

</>code

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

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

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

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

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

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

C#的相关代码如下:

</>code

  1. public bool ReadDataForMe(Socket sck, uint waitSec)
  2. {    int rlen = 1;
  3.     DateTime ttStart = DateTime.Now;    //sck.ReceiveTimeout = 5;
  4.     sck.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, -300);    while (true)
  5.     {      try
  6.       {
  7.         PrintLog("start read socket.\n");        if (BaseTools.GetEclpseTime(ttStart, 0.4) >= waitSec)
  8.         {          break;
  9.         }
  10.         rlen = sck.Receive(m_bufRecv, 0, 1000, SocketFlags.None);        if (rlen == 0)
  11.         {
  12.            BaseTools.Sleep(100);           continue;
  13.         }        return true;
  14.       }      catch (IOException)
  15.       {          // 读取失败,原因:基础 System.Net.Sockets.Socket 被关闭。
  16.           return false;
  17.       }      catch (SocketException e)
  18.       {          if (e.ErrorCode == 10060)              // 超时的时候错误号码是10060
  19.               continue;          break;
  20.       }      catch (Exception e)
  21.       {          break;
  22.       }
  23.     }return false;
  24. }// 如下调用while(true)
  25. {   if (ReadDataForMe(mySock, 1))      break;
  26. }

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

</>code

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

在每一次打印都添加了时间,精确到毫秒,这样方便对照打印时的时间。 
测试方法就是连上服务器之后,服务器不发送数据,此客户端一直调用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(指出是中断操作)。
    下面就是使用这个类的例子:   ...

</>code

  1.   CTimeOutSocket sockServer;
  2.    CAcceptedSocket sockAccept;
  3.    sockServer.Create(777);
  4.    sockServer.Listen();
  5.    // Note the following sequence:
  6.    //  SetTimeOut
  7.    //  <operation which might block>
  8.    //  KillTimeOut
  9.    if(!sockServer.SetTimeOut(10000))
  10.    {
  11.      ASSERT(FALSE);
  12.      // Error Handling...for some reason, we could not setup
  13.      // the timer.
  14.    }
  15.    if(!sockServer.Accept(sockAccept))
  16.    {
  17.      int nError = GetLastError();
  18.      if(nError==WSAEINTR)
  19.        AfxMessageBox("No Connections Arrived For 10 Seconds");
  20.       else
  21.         ; // Do other error processing.
  22.    }
  23.    if(!sockServer.KillTimeOut())
  24.    {
  25.      ASSERT(FALSE);
  26.      // Error Handling...for some reason the timer could not
  27.      // be destroyed...perhaps a memory overwrite has changed
  28.      // m_nTimerID?
  29.      // 
  30.    }
  31.    ...

    下面是示例代码:

</>code

  1.   // 
  2.    // HEADER FILE
  3.    // 
  4.    class CTimeOutSocket : public CSocket
  5.    {
  6.    public:
  7.      BOOL SetTimeOut(UINT uTimeOut);
  8.      BOOL KillTimeOut();
  9.    protected:
  10.      virtual BOOL OnMessagePending();
  11.    private:
  12.      int m_nTimerID;
  13.    };
  14.    // 
  15.    // END OF FILE
  16.    // 
  17.    // 
  18.    // IMPLEMENTATION FILE
  19.    // 
  20.    BOOL CTimeOutSocket::OnMessagePending()
  21.    {
  22.      MSG msg;
  23.      if(::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE))
  24.      {
  25.        if (msg.wParam == (UINT) m_nTimerID)
  26.        {
  27.          // Remove the message and call CancelBlockingCall.
  28.          ::PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_REMOVE);
  29.          CancelBlockingCall();
  30.          return FALSE;  // No need for idle time processing.
  31.        };
  32.      };
  33.      return CSocket::OnMessagePending();
  34.    }
  35.    BOOL CTimeOutSocket::SetTimeOut(UINT uTimeOut)
  36.    {
  37.      m_nTimerID = SetTimer(NULL,0,uTimeOut,NULL);
  38.      return m_nTimerID;
  39.    }
  40.    BOOL CTimeOutSocket::KillTimeOut()
  41.    {
  42.      return KillTimer(NULL,m_nTimerID);
  43.    }


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

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

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

</>code

  1. class CTimeOutSocket : public CSocket{// Attributespublic:
  2. // Operationspublic: CTimeOutSocket(); virtual ~CTimeOutSocket();
  3. // 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
  4.  // Generated message map functions //{{AFX_MSG(CTimeOutSocket)  // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG
  5. // Implementationprotected: int m_nTimerID;private: BOOL KillTimeOut(); BOOL SetTimeOut(int nTimeOut);};
  6. 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();}
  7. int CTimeOutSocket::Receive(void* lpBuf, int nBufLen, int nFlags) { SetTimeOut(10000); int nRecv = CSocket::Receive(lpBuf, nBufLen, nFlags); KillTimeOut(); return nRecv;}
  8. int CTimeOutSocket::Send(const void* lpBuf, int nBufLen, int nFlags) { SetTimeOut(10000); int nSend = CSocket::Send(lpBuf, nBufLen, nFlags); KillTimeOut(); return nSend; }
  9. BOOL CTimeOutSocket::SetTimeOut(int nTimeOut){ m_nTimerID = SetTimer(NULL,0,nTimeOut,NULL); return m_nTimerID;}
  10. BOOL CTimeOutSocket::KillTimeOut(){ return KillTimer(NULL,m_nTimerID);}

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


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

发表评论 (5804人查看0条评论)
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
昵称:
最新评论
------分隔线----------------------------

快速入口

· 365软件
· 杰创官网
· 建站工具
· 网站大全

其它栏目

· 建站教程
· 365学习

业务咨询

· 技术支持
· 服务时间:9:00-18:00
365建站网二维码

Powered by 365建站网 RSS地图 HTML地图

copyright © 2013-2024 版权所有 鄂ICP备17013400号