zoukankan      html  css  js  c++  java
  • socket select模型

    由于socket recv()方法是堵塞式的,当多个客户端连接服务器时,其中一个socket的recv调用时,会产生堵塞,使其他连接不能继续。

    如果想改变这种一直等下去的焦急状态,可以多线程来实现(不再等待,同时去recv,同时阻塞),每个socket连接使用一个线程,这样效率十分低下,根本不可能应对负荷较大的情况(是啊,占用各种资源,电脑啊,你耗不起)。

    这时候我们便可以采取select模型。select允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定时间后才唤醒它。select告诉内核对哪些描述子(文件描述符)感兴趣以及等待多长时间。这就是所谓的非阻塞模型,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况。如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。

    select的函数原型:

    int select
    (
         int nfds, //Winsock中此参数无意义
         fd_set* readfds, //进行可读检测的Socket
         fd_set* writefds, //进行可写检测的Socket
         fd_set* exceptfds, //进行异常检测的Socket
         const struct timeval* timeout //非阻塞模式中设置最大等待时间
     )
    

      第一种解释版本:

    /*参数列表
    int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。 
      
    fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。 
      
    fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。 
      
    fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。 
      
    struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
    第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
    第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
    第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
    */
    

      OS内核通过查看读或者写数据缓冲区是否有数据类决定select的返回值。

    /*
    返回值: 
    
    负值:select错误
    
    正值:某些文件可读写或出错,即使是出错了,但是调用recv函数的时候不会阻塞了
    
    0:等待超时,没有可读写或错误的文件
    */
    

      第二种解释版本:

    参数解释:
    
    参数1 nfds:这个参数是一个被忽略的参数,我们在程序中传入0即可,其目的是与伯克利套接字兼容。
    
    参数2 readfds:可读性监视集合,可读性指有连接到来、有数据到来、连接已关闭、重置或终止。
    
    参数3 writefds:可写性监视集合,可写性指数据可以发送、连接可以成功。
    
    参数4 exceptfds:例外性监视集合,例外性指连接会失败、外带数据到来
    
    参数4 timeout:该参数指定select会等待的时间,者结构很简单,要是有兴趣可以看看msdn
    
    注意:select参数中的三个监视集合至少有一个不能为空,任何其它两个都可为空
    

      


    fd_set的结构,这个结构是用来装SOCKET的,把要监视SOCKET传给这个结构,然后同select函数进行监视。

    struct fd_set 
    {
      u_int    fd_count;       // how many are SET? 
      SOCKET   fd_array[FD_SETSIZE];     // an array of SOCKETs 
    } ;
    

      和select模型紧密结合的四个宏:

    FD_CLR( s,*set) 从队列set删除句柄s。
    
    FD_ISSET( s, *set) 检查句柄s是否存在与队列set中。
    
    FD_SET( s,*set )把句柄s添加到队列set中。
    
    FD_ZERO( *set ) 把set队列初始化成空队列。
    

      select选择模式倚赖于select函数,其思想就是让select函数对传入fd_set进行监视(fd_set中装有你的SOCKET句柄),如果没什么事发生select就将fd_set中的SOCKET清除。

    timeval outTime;
    outTime.tv = 1;   //设置等待时间为1s
    outTime.usec = 0; //毫秒
    fd_set fdread;
    while(true)
    {
       FD_ZERO(&fdread);
       FD_SET(sessionSock, &fdread) //sessionSock为之前创建的会话套接字
       select(0, &fdread, NULL, NULL, &outTime);
       if(FD_ISSET(sessionSock, &fdread))//判断套接字是否还在集合中
       {
          recv_cnt = recv(sessionSock, buf, bufSize, 0);
       }
       else
       {
          //没有数据写入,进行其他操作
       }
    }
    

      结束了吗,有问题吗?是否有人觉得奇怪,我这说的是异步I/O,但这个select依然会阻塞进程啊,不是设置了outTime的吗?此问题很好,select本身是会阻塞的,我们可以使用select实现阻塞式套接字(如上),也可以实现异步套接字。我个人对实现异步套接字的理解是:你可以单独使用一个线程来进行你select,也就是说select阻塞你单独的线程,说白了就是让线程来完成异步。


    下面具体分析一下项目中的socket的select模型:

    对于工具机的select模型,它作为服务器只有一个socket,但是也用select模型,这样可以避免recv()函数阻塞:

    while(ros::ok())
    {
    	if(!connected)
    	{
    		ROS_INFO("Restarting connection...");
    
    		if(serverSocket > 0)
    		{
    		  close(serverSocket);
    		  serverSocket = -1;
    		}
    
    		if(clientSocket > 0)
    		{
    		  close(clientSocket);
    		  clientSocket = -1;
    		}
    
    		bool re;
    		if(bServer)
    		  re = startServer();
    		else
    		  re = connectServer();
    		if(!re)
    			continue;
    	}//if
    	int len =0;
    	int revDateCount = 0;
    	
    	int re = selectSocket(clientSocket, 100);
    
    	if(re < 0)
    	{
    	  ROS_ERROR("Receive Thread Select Error!");
    	  connected = false;
    	  continue;
    	}
    	else if(re == 0)
    	{
    	  continue;
    	}
    	else//re > 0
    	{
    	  len  = recv(clientSocket, (void*)recvBuffer, MAX_BUFFER_LEN, 0);
    	  if(len > 0)
    	  {
    		memcpy((void*)(buff + revDateCount), (void*)recvBuffer, len);
    		revDateCount += len;
    	  }//if
    	  else
    	  {
    		ROS_ERROR("Receive Data Error!");
    		connected = false;
    		goto __reconnect__;
    	  }//else
    	}// re > 0
    
    	__reconnect__:;
    }//while(ros::ok())
    

      其中的selectSocket函数如下,fd_set中只有一个描述符

    int CSocket::selectSocket(int &hSocket, int msec, int sec, bool bRead)
    {
        fd_set fdset;
        struct timeval tv;
        FD_ZERO(&fdset);
        FD_SET(hSocket, &fdset);
        msec = msec > 1000 ? 1000 : msec;
        tv.tv_sec  = sec;
        tv.tv_usec = msec * 1000;
    
        int iRet = 0;
        if ( bRead ) {
            iRet = select(hSocket + 1, &fdset, NULL , NULL, &tv);
        }else{
            iRet = select(hSocket + 1, NULL , &fdset, NULL, &tv);
        }
        return iRet;
    }
    

      利用select的模式,虽然在工控机的服务器端只有一个socket,但是select模式能够很好的在不阻塞线程的情况下,调用recv函数。

     再看云台角度查询的socket客户端:

    socketMutex.Lock();
    byte data[7] ={0xFF, 0x01,0x00, 0x51, 0x00, 0x00, 0x52};
    if(sendData(data, sizeof(data), false) == 7)
    {	
    	Sleep(50);
    	while(1)
    	{	
    		FD_ZERO(&readfds);
    		FD_SET(clientSocket, &readfds);
    		time.tv_sec = 0;
    		time.tv_usec = 500000;
    		re = select(nfds, &readfds, NULL, NULL, &time);
    
    		if (re > 0)
    		{	
    			num = recv(clientSocket, (char*)buffer, 128, 0);
    			if(num == 7)
    			{
    				p=buffer[4];
    				p = (p<<8);
    				p += buffer[5];
    				break;
    			}
    			else if (num <= 0)
    			{
    				bConnect = FALSE;
    				ErroFunc(_T("连接丢失"));
    				goto __reConnect__;
    			}
    			else
    			{
    				ErroFunc(_T("丢包"));
    			}
    		}//if
    		else
    		{
    			bConnect = FALSE;
    			ErroFunc(_T("连接丢失,正在重连..."));
    			socketMutex.Unlock();
    			goto __reConnect__;
    		}//else
    
    	}//while
    
    }//if
    socketMutex.Unlock();
    

      在select返回异常的时候,直接重新连接,不调用recv。

  • 相关阅读:
    Oozie — What Why and How
    git 用户手册
    整理笔记 C语言
    第一次理解通用链表
    C++ 通用队列类
    懂了这些,你才真正懂了C
    简单键盘驱动
    简述进程间通信方式
    几何原本
    GSP几何画板简介
  • 原文地址:https://www.cnblogs.com/stemon/p/4728486.html
Copyright © 2011-2022 走看看