[筆記]windows網絡編程之常見模型

windows 常見模型

select模型

是什麼?

對多個socket 進行管理 調用select()可以獲取指定socket狀態,即select 選擇獲得有響應的指定的socket

爲什麼?

解決基本C/S模型中,accept()、recv()、send()阻塞的問題

select模型與C/S模型的不同點
C/S模型中accept()會阻塞一直傻等socket來鏈接
select模型只解決accept()傻等的問題,不解決recv(),send()執行阻塞問題

怎麼用?

服務端
1.一般創建非阻塞步驟(初始化,創建,綁定ip和端口,監聽,ioctlsocket設置非阻塞)
2.裝填socket數組 FD_SET(socketServer, &allSockets);
3.調用select() 對有響應的socket 做相應處理(accept,send,recv)

具體參照
《windows網絡編程》第八章 8.3 基於select模型的socket編程

參考:網絡編程——select模型(總結)

WSAAsyncSelect模型

是什麼?

應用程序可以在一個socket上接收以windows消息爲基礎的網絡事件通知,它實現了讀寫數據的異步通知功能,但不提供異步的數據傳輸。

爲什麼?

WSAAsyncSelect是select模型的異步版本。在應用程序使用select函數時會發生阻塞現象。可以通過select的timeout參數設置阻塞的時間。在設置的時間內,select函數等待,直到一個或多個套接字滿足可讀或可寫的條件。
而WSAAsyncSelect是非阻塞的。Windows sockets程序在調用recv或send之前,調用WSAAsyncSelect註冊網絡事件。WSAAsyncSelect函數立即返回。當系統中數據準備好時,會嚮應用程序發送消息。此此消息的處理函數中可以調用recv或send進行接收或發送數據。
WSAAsyncSelect模型與select模型的相同點是它們都可以對多個套接字進行管理。但它們也有不小的區別。首先WSAAsyncSelect模型是異步的,且通知方式不同。更重要的一點是:WSAAsyncSelect模型應用在基於消息的Windows環境下,使用該模型時必須創建窗口,而select模型可以廣泛應用在Unix系統,使用該模型不需要創建窗口。最後一點區別:應用程序在調用WSAAsyncSelect函數後,套接字就被設置爲非阻塞狀態。而使用select函數不改變套接字的工作方式。

怎麼用?

1.初始化socket環境,並創建win32自定義事件的socket事件 WM_SOCKET
2.調用WSAAsyncSelect()綁定事件到窗口消息隊列
3.通過GetMessage()輪詢消息,當消息事件發生時,實現對socket的處理(包括accept,send,recv)

具體參照
《windows網絡編程》第八章 8.5 基於WSAAsyncSelect模型的服務器編程

WSAEventSelect模型

是什麼?

允許在多個Socket上接收以事件爲基礎的網絡事件通知,應用程序在創建Socket後,調用WSAEventSelect()函數將事件對象與網絡事件集合相關聯。當網絡事件發生時,應用程序以事件的形式接收網絡事件通知。

爲什麼?

WSAEventSelect與WSAAsyncSelect在網絡事件發生時系統通知應用程序的形式不同。

Select 主動獲取指定socket狀態,
WSAEventSelect,WSAAsyncSelect 則會被動選擇系統通知應用程序socket狀態變化。

WSAEventSelect模型和WSAAsyncSelec模型類似,都是用調用WSAXXXXXSelec函數將socket和事件關聯並註冊到系統,並將socket設置成非阻塞模式。二者不同之處在於socket事件的通知方法:WSAAsyncSelec模型利用窗口句柄和消息映射函數通知網絡事件,而WSAEventSelect模型利用WSAEVENT通知網絡事件。

怎麼用?

服務端:
1.初始化socket環境,並創建用於監聽的socket
2.創建事件對象 WSACreateEvent()
3.將新建的事件對象與監聽Socket相關聯,並註冊該Socket關注的網絡事件集合,通常爲FD_ACCEPT 和 FD_CLOSE
4.等待所有事件對象上發生註冊的網絡事件,並對網絡事件進行處理。
WSWaitForMultipleEvents()
WSAEnumNetWorkEvents();
5.如果觸發了FD_ACCEPT事件,則程序接收來自客戶端的請求,得到與客戶端通信的Socket,併爲該Socket創建相關聯的事件
對象,註冊該Socket關注網絡事件集合,通常爲FD_READ,FD_CLOSE和FD_WRITE
6.如果觸發了FD_CLOSE事件,則關閉Socket,釋放其佔用資源
7.如果觸發了FD_READ事件,則調用recv函數接收來則客戶端的請求
8.如果觸發了FD_WRITE事件,則調用send()函數向客戶端發送數據

