網路程式設計4-select模型
4.3伺服器端IO模型
雙十一時,淘寶同時線上使用者可達上億。想處理上億使用者的連線請求,必須要求阿里伺服器的效能非常高。而以前12306處理過多使用者的需求時,經常出現宕機的情況,這說明伺服器效能不足,在設計時技術有限而導致優化不足。
因此,在伺服器端程式設計需要構造高效能的IO模型,windows平臺常用的IO模型有5種:選擇模型(Select)、非同步選擇(WSAAsyncSelect)、事件選擇(WSAEventSelect)、重疊I/O(Overlapped I/O)、完成埠(Completion Port)。
4.3.1選擇模型
//方法1
//TCP伺服器端
#include
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib,"ws2_32.lib")//引用庫檔案
using namespace std;
int main(int argc, char ** argv)
{
//步驟1:當前應用程式和相應的socket庫繫結
WORD wVersionRequested;
WSADATA
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步驟2:建立TCP網路套接字
SOCKET sockServer = socket(AF_INET
//步驟3:設定伺服器端地址結構SOCKADDR_IN
SOCKADDR_IN addr_server;
//3.1 初始化SOCKADDR_IN
memset(&addr_server, 0, sizeof(addr_server));
//3.2 設定地址協議族
addr_server.sin_family = AF_INET;
//3.3 設定客戶端可連線的IP
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示繫結電腦上所有網絡卡IP
//3.4 設定客戶端可連線的埠
addr_server.sin_port = htons(6000);//不能使用公認埠,即埠>= 1024
//步驟4:將套接字和地址繫結
bind(sockServer, (SOCKADDR*)&addr_server, sizeof(addr_server));
//步驟5:監聽
listen(sockServer, 5);
cout << "Start Listen..." << endl;
SOCKADDR_IN addr_client;
int len = sizeof(SOCKADDR);
const int maxClientCount = 63;
int curClientCount = 0;
SOCKET sockClient_A[maxClientCount];
while (1)
{
//步驟6:將伺服器端的socket和客戶端的socket放入FD_SET中
FD_SET sock_ReadSet;
FD_ZERO(&sock_ReadSet);
FD_SET(sockServer, &sock_ReadSet);
for (int i = 0; i < curClientCount; ++i)
{
if (sockClient_A[i] != 0)
FD_SET(sockClient_A[i], &sock_ReadSet);
}
timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
//步驟7:等待FD_SET中的socket有資料傳輸
//int ret = select(0, &sock_ReadSet, NULL, NULL,NULL);//若有資料傳輸則立即返回,否則一直等待(最後一個引數:NULL表示一直等待)
int ret = select(0, &sock_ReadSet, NULL, NULL, &tv);//若有資料傳輸則立即返回,否則等待2秒,再返回
if (ret != SOCKET_ERROR)
{
for (int i = 0; i < curClientCount + 1; i++)
{
//步驟8:若是客戶端的socket有資料傳輸
if (FD_ISSET(sockClient_A[i], &sock_ReadSet))
{
char Buf[1024] = "\0";
int nRet = recv(sockClient_A[i], Buf, 1024, 0);
//步驟8.1:客戶端和伺服器端斷開連線:nRet == 0
if (nRet == SOCKET_ERROR || nRet == 0)
{
//刪除sock_ReadSet中的socket
closesocket(sockClient_A[i]);
FD_CLR(sockClient_A[i], &sock_ReadSet);
//刪除陣列sockClient_A[i]中的套接字
for (int a = i; a < curClientCount; a++)
{
sockClient_A[a] = sockClient_A[a + 1];
if (a == curClientCount - 1)
sockClient_A[a + 1] = 0;
}
curClientCount--;
}
//步驟8.2:客戶端向伺服器端傳輸資料
else
{
//二次開發
cout << Buf << endl;
strcat(Buf, ":Server Received");
send(sockClient_A[i], Buf, strlen(Buf) + 1, 0);
}
}
//步驟9:若是伺服器端的socket有資料傳輸,則有客戶端連線伺服器端
//防止連線後,立即傳資料,因此把連線的程式碼放在資料接收程式碼的後面
if (FD_ISSET(sockServer, &sock_ReadSet))
{
SOCKET sockClient = accept(sockServer, (SOCKADDR*)&addr_client, &len);
if (curClientCount < maxClientCount)
{
printf("連線個數:%d\n", curClientCount + 1);
sockClient_A[curClientCount] = sockClient;
curClientCount++;
break;
}
else
{
cout << "達到最大連線數..." << endl;
send(sockClient, "Client Exit...", 1024, 0);
closesocket(sockClient);
}
}
}
}
}
//步驟10:關閉套接字
closesocket(sockServer);
//步驟11:將應用程式和socket庫解除繫結
WSACleanup();
return 0;
}
//TCP客戶端:程式碼不變
#include <iostream>
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib,"ws2_32.lib")//引用庫檔案
using namespace std;
int main(int argc, char ** argv)
{
//步驟1:當前應用程式和相應的socket庫繫結
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步驟2:建立TCP網路套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
//步驟3:設定伺服器端地址結構SOCKADDR_IN
SOCKADDR_IN addr_server;
//3.1 初始化SOCKADDR_IN
memset(&addr_server, 0, sizeof(addr_server));
//3.2 設定地址協議族
addr_server.sin_family = AF_INET;
//3.3 設定伺服器端的IP
addr_server.sin_addr.s_addr = inet_addr("192.168.1.101");
//3.4 設定伺服器端的埠號
addr_server.sin_port = htons(1900);//不能使用公認埠,即埠>= 1024
//步驟4:客戶端連線伺服器
int result = connect(sock, (SOCKADDR*)&addr_server, sizeof(addr_server));
if (result == -1)
return -1;
cout << "Connect Seccessed!" << endl;
//步驟5:向伺服器端傳送資料
char Buf[1024] = "\0";
cin.getline(Buf, 1024);
send(sock, Buf, 1024, 0);
recv(sock, Buf, 1024, 0);
printf("%s\n", Buf);
//步驟6:關閉套接字
closesocket(sock);
//步驟7:將應用程式和socket庫解除繫結
WSACleanup();
return 0;
}
方法1是將伺服器端的套接字和客戶端的套接字都放入FD_SET結構體中。當伺服器端的套接字有資料傳輸,說明有客戶端需要連線;當客戶端的套接字有資料傳輸,要麼是斷開連線,要麼是由資料傳送。
知識點1:fd_set結構體
typedef struct fd_set {
u_int fd_count; /* 套接字數量 */
SOCKET fd_array[FD_SETSIZE]; /* 套接字陣列,FD_SETSIZE = 64 */
} fd_set;
知識點2:
select(引數1,引數2,引數3,引數4,引數5):輪循FD_SET中的套接字上是否有資料傳輸。
引數1:0,忽略;
引數2:FD_SET *,指向一組可讀性檢查的套接字(有資料可讀入;連線關閉、重設、中止)。
引數3:FD_SET *,指向一組可寫性檢查的套接字。
引數4:FD_SET *,指向一組等待錯誤檢查的套接字。
引數5:超時時間。若有資料,立即返回;否則等待tv_sec秒+ tv_usec毫秒。若timeval為NULL,表示一直等待。
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 毫秒 */
};
注:當客戶端有資料傳輸時,執行select函式後,fd_count會減一,因此在步驟8的上一行程式碼處,我並未用fd_count來判斷套接字數量,而是使用curClientCount + 1。
選擇模型的本質是在select函式處,對多個客戶端的套接字進行輪循,也因此導致效率低下。
//方法2
//伺服器端
#include <winsock.h>
#include <stdio.h>
#define PORT 6000
#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
HANDLE hHandle = CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
CloseHandle(hHandle);
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 fdTotal
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[i], &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[i], &fdread))
{
// A read event happened on pfdTotal->fd_array[i]
ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Client socket %d closed.\n", g_CliSocketArr[i]);
closesocket(g_CliSocketArr[i]);
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[i], szMessage, strlen(szMessage), 0);
}
}
}
}
return 0;
}
//TCP客戶端:程式碼不變
#include <iostream>
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib,"ws2_32.lib")//引用庫檔案
using namespace std;
int main(int argc, char ** argv)
{
//步驟1:當前應用程式和相應的socket庫繫結
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步驟2:建立TCP網路套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
//步驟3:設定伺服器端地址結構SOCKADDR_IN
SOCKADDR_IN addr_server;
//3.1 初始化SOCKADDR_IN
memset(&addr_server, 0, sizeof(addr_server));
//3.2 設定地址協議族
addr_server.sin_family = AF_INET;
//3.3 設定伺服器端的IP
addr_server.sin_addr.s_addr = inet_addr("192.168.1.101");
//3.4 設定伺服器端的埠號
addr_server.sin_port = htons(1900);//不能使用公認埠,即埠>= 1024
//步驟4:客戶端連線伺服器
int result = connect(sock, (SOCKADDR*)&addr_server, sizeof(addr_server));
if (result == -1)
return -1;
cout << "Connect Seccessed!" << endl;
//步驟5:向伺服器端傳送資料
char Buf[1024] = "\0";
cin.getline(Buf, 1024);
send(sock, Buf, 1024, 0);
recv(sock, Buf, 1024, 0);
printf("%s\n", Buf);
//步驟6:關閉套接字
closesocket(sock);
//步驟7:將應用程式和socket庫解除繫結
WSACleanup();
return 0;
}
方法2中,主執行緒負責連線客戶端,工作執行緒負責處理客戶端傳來的資料。
方法2中的程式碼摘自:《Windows網路程式設計》第八章。