使用 select() 套接字函数的可扩展客户端/服务器






4.23/5 (19投票s)
一篇关于使用 select() 函数创建可扩展客户端/服务器应用程序的文章
 
 
引言
本文是我关于 IOCP 的文章的延续。 在那篇文章中,我演示了使用 I/O 完成端口创建可扩展客户端/服务器应用程序。 在本文中,我将演示如何使用 select() 函数创建可扩展客户端/服务器应用程序。 在此实现中,客户端和服务器发送和显示简单的字符串消息。
select() 函数与同步套接字一起工作,不需要创建线程。
我在这里提供的客户端 clientselect.exe 与我早期文章中的 clientiocp.exe 相同。 我只是将其重命名了。 此客户端的代码可以在我之前的文章中找到。
使用代码
select() 函数允许开发人员将套接字分配到三个不同的集合中,并监控套接字的 state 变化。 我们可以根据套接字的 status 处理套接字。 为套接字创建的三个集合是
- 读取集合 – 检查此组中的套接字是否可读。 当
- 监听套接字上存在待处理的连接时
- 套接字上收到数据时
- 连接关闭或终止时
- 写入集合 – 检查此组中的套接字是否可写。 当可以在套接字上发送数据时,套接字将被认为可写
- 异常集合 – 检查此组中的套接字是否存在错误。
这些集合使用 fd_set 结构实现。 fd_set 的定义可以在 winsock2.h 中找到。
typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;
以下是可以用来操作集合的宏。 这些宏的源代码可以在 winsock2.h 中找到。
- FD_CLR– 从集合中移除套接字
- FD_ISSET– 帮助识别套接字是否属于指定的集合
- FD_SET– 将套接字分配给指定的集合
- FD_ZERO– 重置集合
对于此实现,客户端信息将存储在 CClientContext 结构的单链表中。
class CClientContext  //To store and manage client related information
{
private:
     int               m_nTotalBytes;
     int               m_nSentBytes;
     SOCKET            m_Socket;  //accepted socket
     char              m_szBuffer[MAX_BUFFER_LEN];
     CClientContext   *m_pNext; //this will be a singly linked list
public:
     //Get/Set calls
     void SetTotalBytes(int n)
     {
          m_nTotalBytes = n;
     }
     int GetTotalBytes()
     {
          return m_nTotalBytes;
     }
     void SetSentBytes(int n)
     {
          m_nSentBytes = n;
     }
     void IncrSentBytes(int n)
     {
          m_nSentBytes += n;
     }
     int GetSentBytes()
     {
          return m_nSentBytes;
     }
     void SetSocket(SOCKET s)
     {
          m_Socket = s;
     }
     SOCKET GetSocket()
     {
          return m_Socket;
     }
     void SetBuffer(char *szBuffer)
     {
          strcpy(m_szBuffer, szBuffer);
     }
     void GetBuffer(char *szBuffer)
     {
          strcpy(szBuffer, m_szBuffer);
     }
     char* GetBuffer()
     {
          return m_szBuffer;
     }
     void ZeroBuffer()
     {
          ZeroMemory(m_szBuffer, MAX_BUFFER_LEN);
     }
     CClientContext* GetNext()
     {
          return m_pNext;
     }
     void SetNext(CClientContext *pNext)
     {
          m_pNext = pNext;
     }
     //Constructor
     CClientContext()
     {
          m_Socket =  SOCKET_ERROR;
          ZeroMemory(m_szBuffer, MAX_BUFFER_LEN);
          m_nTotalBytes = 0;
          m_nSentBytes = 0;
          m_pNext = NULL;
     }
     //destructor
     ~CClientContext()
     {
          closesocket(m_Socket);
     }
};
以下是 InitSets() 函数。 此函数将初始化集合。 将套接字分配给适当的集合。 在调用 select() 函数之前将调用此函数。
//Initialize the Sets
void InitSets(SOCKET ListenSocket) 
{
     //Initialize
     FD_ZERO(&g_ReadSet);
     FD_ZERO(&g_WriteSet);
     FD_ZERO(&g_ExceptSet);
     //Assign the ListenSocket to Sets
     FD_SET(ListenSocket, &g_ReadSet);
     FD_SET(ListenSocket, &g_ExceptSet);
     //Iterate the client context list and assign the sockets to Sets
     CClientContext   *pClientContext  = GetClientContextHead();
     while(pClientContext)
     {
          if(pClientContext->GetSentBytes() < pClientContext->GetTotalBytes())
          {
               //We have data to send
               FD_SET(pClientContext->GetSocket(), &g_WriteSet);
          }
          else
          {
               //We can read on this socket
               FD_SET(pClientContext->GetSocket(), &g_ReadSet);
          }
          //Add it to Exception Set
          FD_SET(pClientContext->GetSocket(), &g_ExceptSet); 
          //Move to next node on the list
          pClientContext = pClientContext->GetNext();
     }
}
以下是 AcceptConnections() 函数。 此函数将使用 select() 监控套接字。 它还将根据套接字的 status 处理套接字。
//This function will loop on while it will manage multiple clients 
//using select()
void AcceptConnections(SOCKET ListenSocket)
{
     while (true)
     {
          InitSets(ListenSocket);
          if (select(0, &g_ReadSet, &g_WriteSet, &g_ExceptSet, 0) > 0) 
          {
               //One of the socket changed state, let's process it.
               //ListenSocket?  Accept the new connection
               if (FD_ISSET(ListenSocket, &g_ReadSet)) 
               {
                    sockaddr_in ClientAddress;
                    int nClientLength = sizeof(ClientAddress);
                    //Accept remote connection attempt from the client
                    SOCKET Socket = accept(ListenSocket, 
                    (sockaddr*)&ClientAddress, &nClientLength);
                    if (INVALID_SOCKET == Socket)
                    {
                         printf("\nError occurred while accepting socket: 
                        %ld.", GetSocketSpecificError(ListenSocket));
                    }
                    //Display Client's IP
                    printf("\nClient connected from: %s", inet_ntoa
                        (ClientAddress.sin_addr)); 
                    //Making it a non blocking socket
                    u_long nNoBlock = 1;
                    ioctlsocket(Socket, FIONBIO, &nNoBlock);
                    CClientContext   *pClientContext  = new CClientContext;
                    pClientContext->SetSocket(Socket);
                    //Add the client context to the list
                    AddClientContextToList(pClientContext);
               }
               //Error occurred for ListenSocket?
               if (FD_ISSET(ListenSocket, &g_ExceptSet)) 
               {
                    printf("\nError occurred while accepting socket: %ld.", 
                    GetSocketSpecificError(ListenSocket));
                    continue;
               }
               //Iterate the client context list to see if 
               //any of the socket there has changed its state
               CClientContext   *pClientContext  = GetClientContextHead();
               while (pClientContext)
               {
                    //Check in Read Set
                    if (FD_ISSET(pClientContext->GetSocket(), &g_ReadSet))
                    {
                         int nBytes = recv(pClientContext->GetSocket(), 
                           pClientContext->GetBuffer(), MAX_BUFFER_LEN, 0);
                         if ((0 == nBytes) || (SOCKET_ERROR == nBytes))
                         {
                              if (0 != nBytes) //Some error occurred, 
                              //client didn't close the connection
                              {
                                   printf("\nError occurred while 
                                receiving on the socket: %d.", 
                                GetSocketSpecificError
                                (pClientContext->GetSocket()));
                              }
                              //In either case remove the client from list
                              pClientContext = DeleteClientContext
                                (pClientContext);
                              continue;
                         }
                         //Set the buffer
                         pClientContext->SetTotalBytes(nBytes);
                         pClientContext->SetSentBytes(0);
                         printf("\nThe following message was received: %s", 
                            pClientContext->GetBuffer());
                    }
                    //Check in Write Set
                    if (FD_ISSET(pClientContext->GetSocket(), &g_WriteSet))
                    {
                         int nBytes = 0;
                         if (0 < (pClientContext->GetTotalBytes() - 
                            pClientContext->GetSentBytes()))
                         {
                              nBytes = send(pClientContext->GetSocket(), 
                            (pClientContext->GetBuffer() + 
                            pClientContext->GetSentBytes()), 
                            (pClientContext->GetTotalBytes() - 
                            pClientContext->GetSentBytes()), 0);
                              if (SOCKET_ERROR == nBytes)
                              {
                                   printf("\nError occurred while 
                                sending on the socket: %d.", 
                                GetSocketSpecificError
                                (pClientContext->GetSocket()));
                                   pClientContext = DeleteClientContext
                                    (pClientContext);
                                   continue;
                              }
                              if (nBytes == 
                                (pClientContext->GetTotalBytes() - 
                                pClientContext->GetSentBytes()))
                              {
                                   //We are done sending the data, 
                              //reset Buffer Size
                                   pClientContext->SetTotalBytes(0);
                                   pClientContext->SetSentBytes(0);
                              }
                              else
                              {
                                   pClientContext->IncrSentBytes(nBytes);
                              }
                         }
                    }
                    //Check in Exception Set
                    if (FD_ISSET(pClientContext->GetSocket(), &g_ExceptSet))
                    {
                         printf("\nError occurred on the socket: %d.", 
                           GetSocketSpecificError(pClientContext->GetSocket()));
                         pClientContext = DeleteClientContext(pClientContext);
                         continue;
                    }
                    //Move to next node on the list
                    pClientContext = pClientContext->GetNext();
               }//while
          }
          else //select
          {
               printf("\nError occurred while executing select(): %ld.", 
                            WSAGetLastError());
               return; //Get out of this function
          }
     }
}
我创建了一个函数 GetSocketSpecificError() 来获取特定套接字上的错误。 使用 select() 时,我们不能依赖 WSAGetLastError() ,因为多个套接字可能存在错误,并且我们需要特定于套接字的错误。
//When using select() multiple sockets may have errors
//This function will give us the socket specific error
//WSAGetLastError() can't be relied upon
int GetSocketSpecificError(SOCKET Socket)
{
     int nOptionValue;
     int nOptionValueLength = sizeof(nOptionValue);
     //Get error code specific to this socket
     getsockopt(Socket, SOL_SOCKET, SO_ERROR, (char*)&nOptionValue, 
                                            &nOptionValueLength);
     return nOptionValue;
}
以下是链表操作函数。 这些将用于处理 CClientContext 单链表。
//Get the head node pointer
CClientContext* GetClientContextHead()
{
     return g_pClientContextHead;
}
//Add a new client context to the list
void AddClientContextToList(CClientContext *pClientContext)
{
     //Add the new client context right at the head
     pClientContext->SetNext(g_pClientContextHead);
     g_pClientContextHead = pClientContext;
}
//This function will delete the node and will return the next node of the list
CClientContext * DeleteClientContext(CClientContext *pClientContext)
{
     //See if we have to delete the head node
     if (pClientContext == g_pClientContextHead) 
     {
          CClientContext *pTemp = g_pClientContextHead;
          g_pClientContextHead = g_pClientContextHead->GetNext();
          delete pTemp;
          return g_pClientContextHead;
     }
     //Iterate the list and delete the appropriate node
     CClientContext *pPrev = g_pClientContextHead;
     CClientContext *pCurr = g_pClientContextHead->GetNext();
     while (pCurr)
     {
          if (pCurr == pClientContext)
          {
               CClientContext *pTemp = pCurr->GetNext();
               pPrev->SetNext(pTemp);
               delete pCurr;
               return pTemp;
          }
          pPrev = pCurr;
          pCurr = pCurr->GetNext();
     }
     return NULL;
}
屏幕截图
 
 
 
 
 
 
 
 
历史
- 2007 年 8 月 7 日:初始发布