具體參照
《windows網絡編程》第八章 8.5 基於WSAEventSelect模型的服務器編程

重疊I/O模型

是什麼?

重疊模型是讓應用程序使用重疊數據結構(WSAOVERLAPPED),一次投遞一個或多個Winsock I/O請求。針對這些提交的請求,在它們完成之後,應用程序會收到通知,於是就可以通過自己另外的代碼來處理這些數據了。
重疊i/o是真正意義上的異步io模型 調用輸入/輸出函數後 立即返回(WSARecv,WSASend)。

爲什麼?

1.可以運行在支持Winsock2的所有Windows平臺 ,而不像完成端口只是支持NT系統。

2.比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重疊I/O(Overlapped I/O)模型使應用程序能達到更佳的系統性能。
因爲它和這4種模型不同的是,使用重疊模型的應用程序通知緩衝區收發系統直接使用數據,也就是說,如果應用程序投遞了一個10KB大小的緩衝區來接收數據,且數據已經到達套接字,則該數據將直接被拷貝到投遞的緩衝區。不需要等待調用recv時才拷貝
而這4種模型種,數據到達並拷貝到單套接字接收緩衝區中,此時應用程序會被告知可以讀入的容量。當應用程序調用接收函數之後,數據才從單套接字緩衝區拷貝到應用程序的緩衝區,差別就體現出來了。
3. 從《windows網絡編程》中提供的試驗結果中可以看到,在使用了P4 1.7G Xero處理器(CPU很強啊)以及768MB的迴應服務器中,最大可以處理4萬多個SOCKET連接,在處理1萬2千個連接的時候CPU佔用率才40% 左右 ―― 非常好的性能,已經直逼完成端口。

怎麼用?

使用事件通知來管理重疊i/o操作

通過事件來通知應用程序I/O 操作已完成

在WSASend()或者WSARecv函數中,當重疊操作完成後,如果lpCompletionRoutine參數爲NULL,則lpOverlapped的hEvent參數將被設爲已授信狀態。應用程序調用WSAWaitForMultipleEvents()函數或者WSAGetOverlappedResult函數等待或者輪詢事件對象變爲未授信狀態。

WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) //lpCompletionRoutine參數爲NULL,使用事件通知

// OverlappedServer.cpp : 定義控制檯應用程序的入口點。
//

#include "stdafx.h"
#include <stdlib.h>
#include "winsock2.h"

#pragma   comment(lib,"ws2_32.lib")
#define DATA_BUFSIZE 4096


