1. 程式人生 > >網路程式設計4-select模型

網路程式設計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

 <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 sockServer = 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 = 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網路程式設計》第八章。