socket通訊中select函式的使用和詳解
阿新 • • 發佈:2019-01-27
---------------------------面向連線
#include <winsock.h>
#include <stdio.h>
#define PORT 5150
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib")
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];
DWORD WINAPI WorkerThread(LPVOID lpParameter);
int main()
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
int iaddrSize = sizeof(SOCKADDR_IN);
DWORD dwThreadId;
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen listen(sListen, 3);
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{ // Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Add socket to g_CliSocketArr
g_CliSocketArr[g_iTotalConn++] = sClient;
}
return 0;
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int i;
fd_set fdread;
int ret;
struct timeval tv = {1, 0};
char szMessage[MSGSIZE];
while (TRUE)
{
FD_ZERO(&fdread);
for (i = 0; i < g_iTotalConn; i++)
{
FD_SET(g_CliSocketArr, &fdread);
} // We only care read event
ret = select(0, &fdread, NULL, NULL, &tv);
if (ret == 0)
{ // Time expired
continue;
}
for (i = 0; i < g_iTotalConn; i++)
{
if (FD_ISSET(g_CliSocketArr, &fdread))
{ // A read event happened on g_CliSocketArr
ret = recv(g_CliSocketArr, szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Client socket %d closed.\n", g_CliSocketArr);
closesocket(g_CliSocketArr);
if (i < g_iTotalConn - 1)
{
g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
}
}
else
{
// We received a message from client
szMessage[ret] = '\0';
send(g_CliSocketArr, szMessage, strlen(szMessage), 0);
}
} //if
}//for
}//while
return 0;
}
伺服器的幾個主要動作如下:
1.建立監聽套接字,繫結,監聽;
2.建立工作者執行緒;
3.建立一個套接字陣列,用來存放當前所有活動的客戶端套接字,每accept一個連線就更新一次陣列;
4.接受客戶端的連線。
這裡有一點需要注意的,就是我沒有重新定義FD_SETSIZE巨集,所以伺服器最多支援的併發連線數為64。而且,這裡決不能無條件的ccept,伺服器應該根據當前的連線數來決定
是否接受來自某個客戶端的連線。一種比較好的實現方案就是採用WSAAccept函式,而且讓WSAAccept回撥自己實現的Condition Function。
如下所示:
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR *
g,DWORD dwCallbackData)
{
if (當前連線數 < FD_SETSIZE)
return CF_ACCEPT;
else
return CF_REJECT;
}
工作者執行緒裡面是一個死迴圈,一次迴圈完成的動作是:
1.將當前所有的客戶端套接字加入到讀集fdread中;
2.呼叫select函式;
3.檢視某個套接字是否仍然處於讀集中,如果是,則接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,則表示客戶端套接字主動關閉,這時需要將伺服器中
對應的套接字所繫結的資源釋放掉,然後調整我們的套接字陣列(將陣列中最後一個套接字挪到當前的位置上)。
除了需要有條件接受客戶端的連線外,還需要在連線數為0的情形下做特殊處理,因為如果讀集中沒有任何套接字,select函式會立刻返回,這將導致工作者執行緒成為一個毫無
停頓的死迴圈,CPU的佔用率馬上達到100%。
關係到套接字列表的操作都需要使用迴圈,在輪詢的時候,需要遍歷一次,再新的一輪開始時,將列表加入佇列又需要遍歷一次.也就是說,Select在工作一次時,需要至少遍歷2次
列表,這是它效率較低的原因之一.
在大規模的網路連線方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰類遊戲上,比如類似星際這種,因為它小巧易於實現,且對戰類遊戲的網路連線量
並不大. 對於Select模型想要突破Windows 64個限制的話,可以採取分段輪詢,一次輪詢64個.例如套接字列表為128個,在第一次輪詢時,將前64個放入佇列中用Select進行狀態查詢,
待本次操作全部結束後.將後64個再加入輪詢佇列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支援無限多個.
#include <winsock.h>
#include <stdio.h>
#define PORT 5150
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib")
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];
DWORD WINAPI WorkerThread(LPVOID lpParameter);
int main()
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
int iaddrSize = sizeof(SOCKADDR_IN);
DWORD dwThreadId;
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen listen(sListen, 3);
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{ // Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Add socket to g_CliSocketArr
g_CliSocketArr[g_iTotalConn++] = sClient;
}
return 0;
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int i;
fd_set fdread;
int ret;
struct timeval tv = {1, 0};
char szMessage[MSGSIZE];
while (TRUE)
{
FD_ZERO(&fdread);
for (i = 0; i < g_iTotalConn; i++)
{
FD_SET(g_CliSocketArr, &fdread);
} // We only care read event
ret = select(0, &fdread, NULL, NULL, &tv);
if (ret == 0)
{ // Time expired
continue;
}
for (i = 0; i < g_iTotalConn; i++)
{
if (FD_ISSET(g_CliSocketArr, &fdread))
{ // A read event happened on g_CliSocketArr
ret = recv(g_CliSocketArr, szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Client socket %d closed.\n", g_CliSocketArr);
closesocket(g_CliSocketArr);
if (i < g_iTotalConn - 1)
{
g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
}
}
else
{
// We received a message from client
szMessage[ret] = '\0';
send(g_CliSocketArr, szMessage, strlen(szMessage), 0);
}
} //if
}//for
}//while
return 0;
}
伺服器的幾個主要動作如下:
1.建立監聽套接字,繫結,監聽;
2.建立工作者執行緒;
3.建立一個套接字陣列,用來存放當前所有活動的客戶端套接字,每accept一個連線就更新一次陣列;
4.接受客戶端的連線。
這裡有一點需要注意的,就是我沒有重新定義FD_SETSIZE巨集,所以伺服器最多支援的併發連線數為64。而且,這裡決不能無條件的ccept,伺服器應該根據當前的連線數來決定
是否接受來自某個客戶端的連線。一種比較好的實現方案就是採用WSAAccept函式,而且讓WSAAccept回撥自己實現的Condition Function。
如下所示:
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR *
g,DWORD dwCallbackData)
{
if (當前連線數 < FD_SETSIZE)
return CF_ACCEPT;
else
return CF_REJECT;
}
工作者執行緒裡面是一個死迴圈,一次迴圈完成的動作是:
1.將當前所有的客戶端套接字加入到讀集fdread中;
2.呼叫select函式;
3.檢視某個套接字是否仍然處於讀集中,如果是,則接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,則表示客戶端套接字主動關閉,這時需要將伺服器中
對應的套接字所繫結的資源釋放掉,然後調整我們的套接字陣列(將陣列中最後一個套接字挪到當前的位置上)。
除了需要有條件接受客戶端的連線外,還需要在連線數為0的情形下做特殊處理,因為如果讀集中沒有任何套接字,select函式會立刻返回,這將導致工作者執行緒成為一個毫無
停頓的死迴圈,CPU的佔用率馬上達到100%。
關係到套接字列表的操作都需要使用迴圈,在輪詢的時候,需要遍歷一次,再新的一輪開始時,將列表加入佇列又需要遍歷一次.也就是說,Select在工作一次時,需要至少遍歷2次
列表,這是它效率較低的原因之一.
在大規模的網路連線方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰類遊戲上,比如類似星際這種,因為它小巧易於實現,且對戰類遊戲的網路連線量
並不大. 對於Select模型想要突破Windows 64個限制的話,可以採取分段輪詢,一次輪詢64個.例如套接字列表為128個,在第一次輪詢時,將前64個放入佇列中用Select進行狀態查詢,
待本次操作全部結束後.將後64個再加入輪詢佇列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支援無限多個.