int _tmain(int argc, _TCHAR* argv[])
{
	//-----------------------------------------
	// 聲明和初始化變量
	WSABUF DataBuf;							// 發送和接收數據的緩衝區結構體
	char buffer[DATA_BUFSIZE];			// 緩衝區結構體DataBuf中
	DWORD EventTotal = 0,					// 記錄事件對象數組中的數據
		RecvBytes = 0,								// 接收的字節數
		Flags = 0,										// 標識位
		BytesTransferred = 0;					// 在讀、寫操作中實際傳輸的字節數
	
	// 數組對象數組
	WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
	WSAOVERLAPPED AcceptOverlapped;		// 重疊結構體
	SOCKET ListenSocket, AcceptSocket;		// 監聽套接字和與客戶端進行通信的套接字

	//-----------------------------------------
	// 初始化Windows Sockets
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2,2), &wsaData);

	//-----------------------------------------
	// 創建監聽套接字,並將其綁定到本地IP地址和9990端口
	ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	u_short port = 9990;
	char* ip;
	sockaddr_in service;
	service.sin_family = AF_INET;
	service.sin_port = htons(port);
	hostent* thisHost;
	thisHost = gethostbyname("");
	ip = inet_ntoa (*(struct in_addr *)*thisHost->h_addr_list);	
	service.sin_addr.s_addr = inet_addr(ip);
	bind(ListenSocket, (SOCKADDR *) &service, sizeof(SOCKADDR));

	//-----------------------------------------
	// 開始監聽
	listen(ListenSocket, 1);
	printf("Listening...\n");

	//-----------------------------------------
	// 接收連接請求
	AcceptSocket = accept(ListenSocket, NULL, NULL);
	printf("Client Accepted...\n");

	//-----------------------------------------
	// 創建事件對象,建立重疊結構
	EventArray[EventTotal] = WSACreateEvent();
	ZeroMemory(buffer, DATA_BUFSIZE);
	ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));		// 初始化重疊結構
	AcceptOverlapped.hEvent = EventArray[EventTotal];					// 設置重疊結構中的hEvent字段
	DataBuf.len = DATA_BUFSIZE;														// 設置緩衝區
	DataBuf.buf = buffer;
	EventTotal++;																				// 事件對象總數加1

	//-----------------------------------------
	// 處理在套接字上接收到數據
	while (1) {
		DWORD Index;		// 保存處於授信狀態的事件對象句柄

		//-----------------------------------------
		// 調用WSARecv()函數在AcceptSocket套接字上以重疊I/O方式接收數據,保存到DataBuf緩衝區中
		if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) {
			if (WSAGetLastError() != WSA_IO_PENDING)
				printf("Error occured at WSARecv()\n");
		}
		//-----------------------------------------
		// 等待完成的重疊I/O調用
		Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE);


		//-----------------------------------------
		// 決定重疊事件的狀態
		WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags);
		//-----------------------------------------
		// 如果連接已經關閉,則關閉AcceptSocket套接字
		if (BytesTransferred == 0) {
			printf("Closing Socket %d\n", AcceptSocket);
			closesocket(AcceptSocket);
			WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
			return -1;
		}
		//-----------------------------------------
		// 如果有數據到達,則將收到的數據則發送回客戶端
		if (WSASend(AcceptSocket, &DataBuf, 1, &RecvBytes, Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR)
			printf("WSASend() is busted\n");

		//-----------------------------------------
		// 重置已授信的事件對象
		WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
		//-----------------------------------------		
		// 重置Flags變量和重疊結構
		Flags = 0;
		ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
		ZeroMemory(buffer, DATA_BUFSIZE);

		AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0];
		//-----------------------------------------
		// 重置緩衝區
		DataBuf.len = DATA_BUFSIZE;
		DataBuf.buf = buffer;
	}
	system("pause");
	return 0;
}

使用完成例程來管理重疊i/o操作

完成例程則指定應用程序在完成i/o操作後調用一個事先定義的回調函數。

在WSASend()或者WSARecv函數中,如果lpCompletionRoutine參數不爲NULL,則hEvent參數將被忽略,而是將上下文信息傳送給完成例程函數,調用WSAGet-OverlapperResult函數查詢重疊操作的結果。

完成例程函數原型如下:
void CALLBACK CompletionROUTINE( DWORD dwError, // 重疊操作的完成狀態
DWORD cbTransferred, // 發送的字節數
LPWSAOVERLAPPED lpOverlapped, // 指定重疊操作的結構體
DWORD dwFlags) // 標識位

服務端
1.初始化windows socket環境
2.創建監聽socket sListen 並將其綁定到本地地址,端口
3.在socket sListen上進行監聽
4.創建工作線程,對客戶端發送來的數據進行處理
5.循環處理來自客戶端的連接請求,接收連接,並將得到的與客戶端的socket保存到g_sNewClientConnectio中,將變量g_bNewConnectionArried設置爲TRUE,表示存在新的客戶端連接。

// CompletionRoutineServer.cpp : 定義控制檯應用程序的入口點。
//

#include "stdafx.h"
#include <winsock2.h> 
#include <stdio.h> 

#define PORT 9990				// 監聽的端口
#define MSGSIZE 1024		// 發送和接收消息的最大長度
# pragma comment( lib, "ws2_32.lib" ) 

