使用 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 日:初始发布