OCP模型與網絡編程
一、前言:
1. 我想只要是寫過或者想要寫C/S模式網絡服務器端的朋友,都應該或多或少的聽過完成端口的大名吧,完成端口會充分利用Windows內核來進行I/O的調度,是用于C/S通信模式中性能最好的網絡通信模型,沒有之一;甚至連和它性能接近的通信模型都沒有。
2. 完成端口和其他網絡通信方式最大的區別在哪里呢?
(1) 首先,如果使用“同步”的方式來通信的話,這里說的同步的方式就是說所有的操作都在一個線程內順序執行完成,這么做缺點是很明顯的:因為同步的通信操作會阻塞住來自同一個線程的任何其他操作,只有這個操作完成了之后,后續的操作才可以完成;一個最明顯的例子就是咱們在MFC的界面代碼中,直接使用阻塞Socket調用的代碼,整個界面都會因此而阻塞住沒有響應!所以我們不得不為每一個通信的Socket都要建立一個線程,多麻煩?這不坑爹呢么?所以要寫高性能的服務器程序,要求通信一定要是異步的。
(2) 各位讀者肯定知道,可以使用使用“同步通信(阻塞通信)+多線程”的方式來改善(1)的情況,那么好,想一下,我們好不容易實現了讓服務器端在每一個客戶端連入之后,都要啟動一個新的Thread和客戶端進行通信,有多少個客戶端,就需要啟動多少個線程,對吧;但是由于這些線程都是處于運行狀態,所以系統不得不在所有可運行的線程之間進行上下文的切換,我們自己是沒啥感覺,但是CPU卻痛苦不堪了,因為線程切換是相當浪費CPU時間的,如果客戶端的連入線程過多,這就會弄得CPU都忙著去切換線程了,根本沒有多少時間去執行線程體了,所以效率是非常低下的,承認坑爹了不?
(3) 而微軟提出完成端口模型的初衷,就是為了解決這種"one-thread-per-client"的缺點的,它充分利用內核對象的調度,只使用少量的幾個線程來處理和客戶端的所有通信,消除了無謂的線程上下文切換,最大限度的提高了網絡通信的性能,這種神奇的效果具體是如何實現的請看下文。
3. 完成端口被廣泛的應用于各個高性能服務器程序上,例如著名的Apache….如果你想要編寫的服務器端需要同時處理的并發客戶端連接數量有數百上千個的話,那不用糾結了,就是它了
二、提出相關問題:
1. IOCP模型是什么?
2. IOCP模型是用來解決什么問題的?它為什么存在?
3. 使用IOCP模型需要用到哪些知識?
4. 如何使用IOCP模型與Socket網絡編程結合起來?
5. 學會了這個模型以后與我之前寫過的簡單的socket程序主要有哪些不同點?
部分問題探究及解決:
1. 什么是IOCP?什么是IOCP模型?IOCP模型有什么作用?
1) IOCP(I/O Completion Port),常稱I/O完成端口。
2) IOCP模型屬于一種通訊模型,適用于(能控制并發執行的)高負載服務器的一個技術。
3) 通俗一點說,就是用于高效處理很多很多的客戶端進行數據交換的一個模型。
4) 或者可以說,就是能異步I/O操作的模型。
5) 只是了解到這些會讓人很糊涂,因為還是不知道它究意具體是個什么東東呢?
下面我想給大家看三個圖:
第一個是IOCP的內部工作隊列圖。(整合于《IOCP本質論》文章,在英文的基礎上加上中文對照)
第二個是程序實現IOCP模型的基本步驟。(整合于《深入解釋IOCP》,加個人觀點、理解、翻譯)
第三個是使用了IOCP模型及沒使用IOCP模型的程序流程圖。(個人理解繪制)
2. IOCP的存在理由(IOCP的優點)及技術相關有哪些?
之前說過,很通俗地理解可以理解成是用于高效處理很多很多的客戶端進行數據交換的一個模型,那么,它具體的優點有些什么呢?它到底用到了哪些技術了呢?在Windows環境下又如何去使用這些技術來編程呢?它主要使用上哪些API函數呢?呃~看來我真是一個問題多多的人,跟前面提出的相關問題變種延伸了不少的問題,好吧,下面一個個來解決。
1) 使用IOCP模型編程的優點
① 幫助維持重復使用的內存池。(與重疊I/O技術有關)
② 去除刪除線程創建/終結負擔。
③ 利于管理,分配線程,控制并發,最小化的線程上下文切換。
④ 優化線程調度,提高CPU和內存緩沖的命中率。
2) 使用IOCP模型編程汲及到的知識點(無先后順序)
① 同步與異步
② 阻塞與非阻塞
③ 重疊I/O技術
④ 多線程
⑤ 棧、隊列這兩種基本的數據結構
3) 需要使用上的API函數
① 與SOCKET相關
1、鏈接套接字動態鏈接庫:int WSAStartup(...);
2、創建套接字庫: SOCKET socket(...);
3、綁字套接字: int bind(...);
4、套接字設為監聽狀態: int listen(...);
5、接收套接字: SOCKET accept(...);
6、向指定套接字發送信息:int send(...);
7、從指定套接字接收信息:int recv(...);
② 與線程相關
1、創建線程:HANDLE CreateThread(...);
③ 重疊I/O技術相關
1、向套接字發送數據: int WSASend(...);
2、向套接字發送數據包: int WSASendFrom(...);
3、從套接字接收數據: int WSARecv(...);
4、從套接字接收數據包: int WSARecvFrom(...);
④ IOCP相關
1、創建完成端口: HANDLE WINAPI CreateIoCompletionPort(...);
2、關聯完成端口: HANDLE WINAPI CreateIoCompletionPort(...);
3、獲取隊列完成狀態: BOOL WINAPI GetQueuedCompletionStatus(...);
4、投遞一個隊列完成狀態:BOOL WINAPI PostQueuedCompletionStatus(...);
四。完整的簡單的IOCP服務器與客戶端代碼實例:
[cpp] view plaincopyprint?
1. // IOCP_TCPIP_Socket_Server.cpp
2.
3. #include <windows.h>
4. #include <iostream>
5. #include <winsock2.h>
6. #include <stdio.h>
7.
8. using namespace std;
9.
10. #pragma comment(lib, "Ws2_32.lib") // Socket編程需用的動態鏈接庫
11. #pragma comment(lib, "Kernel32.lib") // IOCP需要用到的動態鏈接庫
12.
13.
17. const int DataBuffSize = 2 * 1024;
18. typedef struct
19. {
20. OVERLAPPED overlapped;
21. WSABUF databuff;
22. char buffer[ DataBuffSize ];
23. int BufferLen;
24. int operationType;
25. }PER_IO_OPERATEION_DATA, *LPPER_IO_OPERATION_DATA, *LPPER_IO_DATA, PER_IO_DATA;
26.
27.
32. typedef struct
33. {
34. SOCKET socket;
35. SOCKADDR_STORAGE ClientAddr;
36. }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
37.
38. // 定義全局變量
39. const int DefaultPort = 6000;
40. vector < PER_HANDLE_DATA* > clientGroup; // 記錄客戶端的向量組
41.
42. HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
43. DWORD WINAPI ServerWorkThread(LPVOID CompletionPortID);
44. DWORD WINAPI ServerSendThread(LPVOID IpParam);
45.
46. // 開始主函數
47. int main()
48. {
49. // 加載socket動態鏈接庫
50. WORD wVersionRequested = MAKEWORD(2, 2); // 請求2.2版本的WinSock庫
51. WSADATA wsaData; // 接收Windows Socket的結構信息
52. DWORD err = WSAStartup(wVersionRequested, &wsaData);
53.
54. if (0 != err){ // 檢查套接字庫是否申請成功
55. cerr << "Request Windows Socket Library Error!\n";
56. system("pause");
57. return -1;
58. }
59. if(LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){// 檢查是否申請了所需版本的套接字庫
60. WSACleanup();
61. cerr << "Request Windows Socket Version 2.2 Error!\n";
62. system("pause");
63. return -1;
64. }
65.
66. // 創建IOCP的內核對象
67.
76. HANDLE completionPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);
77. if (NULL == completionPort){ // 創建IO內核對象失敗
78. cerr << "CreateIoCompletionPort failed. Error:" << GetLastError() << endl;
79. system("pause");
80. return -1;
81. }
82.
83. // 創建IOCP線程--線程里面創建線程池
84.
85. // 確定處理器的核心數量
86. SYSTEM_INFO mySysInfo;
87. GetSystemInfo(&mySysInfo);
88.
89. // 基于處理器的核心數量創建線程
90. for(DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i){
91. // 創建服務器工作器線程,并將完成端口傳遞到該線程
92. HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, completionPort, 0, NULL);
93. if(NULL == ThreadHandle){
94. cerr << "Create Thread Handle failed. Error:" << GetLastError() << endl;
95. system("pause");
96. return -1;
97. }
98. CloseHandle(ThreadHandle);
99. }
100.
101. // 建立流式套接字
102. SOCKET srvSocket = socket(AF_INET, SOCK_STREAM, 0);
103.
104. // 綁定SOCKET到本機
105. SOCKADDR_IN srvAddr;
106. srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
107. srvAddr.sin_family = AF_INET;
108. srvAddr.sin_port = htons(DefaultPort);
109. int bindResult = bind(srvSocket, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
110. if(SOCKET_ERROR == bindResult){
111. cerr << "Bind failed. Error:" << GetLastError() << endl;
112. system("pause");
113. return -1;
114. }
115.
116. // 將SOCKET設置為監聽模式
117. int listenResult = listen(srvSocket, 10);
118. if(SOCKET_ERROR == listenResult){
119. cerr << "Listen failed. Error: " << GetLastError() << endl;
120. system("pause");
121. return -1;
122. }
123.
124. // 開始處理IO數據
125. cout << "本服務器已準備就緒,正在等待客戶端的接入...\n";
126.
127. // 創建用于發送數據的線程
128. HANDLE sendThread = CreateThread(NULL, 0, ServerSendThread, 0, 0, NULL);
129.
130. while(true){
131. PER_HANDLE_DATA * PerHandleData = NULL;
132. SOCKADDR_IN saRemote;
133. int RemoteLen;
134. SOCKET acceptSocket;
135.
136. // 接收連接,并分配完成端,這兒可以用AcceptEx()
137. RemoteLen = sizeof(saRemote);
138. acceptSocket = accept(srvSocket, (SOCKADDR*)&saRemote, &RemoteLen);
139. if(SOCKET_ERROR == acceptSocket){ // 接收客戶端失敗
140. cerr << "Accept Socket Error: " << GetLastError() << endl;
141. system("pause");
142. return -1;
143. }
144.
145. // 創建用來和套接字關聯的單句柄數據信息結構
146. PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); // 在堆中為這個PerHandleData申請指定大小的內存
147. PerHandleData -> socket = acceptSocket;
148. memcpy (&PerHandleData -> ClientAddr, &saRemote, RemoteLen);
149. clientGroup.push_back(PerHandleData); // 將單個客戶端數據指針放到客戶端組中
150.
151. // 將接受套接字和完成端口關聯
152. CreateIoCompletionPort((HANDLE)(PerHandleData -> socket), completionPort, (DWORD)PerHandleData, 0);
153.
154.
155. // 開始在接受套接字上處理I/O使用重疊I/O機制
156. // 在新建的套接字上投遞一個或多個異步
157. // WSARecv或WSASend請求,這些I/O請求完成后,工作者線程會為I/O請求提供服務
158. // 單I/O操作數據(I/O重疊)
159. LPPER_IO_OPERATION_DATA PerIoData = NULL;
160. PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATEION_DATA));
161. ZeroMemory(&(PerIoData -> overlapped), sizeof(OVERLAPPED));
162. PerIoData->databuff.len = 1024;
163. PerIoData->databuff.buf = PerIoData->buffer;
164. PerIoData->operationType = 0; // read
165.
166. DWORD RecvBytes;
167. DWORD Flags = 0;
168. WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);
169. }
170.
171. system("pause");
172. return 0;
173. }
174.
175. // 開始服務工作線程函數
176. DWORD WINAPI ServerWorkThread(LPVOID IpParam)
177. {
178. HANDLE CompletionPort = (HANDLE)IpParam;
179. DWORD BytesTransferred;
180. LPOVERLAPPED IpOverlapped;
181. LPPER_HANDLE_DATA PerHandleData = NULL;
182. LPPER_IO_DATA PerIoData = NULL;
183. DWORD RecvBytes;
184. DWORD Flags = 0;
185. BOOL bRet = false;
186.
187. while(true){
188. bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);
189. if(bRet == 0){
190. cerr << "GetQueuedCompletionStatus Error: " << GetLastError() << endl;
191. return -1;
192. }
193. PerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(IpOverlapped, PER_IO_DATA, overlapped);
194.
195. // 檢查在套接字上是否有錯誤發生
196. if(0 == BytesTransferred){
197. closesocket(PerHandleData->socket);
198. GlobalFree(PerHandleData);
199. GlobalFree(PerIoData);
200. continue;
201. }
202.
203. // 開始數據處理,接收來自客戶端的數據
204. WaitForSingleObject(hMutex,INFINITE);
205. cout << "A Client says: " << PerIoData->databuff.buf << endl;
206. ReleaseMutex(hMutex);
207.
208. // 為下一個重疊調用建立單I/O操作數據
209. ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED)); // 清空內存
210. PerIoData->databuff.len = 1024;
211. PerIoData->databuff.buf = PerIoData->buffer;
212. PerIoData->operationType = 0; // read
213. WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);
214. }
215.
216. return 0;
217. }
218.
219.
220. // 發送信息的線程執行函數
221. DWORD WINAPI ServerSendThread(LPVOID IpParam)
222. {
223. while(1){
224. char talk[200];
225. gets(talk);
226. int len;
227. for (len = 0; talk[len] != '>227. for (len = 0; talk[len] != '\0'; ++len){<'; ++len){
228. // 找出這個字符組的長度
229. }
230. talk[len] = '\n';
231. talk[++len] = '>231. talk[++len] = '\0';<';
232. printf("I Say:");
233. cout << talk;
234. WaitForSingleObject(hMutex,INFINITE);
235. for(int i = 0; i < clientGroup.size(); ++i){
236. send(clientGroup[i]->socket, talk, 200, 0); // 發送信息
237. }
238. ReleaseMutex(hMutex);
239. }
240. return 0;
241. }
[cpp] view plaincopyprint?
1. // IOCP_TCPIP_Socket_Client.cpp
2.
3. #include <windows.h>
4. #include <winsock2.h>
5. #include <iostream>
6. #include <stdio.h>
10. using namespace std;
11.
12. #pragma comment(lib, "Ws2_32.lib") // Socket編程需用的動態鏈接庫
13.
14. SOCKET sockClient; // 連接成功后的套接字
15. HANDLE bufferMutex; // 令其能互斥成功正常通信的信號量句柄
16. const int DefaultPort = 6000;
17.
18. int main()
19. {
20. // 加載socket動態鏈接庫(dll)
21. WORD wVersionRequested;
22. WSADATA wsaData; // 這結構是用于接收Wjndows Socket的結構信息的
23. wVersionRequested = MAKEWORD( 2, 2 ); // 請求2.2版本的WinSock庫
24. int err = WSAStartup( wVersionRequested, &wsaData );
25. if ( err != 0 ) { // 返回值為零的時候是表示成功申請WSAStartup
26. return -1;
27. }
28. if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 ) { // 檢查版本號是否正確
29. WSACleanup( );
30. return -1;
31. }
32.
33. // 創建socket操作,建立流式套接字,返回套接字號sockClient
34. sockClient = socket(AF_INET, SOCK_STREAM, 0);
35. if(sockClient == INVALID_SOCKET) {
36. printf("Error at socket():%ld\n", WSAGetLastError());
37. WSACleanup();
38. return -1;
39. }
40.
41. // 將套接字sockClient與遠程主機相連
42. // int connect( SOCKET s, const struct sockaddr* name, int namelen);
43. // 第一個參數:需要進行連接操作的套接字
44. // 第二個參數:設定所需要連接的地址信息
45. // 第三個參數:地址的長度
46. SOCKADDR_IN addrSrv;
47. addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 本地回路地址是127.0.0.1;
48. addrSrv.sin_family = AF_INET;
49. addrSrv.sin_port = htons(DefaultPort);
50. while(SOCKET_ERROR == connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR))){
51. // 如果還沒連接上服務器則要求重連
52. cout << "服務器連接失敗,是否重新連接?(Y/N):";
53. char choice;
54. while(cin >> choice && (!((choice != 'Y' && choice == 'N') || (choice == 'Y' && choice != 'N')))){
55. cout << "輸入錯誤,請重新輸入:";
56. cin.sync();
57. cin.clear();
58. }
59. if (choice == 'Y'){
60. continue;
61. }
62. else{
63. cout << "退出系統中...";
64. system("pause");
65. return 0;
66. }
67. }
68. cin.sync();
69. cout << "本客戶端已準備就緒,用戶可直接輸入文字向服務器反饋信息。\n";
70.
71. send(sockClient, "\nAttention: A Client has enter...\n", 200, 0);
72.
73. bufferMutex = CreateSemaphore(NULL, 1, 1, NULL);
74.
75. DWORD WINAPI SendMessageThread(LPVOID IpParameter);
76. DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter);
77.
78. HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);
79. HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL);
80.
81.
82. WaitForSingleObject(sendThread, INFINITE); // 等待線程結束
83. closesocket(sockClient);
84. CloseHandle(sendThread);
85. CloseHandle(receiveThread);
86. CloseHandle(bufferMutex);
87. WSACleanup(); // 終止對套接字庫的使用
88.
89. printf("End linking...\n");
90. printf("\n");
91. system("pause");
92. return 0;
93. }
94.
95.
96. DWORD WINAPI SendMessageThread(LPVOID IpParameter)
97. {
98. while(1){
99. string talk;
100. getline(cin, talk);
101. WaitForSingleObject(bufferMutex, INFINITE); // P(資源未被占用)
102. if("quit" == talk){
103. talk.push_back('>103. talk.push_back('\0');<');
104. send(sockClient, talk.c_str(), 200, 0);
105. break;
106. }
107. else{
108. talk.append("\n");
109. }
110. printf("\nI Say:("quit"to exit):");
111. cout << talk;
112. send(sockClient, talk.c_str(), 200, 0); // 發送信息
113. ReleaseSemaphore(bufferMutex, 1, NULL); // V(資源占用完畢)
114. }
115. return 0;
116. }
117.
118.
119. DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter)
120. {
121. while(1){
122. char recvBuf[300];
123. recv(sockClient, recvBuf, 200, 0);
124. WaitForSingleObject(bufferMutex, INFINITE); // P(資源未被占用)
125.
126. printf("%s Says: %s", "Server", recvBuf); // 接收信息
127.
128. ReleaseSemaphore(bufferMutex, 1, NULL); // V(資源占用完畢)
129. }
130. return 0;
131. }
這篇教程主要是告訴大家如何利用TCP和HTTP協議來完成網站的搭建。
首先你需要有C/C++語言基礎,且有服務器、客戶端概念,如果你了解TCP或者HTTP協議的話,那么將會幫助你更快的學會如何搭建個人網站。
該服務器使用的Windows中的IOCP模式來進行,我將put代碼中最為重要的幾個部分
1.準備好HTML文件,也就是你所要發布的網頁,可以是靜態網頁也可以是動態網頁,為了更好的幫助大家入門,我準備了一個最為簡便的html代碼如下:
<!DOCTYPE html>
<html lang="ch-ZH">
<head>
<meta charset="UTF-8">
<title>My Website</title>
<link rel="icon" href="img/sad.png">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div style="width: 100px;height:100px;">
</div>
<span>HelloWorld</span>
<div style="background-color: blue; border: 1px solid orange">
Hi.Im david
</div>
</body>
</html>
123456789101112131415161718192021
你可以直接復制該文件并保存為html形式,文件名保存為 index.html。
如果你不關系如何去自己書寫代碼完成搭建個人網站的,請點擊這里。
2.開始正式的編程。 我假設你有TCP/IP協議的相關知識,故不做太多介紹。
a)首先我們配置開發環境,如果你使用的VS的話,那么首先你需要幾步來完成網絡通信的環境搭建
1.) 創建項目并打開項目屬性。轉到 鏈接器中的輸入選項之中:在附加依賴項之中添加 ws2_32.lib; 該庫為網絡通信庫
2.) 引入頭文件 #include <WinSock2.h> 和 #include<Windows.h>。 注意: <Windows.h>必須在<WinSock2.h>之后。否則會出現重定義錯誤
b) 創建套接字過程代碼
#include <WinSock2.h>
#include <Windows.h>
int main(){
WSADATA wsaData; //在Windows之中創建套接字需要使用到的對象
HANDLE hComPort; //完成端口CP對象
SYSTEM_INFO sysInfo; //獲取系統信息
WSAEVENT wEvent; //重疊事件
/// 關于客戶端的信息與io相關的信息一定要是動態分配的。否則在傳遞給其他線程的時候將會引用原本的地址且無法重新分配新的客戶端與io數據
LPCLNTINFO pClntInfo; //保存客戶端信息
LP_IO_DATA pIoData; //保存著io相關的數據
SOCKET servSock, clntSock; //服務器與客戶端套接字
SOCKADDR_IN servAddr, clntAddr; //服務器與客戶端地址
int clntAddrSz; //客戶端地址長度
DWORD recvBytes = 0,flag = 0; //接收的數據大小和標志
int port = 0; //端口號
// std::ofstream connectLog("connectLog.txt"); //保存客戶端連接信息
//這是一個處理輸入的函數,我們將會在之后講到。
getPortNumber(port); //處理輸入數據并獲取端口號。
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NULL)
ErrorMsg("WSAStartup() error");
///創建完后端口號。
hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
GetSystemInfo(&sysInfo); //獲取系統信息
///創建多個線程來分離IO處理
for (int i = 0; i < sysInfo.dwNumberOfProcessors; ++i)
_beginthreadex(NULL, 0, ClntHandle, (LPVOID)hComPort, 0, NULL); //開始線程處理。
servSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); //創建重疊套接字
///錯誤處理
if (servSock == INVALID_SOCKET)
ErrorMsg("WSASocket() Error");
///初始化IP地址和端口號。
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET; //IPV4協議
servAddr.sin_port = htons(port); //小端序轉位網路大端序
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自動獲取本地IP地址
///綁定服務器地址信息
if (bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorMsg("bind() error");
///監聽來自客戶端的連接請求。 500表示最大接代500個客戶端同時請求。
if (listen(servSock, 500) == SOCKET_ERROR)
ErrorMsg("listen() error");
clntAddrSz = sizeof(clntAddr); //獲取客戶端地址結構的大小
std::cout << std::left; //設置左對齊
處理來自客戶端的連接請求
while (1)
{
clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &clntAddrSz);
wEvent = WSACreateEvent(); //存放事件句柄
if (clntSock == INVALID_SOCKET) //處理連接請求出錯
{
ErrorMsg("accept() error");
}
///動態分配內存處理客戶端的連接
pClntInfo = (LPCLNTINFO)malloc(sizeof(CLNTINFO));
pClntInfo->hClntSock = clntSock;
memcpy(&(pClntInfo->hClntAddr), &clntAddr, clntAddrSz); //復制客戶端套接字的地址信息
/// 建立套接字到完后端口的連接
CreateIoCompletionPort((HANDLE)clntSock, hComPort, (DWORD)pClntInfo, 0); //傳遞的四clntInfo整個結構的地址。之后可以提取出來。
/// 動態分配保存著數據傳輸信息的IO對象
pIoData = (LP_IO_DATA)malloc(sizeof(IO_DATA));
memset(&(pIoData->overlapped), 0, sizeof(OVERLAPPED));
pIoData->wsaBuf.buf = pIoData->buf;
pIoData->wsaBuf.len = BUF_SIZE;
///接收客戶端信息
//當客戶端未向服務器發送數據的時候,此時可以判定,客戶端需要請求服務器的數據
if (WSARecv(clntSock, &(pIoData->wsaBuf), 1, (LPDWORD)&recvBytes, (LPDWORD)&flag, &(pIoData->overlapped), NULL) == SOCKET_ERROR) //注意此處傳遞的是ioData的整個結構地址
{
// if (WSAGetLastError() == WSA_IO_PENDING) //數據仍在接收中
// {
// std::cout << "數據仍在接收中..." << std::endl;
// }
}
}
closesocket(servSock);
WSACleanup();
return 0;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
其中相關數據結構如下:
/// 利用了結構指針的地址就是結構首成員地址該點來傳遞IO端口完成信息
typedef struct
{
SOCKET hClntSock; //客戶端套接字
SOCKADDR_IN hClntAddr; //客戶端IP地址
}CLNTINFO, *LPCLNTINFO; //保存著客戶端套接字屬性
typedef struct
{
OVERLAPPED overlapped; //重疊屬性結構
WSABUF wsaBuf; //存放緩沖數據的結構 主要存放了 待傳輸數據的大小和緩沖地址值
char buf[BUF_SIZE]; //存放數據的緩沖區
}IO_DATA, *LP_IO_DATA; //保存著IO相關的數據
12345678910111213
之前 提到的getPortNumber函數。 使用的C++實現輸入,如果你對C語言有了解可以換為C代碼模式。需要注意的是,我的代碼為C/C++共同實現。 該函數主要是保證用戶輸入流是正確的。
void getPortNumber(int & port)
{
///輸入端口號進行監聽
std::cout << "輸入有效的監聽端口號:";
///確保輸入了有效的換行符
while (!(std::cin >> port))
{
std::cin.clear(); //將cin中的錯誤位置位
std::cin.ignore(224, '\n'); //輸入有誤時忽略輸入流中的緩沖信息.忽略最多224個字符,直到遇到了換行符為止
std::cout << "輸入的端口號有誤,重新輸入:";
}
}
12345678910111213
接下來需要分別講解在main函數之中調用的各個函數:
/// 建立套接字到完后端口的連接
CreateIoCompletionPort((HANDLE)clntSock, hComPort, (DWORD)pClntInfo, 0);
12
該函數是一個關鍵點,主要是將連接進入的客戶端與我們的輸入輸出完成端口進行綁定。
之后該套接字存在事件時,將會轉到我們的線程處理函數之中。
開始接收來自客戶端的請求數據。使用的是重疊IO無阻塞模式。
WSARecv(clntSock, &(pIoData->wsaBuf), 1, (LPDWORD)&recvBytes, (LPDWORD)&flag, &(pIoData->overlapped), NULL);
1
之后每次當客戶端發出請求數據,都會在以下函數中處理
unsigned WINAPI ClntHandle(LPVOID cp)
{
DWORD recvBytes = 0, flag = 0;
LP_IO_DATA pIoData; //保存著IO對象的數據。 調用WSARecv傳遞的overlapped結構
LPCLNTINFO pClntInfo; //保存客戶端的相關信息。調用CreateIoCompletionPort傳遞的第三個參數
SOCKET sock; //主要指的是客戶端的套接字
HANDLE hComPort = (HANDLE)cp; //CP對象
while (1)
{
///確定IO完成狀態
if (!GetQueuedCompletionStatus(hComPort, (LPDWORD)&recvBytes, (LPDWORD)&pClntInfo, (LPOVERLAPPED*)&pIoData, INFINITE))
{
///在客戶端請求數據的過程中如果退出了請求,那么此時該函數則會出現錯誤,那么我們需要從錯誤中恢復過來
std::cout << GetLastError() << std::endl;
closesocket(pClntInfo->hClntSock); //關閉該套接字。
FreeData(&pIoData, &pClntInfo); //釋放動態分配的內存。
continue;
}
///插入連接的客戶端 IP地址信息
connectCnt[inet_ntoa(pClntInfo->hClntAddr.sin_addr)]++;
sock = pClntInfo->hClntSock;
pIoData->wsaBuf.buf[recvBytes] = 0;
if (recvBytes == 0) //接收數據為0的時候,此時沒有接收到數據
{
closesocket(sock);
FreeData(&pIoData, &pClntInfo);
continue;
}
// std::cout << "開始接收客戶端信息:" << pIoData->wsaBuf.buf << std::endl;
std::string fileName; //文件名
std::string compleHead(pIoData->wsaBuf.buf, 100); //部分頭信息。最多獲取100個字符
char cntType[SMALL_SIZE];
int mFind = compleHead.find('/'); //查找請求方式
int fFind = compleHead.find("HTTP/"); //查找文件名
///1. 當非GET請求,不理會。 2.當非HTTP/發出的請求不理會 3.當請求格式錯誤,不理會
if (mFind == std::string::npos || fFind == std::string::npos
|| std::string(compleHead, 0, mFind - 1) != "GET") //請求方式錯誤
{
closesocket(sock);
ExcptionRequst(&(pClntInfo->hClntAddr), pIoData->wsaBuf.buf); //發生異常連接 寫入文件日志中
FreeData(&pIoData, &pClntInfo);
continue;
}
///獲取完整文件名 在推斷了多種可能的情況下,此種情況下獲取文件名應該是安全的
fileName = std::string(compleHead.cbegin() + mFind + 1, compleHead.cbegin() + fFind - 1);
if (fileName.empty()) //當文件名為空,表示請求的是首頁
{
fileName = "index.html";
strcpy_s(cntType, "text/html");
}
else
{
char * cs = GetContentType(fileName);
if (!cs) //返回的指針為空的時候表示文件不存在后綴
{
ErrorFile(sock);
FreeData(&pIoData, &pClntInfo);
continue;
}
strcpy_s(cntType, cs); //復制請求文件類型
}
///給客戶端發送數據
SendDataFile(sock, fileName.c_str(), cntType, pIoData);
///釋放內存
closesocket(sock);
FreeData(&pIoData, &pClntInfo);
// std::cout << "網頁數據發送完成..." << std::endl;
}
return 0;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
其中該函數使用到一個非常重要的函數如下:該函數負責向客戶端發送請求文件
int SendDataFile(SOCKET sock, const char * fileName, const char *contType, LP_IO_DATA &pIoData)
{
std::ifstream inFile(fileName, std::fstream::binary); //以讀的方式打開文件
if (!inFile) //請求的文件不存在時
{
ErrorFile(sock);
return -1;
}
///傳輸回應頭信息
//\r是回車(Carriage return) \n是換行 (New line)
char protocol[] = "HTTP/1.1 200 OK\r\n"; //狀態
char servName[] = "Server: TD Web server\r\n"; //服務器名字
char cntEncode[] = "Content-Encoding: gzip\r\n"; //壓縮方式
char transEncode[] = "Transfer-Encoding: chunked\r\n"; //傳輸編碼
char vary[] = "Vary: Accept-Encoding\r\n"; //接受編碼
char cntType[SMALL_SIZE]; //接收類型
char buf[BUF_SIZE]; //數據
char end[] = "\r\n"; //結束符
char cntLen[SMALL_SIZE]; //內容長度
WSABUF wsaBuf; //存放數據緩沖
DWORD sendBytes = 0;
//WSAEVENT wsaEvent = WSACreateEvent();
sprintf_s(cntType, "Content-type:%s\r\n", contType);
///向客戶端發送回應頭信息
send(sock, protocol, strlen(protocol), 0); //傳遞狀態行
send(sock, servName, strlen(servName), 0); //傳遞消息頭的服務端名
send(sock, cntType, strlen(cntType), 0); //傳遞content-type
/// 獲取文件長度. 當第一次發送一個文件的時候,計算出該文件的大小值,之后每次只需要提取該文件對應的大小即可。
if (fileSize.find(fileName) == fileSize.cend())
{
fileSize[fileName] = MyGetFileSize(fileName);
}
LARGE_INTEGER size = fileSize[fileName];
///向客戶端發送文件大小長度
sprintf_s(cntLen, "Content-length:%lld\r\n", size.QuadPart);
send(sock, cntLen, strlen(cntLen), 0);
send(sock, end, strlen(end), 0);
wsaBuf.buf = buf;
wsaBuf.len = BUF_SIZE;
///讀取請求文件的數據并傳遞給客戶端
do
{
inFile.read(wsaBuf.buf, BUF_SIZE - 1);
int cnt = inFile.gcount();
wsaBuf.buf[cnt] = 0;
send(sock, buf, cnt, 0);
} while (!inFile.eof());
inFile.close(); //關閉文件
return 0;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
該函數非常重要,注意觀察該函數中的回應頭信息,頭信息必須按照HTTP協議的固定格式進行發送。
其中又牽扯到了其他幾個函數。分別是獲取文件的大小信息
ARGE_INTEGER MyGetFileSize(const char * fileName)
{
///創建文件句柄并依此來獲取到文件的大小信息。 對于操作系統來講,每次創建句柄和調用系統函數將會存在額外的開銷,絕對會影響性能
HANDLE hFile = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LARGE_INTEGER size;
::GetFileSizeEx(hFile, &size); //獲取文件大小
return size;
}
12345678910
獲取請求文件的格式
char * GetContentType(const std::string& fileName)
{
///通過string的反向查找函數首先查找.符號
size_t index = fileName.find_last_of('.');
if (index == std::string::npos || index == fileName.size()) //請求的文件無后綴名的話 返回一個空指針
return nullptr;
std::string s(fileName, index + 1); //如果存在后綴的話,那么則檢查后綴
///通過后綴名來進行判斷請求數據的類型
std::string type = dataType[s];
if (type.empty())
return "text/plain"; //如果在關聯容器中沒有找到相對應的類型
///返回轉換為char*的類型
static char str[20];
strcpy_s(str, type.c_str());
return str;
}
12345678910111213141516171819202122
當然其中也牽扯到其他的結構。這里并不一一舉例,如果需要請發郵件給我。
當你完成基本的結構的時候,則可以測試你的程序。 服務器輸入 80 端口進行監聽。 此時打開瀏覽器。輸入 localhost并回車
此時你的控制臺將會打印出連接的客戶信息。當然這是在你實現了該項功能之后。 但是最起碼你在瀏覽器中看到你的網頁了。
如果你希望能夠讓其他人也能夠看到你網頁,你可以嘗試如下方法:
如果你不想編寫自己的服務器代碼,而只想要put自己的網站讓其他人訪問,你可以使用如下軟件:
當你完成這些之后,那么恭喜你,你的網站應該可以正常被訪問了! 那么豐富你的網站讓更多人看到吧