// I/O操作的數據
typedef struct 
{ 
	WSAOVERLAPPED overlap;				// 重疊結構體
	WSABUF Buffer;								// 緩衝區對象
	char szMessage[MSGSIZE] ;			// 緩衝區字符數組
	DWORD NumberOfBytesRecvd;		// 接收字節數
	DWORD Flags;									// 標識位
	SOCKET sClient;								// 套接字
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA; 

// 工作線程,用於接收用戶數據
DWORD WINAPI WorkerThread( LPVOID); 
// 在工作線程中調用WSARecv()函數接收數據時指定的完成例程函數
void CALLBACK CompletionROUTINE( DWORD, DWORD, LPWSAOVERLAPPED, DWORD);

SOCKET g_sNewClientConnection;					// 接收客戶端連接請求後得到的
BOOL g_bNewConnectionArrived = FALSE ;		// 標識是否存在未經WorkerThread()函數處理的新的連接

int _tmain(int argc, _TCHAR* argv[])
{
	WSADATA wsaData;										// Windows Sockets對象
	SOCKET sListen;											// 與客戶端進行通信的套接字
	SOCKADDR_IN local, client;							// 服務器本地地址和客戶端地址
	DWORD dwThreadId;									// 工作線程的線程ID
	int iaddrSize = sizeof(SOCKADDR_IN) ;		// 地址的大小  
	
	// 初始化Windows Socket環境
	WSAStartup( 0x0202, & wsaData) ; 
	// 創建監聽套接字
	sListen = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ; 
	// 綁定
	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(sListen, 3) ; 
	
	// 創建工作線程
	CreateThread( NULL , 0, WorkerThread, NULL , 0, & dwThreadId) ; 
	// 循環處理來自客戶端的連接請求
	while(TRUE) 
	{ 
		// 接收連接,得到與客戶端進行通信的套接字g_sNewClientConnection
		g_sNewClientConnection = accept(sListen, (struct sockaddr *)&client, &iaddrSize) ; 
		// 標識有新的連接
		g_bNewConnectionArrived = TRUE ; 
		// 打印接入的客戶端
		printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client. sin_port)); 
	} 
} 

// 工作線程
DWORD WINAPI WorkerThread( LPVOID lpParam) 
{ 
	LPPER_IO_OPERATION_DATA lpPerIOData = NULL;		// 保存I/O操作的數據
	while (TRUE) 
	{ 
		if ( g_bNewConnectionArrived)		// 如果有新的連接請求
		{ 
			// 爲新的連接執行一個異步操作
			// 爲LPPER_IO_OPERATION_DATA結構體分配堆空間
			lpPerIOData = (LPPER_IO_OPERATION_DATA) HeapAlloc( 
				GetProcessHeap( ), 
				HEAP_ZERO_MEMORY, 
				sizeof (PER_IO_OPERATION_DATA)) ; 
			// 初始化結構體lpPerIOData
			lpPerIOData->Buffer.len = MSGSIZE; 
			lpPerIOData->Buffer.buf = lpPerIOData->szMessage; 
			lpPerIOData->sClient = g_sNewClientConnection; 
			// 接收數據
			WSARecv( lpPerIOData->sClient,					// 接收數據的套接字
				&lpPerIOData->Buffer,								// 接收數據的緩衝區
				1,																// 緩衝區對象的數量
				&lpPerIOData->NumberOfBytesRecvd,		// 接收數據的字節數
				&lpPerIOData->Flags,								// 標識位
				&lpPerIOData->overlap,							// 重疊結構
				CompletionROUTINE) ;								// 完成例程函數,將會在接收數據完成的時候進行相應的調用 
			g_bNewConnectionArrived = FALSE ;			// 標識新的連接已經處理完成
		} 
		SleepEx(1000, TRUE) ;										// 休息1秒鐘,然後繼續
	} 
	return 0; 
} 

// 完成例程函數
void CALLBACK CompletionROUTINE( DWORD dwError,	// 重疊操作的完成狀態
        DWORD cbTransferred,											// 發送的字節數
        LPWSAOVERLAPPED lpOverlapped,							// 指定重疊操作的結構體
        DWORD dwFlags)														// 標識位
{ 
	// 將LPWSAOVERLAPPED類型的lpOverlapped轉化成了LPPER_IO_OPERATION_DATA
	LPPER_IO_OPERATION_DATA lpPerIOData = ( LPPER_IO_OPERATION_DATA) lpOverlapped;
	// 如果發生錯誤或者沒有數據傳輸,則關閉套接字,釋放資源
	if (dwError != 0 || cbTransferred == 0)		
	{ 
		closesocket( lpPerIOData-> sClient) ; 
		HeapFree( GetProcessHeap(), 0, lpPerIOData) ; 
	} 
	else 
	{ 
		lpPerIOData->szMessage[cbTransferred] = '\0';	  // 標識接收數據的結束
		// 向客戶端發送接收到的數據
		send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0) ; 
		// 執行另一個異步操作,接收數據
		memset (&lpPerIOData->overlap, 0, sizeof(WSAOVERLAPPED)); 
		lpPerIOData->Buffer.len = MSGSIZE; 
		lpPerIOData->Buffer.buf = lpPerIOData->szMessage; 
		// 接收數據
		WSARecv( lpPerIOData->sClient, 
						&lpPerIOData->Buffer, 
						1, 
						&lpPerIOData->NumberOfBytesRecvd, 
						&lpPerIOData->Flags, 
						&lpPerIOData->overlap, 
						CompletionROUTINE); 
	} 
}

具體參照
《windows網絡編程》第八章 8.6 基於重疊io模型的服務器編程

重疊IO模型
重疊IO模型之完成例程

完成端口模型

是什麼?

利用線程池處理異步I/O請求,利用完成端口模型可以管理成百上千Socket。

可以把完成端口看成系統維護的一個隊列,操作系統把重疊I/O操作完成的事件通知放到該隊列中,因此稱其爲“完成”端口,當Socket被創建後,可以將其與一個完成端口聯繫起來。

一個應用程序可以創建多個工作線程用於處理完成端口上的通知事件,通常應該爲每個CPU創建一個線程。

一個完成端口實際就是一個通知隊列,操作系統把已經完成的重疊I/O請求的通知放到隊列中,當某項IO操作完成後,系統會向服務端完成端口發送一個i/o完成數據包,此操作在系統內部完成,應用程序在收到I/o完成數據包後,完成端口隊列的一個線程被喚醒,爲客戶端提供服務,服務完成後,該線程會繼續在完成端口上等待.

爲什麼?

(1) 首先,如果使用“同步”的方式來通信的話,這裏說的同步的方式就是說所有的操作都在一個線程內順序執行完成,這麼做缺點是很明顯的:因爲同步的通信操作會阻塞住來自同一個線程的任何其他操作,只有這個操作完成了之後,後續的操作纔可以完成;一個最明顯的例子就是咱們在MFC的界面代碼中,直接使用阻塞Socket調用的代碼,整個界面都會因此而阻塞住沒有響應!所以我們不得不爲每一個通信的Socket都要建立一個線程,多麻煩?這不坑爹呢麼?所以要寫高性能的服務器程序,要求通信一定要是異步的。

    (2) 各位讀者肯定知道,可以使用使用“同步通信(阻塞通信)+多線程”的方式來改善(1)的情況,那麼好,想一下,我們好不容易實現了讓服務器端在每一個客戶端連入之後,都要啓動一個新的Thread和客戶端進行通信,有多少個客戶端,就需要啓動多少個線程,對吧;但是由於這些線程都是處於運行狀態,所以系統不得不在所有可運行的線程之間進行上下文的切換,我們自己是沒啥感覺,但是CPU卻痛苦不堪了,因爲線程切換是相當浪費CPU時間的,如果客戶端的連入線程過多,這就會弄得CPU都忙着去切換線程了,根本沒有多少時間去執行線程體了,所以效率是非常低下的,承認坑爹了不?

    (3) 而微軟提出完成端口模型的初衷,就是爲了解決這種"one-thread-per-client"的缺點的,它充分利用內核對象的調度,只使用少量的幾個線程來處理和客戶端的所有通信,消除了無謂的線程上下文切換,最大限度的提高了網絡通信的性能,

怎麼用?

服務端:
1.初始化windows socket 環境
2.創建完成端口對象completionPort
3.根據當前計算機CPU的數量創建工作線程,並將新建的完成端口對象CompletionPort作爲線程的參數
4.創建監聽SocketListen 並將其綁定到本地地址 端口
5.在while循環中處理來自客服端的請求連接,接收連接,並將得到的與客戶端進行通信的socketAccept保存到PER_HANDLE_DATA結構體對象PerHandleData中.將SocketAccept與前面的端口completionPort關聯
6.在socket accept上調用 WSARecv函數,異步接收socket上來自客戶端的數據,WSARecv函數立即返回,此時socketAccept上不一定有客戶端發送來的消息,在工作線程中會檢測完成端口對象的狀態,並接收來自客戶端的數據,再將這些數據發送回客戶端程序.

// CompletionPortServer.cpp : 定義控制檯應用程序的入口點。
//

#include "stdafx.h"
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#define PORT 9990							// 監聽的端口
#define DATA_BUFSIZE 8192			// 發送和接收消息的最大長度
#pragma comment(lib, "Ws2_32")

// 定義I/O操作的結構體
typedef struct                       
{
   OVERLAPPED Overlapped;					// 重疊結構
   WSABUF DataBuf;									// 緩衝區對象
   CHAR Buffer[DATA_BUFSIZE];				// 緩衝區數組
   DWORD BytesSEND;                             // 發送字節數
   DWORD BytesRECV;                             // 接收的字節數    
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

// 套接字句柄結構體
typedef struct											
{
   SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

// 服務器端工作線程
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);


int _tmain(int argc, _TCHAR* argv[])
{
	SOCKADDR_IN InternetAddr;							// 服務器地址
	SOCKET Listen;												// 監聽套接字
	SOCKET Accept;												// 與客戶端進行通信的套接字
	HANDLE CompletionPort;									// 完成端口句柄
	SYSTEM_INFO SystemInfo;								// 獲取系統信息(這裏主要用於獲取CPU數量)
	LPPER_HANDLE_DATA PerHandleData;			// 套接字句柄結構體
	LPPER_IO_OPERATION_DATA PerIoData;		// 定義I/O操作的結構體
	DWORD RecvBytes;											// 接收到的字節數
	DWORD Flags;													// WSARecv()函數中指定的標識位
	DWORD ThreadID;											// 工作線程編號
	WSADATA wsaData;											// Windows Socket初始化信息
	DWORD Ret;														// 函數返回值
	
	// 初始化Windows Sockets環境
	if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
	{
		printf("WSAStartup failed with error %d\n", Ret);
		return -1;
	}
	// 創建新的完成端口
	if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
	{
		printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());
		return -1;
	}
	// 獲取系統信息
	GetSystemInfo(&SystemInfo);   
   // 根據CPU數量啓動線程
	for(int i = 0; i<SystemInfo.dwNumberOfProcessors * 2; i++)
	{
		HANDLE ThreadHandle;
		// 創建線程,運行ServerWorkerThread()函數            
		if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
		 0, &ThreadID)) == NULL)
		{
			 printf("CreateThread() failed with error %d\n", GetLastError());
			 return -1;
		}      
		CloseHandle(ThreadHandle);
	}
	// 創建監聽套接字
	if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
	  WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
	{
		printf("WSASocket() failed with error %d\n", WSAGetLastError());
		return -1;
	}
	// 綁定到本地地址的9990端口
	InternetAddr.sin_family = AF_INET;
	InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	InternetAddr.sin_port = htons(PORT);
	if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
	{
		printf("bind() failed with error %d\n", WSAGetLastError());
		return -1;
	}   
	// 開始監聽
	if (listen(Listen, 5) == SOCKET_ERROR)
	{
		printf("listen() failed with error %d\n", WSAGetLastError());
		return -1;
	}
   // 監聽端口打開,就開始在這裏循環,一有socket連上,WSAAccept就創建一個socket,
   // 這個socket 和完成端口聯上
	while(TRUE)
	{
		// 等待客戶連接
		if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
		{
			printf("WSAAccept() failed with error %d\n", WSAGetLastError());
			return -1;
		}
		// 分配並設置套接字句柄結構體
		if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)
		{
			printf("GlobalAlloc() failed with error %d\n", GetLastError());
			return -1;
		}	  
		PerHandleData->Socket = Accept;
	  
		// 將與客戶端進行通信的套接字Accept與完成端口CompletionPort相關聯
		if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
				0) == NULL)
		{
			printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
			return -1;
		}
		// 爲I/O操作結構體分配內存空間
		if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL)
		{
			printf("GlobalAlloc() failed with error %d\n", GetLastError());
			return -1;
		}
		// 初始化I/O操作結構體
		ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
		PerIoData->BytesSEND = 0;
		PerIoData->BytesRECV = 0;
		PerIoData->DataBuf.len = DATA_BUFSIZE;
		PerIoData->DataBuf.buf = PerIoData->Buffer;
		Flags = 0;
	  
		// 接收數據,放到PerIoData中,而perIoData又通過工作線程中的ServerWorkerThread函數取出,
		if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
			&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
		{
			if (WSAGetLastError() != ERROR_IO_PENDING)
			{
				printf("WSARecv() failed with error %d\n", WSAGetLastError());
				return -1;
			}
		}
	}
	return 0;
}


//	工作線程,循環檢測完成端口狀態,獲取PerIoData中的數據
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
	HANDLE CompletionPort = (HANDLE) CompletionPortID;	// 完成端口句柄   
	DWORD BytesTransferred;										// 數據傳輸的字節數
	LPOVERLAPPED Overlapped;									// 重疊結構體
	LPPER_HANDLE_DATA PerHandleData;					// 套接字句柄結構體
	LPPER_IO_OPERATION_DATA PerIoData;				// I/O操作結構體
	DWORD SendBytes, RecvBytes;								// 發送和接收的數量
	DWORD Flags;															// WSARecv()函數中的標識位
  
	while(TRUE)
	{
		// 檢查完成端口的狀態
		if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
			(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)
		{
			printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError());
			return 0;
		}

		// 如果數據傳送完了,則退出
		if (BytesTransferred == 0)
		{
			printf("Closing socket %d\n", PerHandleData->Socket);
			// 關閉套接字
			if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
			{
				printf("closesocket() failed with error %d\n", WSAGetLastError());
				return 0;
			}
			// 釋放結構體資源
			GlobalFree(PerHandleData);
			GlobalFree(PerIoData);
			continue;
		}     
		// 如果還沒有記錄接收的數據數量,則將收到的字節數保存在PerIoData->BytesRECV中
		if (PerIoData->BytesRECV == 0)
		{
			PerIoData->BytesRECV = BytesTransferred;
			PerIoData->BytesSEND = 0;
		}
		else   // 如果已經記錄了接收的數據數量,則記錄發送數據量
		{
			PerIoData->BytesSEND += BytesTransferred;
		}
   
		// 將收到的數據原樣發送回客戶端
		if (PerIoData->BytesRECV > PerIoData->BytesSEND)
		{
			ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED)); //清0爲發送準備
			PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;
			PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;

			// 一個字節一個字節發送發送數據出去
			if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,
					&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
			{
				if (WSAGetLastError() != ERROR_IO_PENDING)
				{
					printf("WSASend() failed with error %d\n", WSAGetLastError());
					return 0;
				}
			}
		}
		else
		{
			PerIoData->BytesRECV = 0;
			Flags = 0;
			ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
			PerIoData->DataBuf.len = DATA_BUFSIZE;
			PerIoData->DataBuf.buf = PerIoData->Buffer;

			if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
				&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
			{
				if (WSAGetLastError() != ERROR_IO_PENDING)
				{
					printf("WSARecv() failed with error %d\n", WSAGetLastError());
					return 0;
				}
			}
		}
   }
}

具體參照
《windows網絡編程》第八章 8.7 基於完成端口模型的服務器編程

完成端口(CompletionPort)詳解 - 手把手教你玩轉網絡編程系列之三

單播,組播,多播

是什麼?

單播,1 對 1
組播,1 對 多
廣播,1 對 局域網所有

爲什麼?

單播,基於tcp,建立可靠連接,需要c/s ip;
組播,基於udp,監聽同一ip(224.0.0.0至239.255.255.255)的一組都會接 收到消息;
廣播,基於udp,監聽 地址:xx.xx.xx.255 都會接收到消息

用途

單播用於連接建立後的私密通訊,組播和廣播用於通知,建立連接前的通訊,獲取ip。

安全套接字協議SSL

是什麼?

SSL 可以用來保障在internet上數據傳輸的安全,利用數據加密技術,可確保數據在網絡上的傳輸過程不會被截取及其監聽.

SSL用於在web服務器和瀏覽器之間建立加密連接的標準安全技術.

SSL協議提供的安全信道有以下三個特性:

私密性。因爲在握手協議定義了會話密鑰後,所有的消息都被加密。

確認性。因爲儘管會話的客戶端認證是可選的,但是服務器端始終是被認證的。

可靠性。因爲傳送的消息包括消息完整性檢查(使用MAC)。

主要工作在應用層(http,ftp,telnet)和 傳輸層(tcp/udp)之間的

在這裏插入圖片描述

參考
圖解安全套接字SSL協議的工作原理

安全套接層(SSL)協議

簡單瞭解:Openssl開源安全套接字協議

<<windows網絡編程代碼>>以及廣播代碼

百度網盤代碼 提取碼:72y7

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章