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. }
前言:由于學習網絡編程時間不長,對各個網絡編程函數,不是很熟悉,也記得不夠精確。每次都查半天,經常煩惱于此。索性把它們都記錄下來,便于自己記憶以及日后查閱、回顧。主要介紹:socket、bind、listen、connect、accept、send、recv、close。
在 Linux 下使用 <sys/socket.h> 頭文件中 socket() 函數來創建套接字,原型為:
int socket(int af, int type, int protocol);
1) af 為地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫,INET是“Inetnet”的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址。
注意:也可以使用 PF(Protocol Family的簡寫) 前綴,它和 AF 是一樣的。例如,PF_INET 等價于 AF_INET,PF_INET6 等價于 AF_INET6。
2) type 為數據傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字/面向連接的套接字) 和 SOCK_DGRAM(數據報套接字/無連接的套接字)。
3) protocol 表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。
4) 返回值,-1表示出錯,非負表示成功,返回值為socket文件描述符。
有了地址類型和數據傳輸方式,還不足以決定采用哪種協議嗎?為什么還需要第三個參數呢?
一般情況下有了 af 和 type 兩個參數就可以創建套接字了,操作系統會自動推演出協議類型,除非遇到這樣的情況:有兩種不同的協議支持同一種地址類型和數據傳輸類型。如果我們不指明使用哪種協議,操作系統就沒辦法自動推演了。
如果使用 IPv4 地址,參數 af 的值就應設為 AF_INET,此時如果使用 SOCK_STREAM 傳輸數據,那么滿足這兩個條件的協議只有 TCP,因此可以這樣來調用 socket() 函數:
int tcp_socket=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP協議
這種套接字稱為 TCP 套接字。
如果使用 SOCK_DGRAM 傳輸方式,那么滿足這兩個條件的協議只有 UDP,因此可以這樣來調用 socket() 函數:
int udp_socket=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP協議
這種套接字稱為 UDP 套接字。
更多linux內核視頻教程文檔資料免費領取后臺私信【內核】自行獲取。
上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設為 0,系統會自動推演出應該使用什么協議,如下所示:
int tcp_socket=socket(AF_INET, SOCK_STREAM, 0); //創建TCP套接字
int udp_socket=socket(AF_INET, SOCK_DGRAM, 0); //創建UDP套接字
bind() 函數的原型為:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
下面介紹Linux下的bind函數,Windows 與此類似。
sock 為 socket 文件描述符,addr 為 sockaddr 結構體變量的指針,addrlen 為 addr 變量的大小,可由 sizeof() 計算得出。失敗返回-1,成功返回非0。
下面的代碼,將創建的套接字與IP地址 127.0.0.1、端口7777 綁定:
int listenfd=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //創建套接字
if (listenfd==-1)
{
printf("create socket failed.\n");
return -1;
}
struct sockaddr_in serv_addr; //創建sockaddr_in結構體變量
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充
serv_addr.sin_family=AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port=htons(7777); //端口7777
if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) //將套接字和IP、端口綁定
{
printf("bind socket failed.\n");
return -1;
}
這里使用 sockaddr_in 結構體,然后再強制轉換為 sockaddr 類型,因為sockaddr_in 結構體賦值比較方便,大小與sockaddr相等。serv_addr.sin_addr.s_addr 也可以賦值,serv_addr.sin_addr.s_addr=htonl(INADDR_ANY),這樣就能監聽服務器所有IP了。
sockaddr_in 結構體
接下來不妨先看一下 sockaddr_in 結構體,它的成員變量如下:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
uint16_t sin_port; //16位的端口號
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
1) sin_family 和 socket() 的第一個參數的含義相同,取值也要保持一致。
2) sin_prot 為端口號。uint16_t 的長度為兩個字節,理論上端口號的取值范圍為 0~65536,但 0~1023 的端口一般由系統分配給特定的服務程序,例如 Web 服務的端口號為 80,FTP 服務的端口號為 21,所以我們的程序要盡量在1024~65536 之間分配端口號。
端口號需要用 htons() 函數轉換。
3) sin_addr 是 struct in_addr 結構體類型的變量,下面會詳細講解。
4) sin_zero[8] 是多余的8個字節,沒有用,一般使用 memset() 函數填充為 0。上面的代碼中,先用 memset() 將結構體的全部字節填充為 0,再給前3個成員賦值,剩下的 sin_zero 自然就是 0 了。
in_addr 結構體
sockaddr_in 的第3個成員是 in_addr 類型的結構體,該結構體只包含一個成員,如下所示:
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};
in_addr_t 在頭文件 <netinet/in.h> 中定義,等價于 unsigned long,長度為4個字節。也就是說,s_addr 是一個整數,而IP地址是一個字符串,所以需要 inet_addr() 函數進行轉換,例如:
unsigned long ip=inet_addr("127.0.0.1");
printf("%ld\n", ip);
運行結果:
16777343
為什么要搞這么復雜,結構體中嵌套結構體,而不用 sockaddr_in 的一個成員變量來指明IP地址呢?socket() 函數的第一個參數已經指明了地址類型,為什么在 sockaddr_in 結構體中還要再說明一次呢,這不是啰嗦嗎?
這些繁瑣的細節確實給初學者帶來了一定的障礙,我想,這或許是歷史原因吧,后面的接口總要兼容前面的代碼。
為什么使用 sockaddr_in 而不使用 sockaddr
bind() 第二個參數的類型為 sockaddr,而代碼中卻使用 sockaddr_in,然后再強制轉換為 sockaddr,這是為什么呢?
sockaddr 結構體的定義如下:
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
char sa_data[14]; //IP地址和端口號
};
下圖是 sockaddr 與 sockaddr_in 的對比(括號中的數字表示所占用的字節數):
sockaddr 和 sockaddr_in 的長度相同,都是16字節,只是將IP地址和端口號合并到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和端口號,例如”127.0.0.1:80“,遺憾的是,沒有相關函數將這個字符串轉換成需要的形式,也就很難給 sockaddr 類型的變量賦值,所以使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換類型時不會丟失字節,也沒有多余的字節。
可以認為,sockaddr 是一種通用的結構體,用來保存多種類型的IP地址和端口號,而 sockaddr_in 是專門用來保存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來保存 IPv6 地址,它的定義如下:
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址類型,取值為AF_INET6
in_port_t sin6_port; //(2)16位端口號
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具體的IPv6地址
uint32_t sin6_scope_id; //(4)接口范圍ID
};
正是由于通用結構體 sockaddr 使用不便,才針對不同的地址類型定義了不同的結構體。
import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import warnings warnings.filterwarnings('ignore') import ssl ssl._create_default_https_context=ssl._create_unverified_context
connect() 函數用來建立連接,它的原型為:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen); //Windows
通過 listen() 函數可以讓套接字進入被動監聽狀態,它的原型為:
int listen(int sock, int backlog); //Linux
int listen(SOCKET sock, int backlog); //Windows
sock 為需要進入監聽狀態的套接字,backlog 為請求隊列的最大長度,即最大監聽數量。
所謂被動監聽,是指當沒有客戶端請求時,套接字處于“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。
當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩沖區,待當前請求處理完畢后,再從緩沖區中讀取出來處理。如果不斷有新的請求進來,它們就按照先后順序在緩沖區中排隊,直到緩沖區滿。這個緩沖區,就稱為請求隊列(Request Queue)。
緩沖區的長度(能存放多少個客戶端請求)可以通過 listen() 函數的 backlog 參數指定,但究竟為多少并沒有什么標準,可以根據你的需求來定,并發量小的話可以是10或者20。
如果將 backlog 的值設置為 SOMAXCONN,就由系統來決定請求隊列長度,這個值一般比較大,可能是幾百,或者更多。
當請求隊列滿時,就不再接收新的請求,對于 Linux,客戶端會收到 ECONNREFUSED 錯誤,對于 Windows,客戶端會收到 WSAECONNREFUSED 錯誤。
注意:listen() 只是讓套接字處于監聽狀態,并沒有接收請求。接收請求需要使用 accept() 函數。
當套接字處于監聽狀態時,可以通過 accept() 函數來接收客戶端請求。它的原型為:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows
它的參數與 listen() 和 connect() 是相同的:sock 為服務器端套接字,addr 為 sockaddr_in 結構體變量,addrlen 為參數 addr 的長度,可由 sizeof() 求得。
accept() 返回一個新的套接字來和客戶端通信,addr 保存了客戶端的IP地址和端口號,而 sock 是服務器端的套接字,大家注意區分。后面和客戶端通信時,要使用這個新生成的套接字,而不是原來服務器端的套接字。
最后需要說明的是:listen() 只是讓套接字進入監聽狀態,并沒有真正接收客戶端請求,listen() 后面的代碼會繼續執行,直到遇到 accept()。accept() 會阻塞程序執行(后面代碼不能被執行),直到有新的請求到來。
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
1) send先比較發送數據的長度nbytes和套接字sockfd的發送緩沖區的長度,如果nbytes > 套接字sockfd的發送緩沖區的長度, 該函數返回SOCKET_ERROR;
2) 如果nbtyes <=套接字sockfd的發送緩沖區的長度,那么send先檢查協議是否正在發送sockfd的發送緩沖區中的數據,如果是就等待協議把數據發送完,如果協議還沒有開始發送sockfd的發送緩沖區中的數據或者sockfd的發送緩沖區中沒有數據,那么send就比較sockfd的發送緩沖區的剩余空間和nbytes
3) 如果 nbytes > 套接字sockfd的發送緩沖區剩余空間的長度,send就一起等待協議把套接字sockfd的發送緩沖區中的數據發送完
4) 如果 nbytes < 套接字sockfd的發送緩沖區剩余空間大小,send就僅僅把buf中的數據copy到剩余空間里(注意并不是send把套接字sockfd的發送緩沖區中的數據傳到連接的另一端的,而是協議傳送的,send僅僅是把buf中的數據copy到套接字sockfd的發送緩沖區的剩余空間里)。
5) 如果send函數copy成功,就返回實際copy的字節數,如果send在copy數據時出現錯誤,那么send就返回SOCKET_ERROR; 如果在等待協議傳送數據時網絡斷開,send函數也返回SOCKET_ERROR。
6) send函數把buff中的數據成功copy到sockfd的改善緩沖區的剩余空間后它就返回了,但是此時這些數據并不一定馬上被傳到連接的另一端。如果協議在后續的傳送過程中出現網絡錯誤的話,那么下一個socket函數就會返回SOCKET_ERROR。(每一個除send的socket函數在執行的最開始總要先等待套接字的發送緩沖區中的數據被協議傳遞完畢才能繼續,如果在等待時出現網絡錯誤那么該socket函數就返回SOCKET_ERROR)
7) 在unix系統下,如果send在等待協議傳送數據時網絡斷開,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的處理是進程終止。
不論是客戶還是服務器應用程序都用send函數來向TCP連接的另一端發送數據??蛻舫绦蛞话阌胹end函數向服務器發送請求,而服務器則通常用send函數來向客戶程序發送應答。
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
1) recv先等待s的發送緩沖區的數據被協議傳送完畢,如果協議在傳送sock的發送緩沖區中的數據時出現網絡錯誤,那么recv函數返回SOCKET_ERROR
2) 如果套接字sockfd的發送緩沖區中沒有數據或者數據被協議成功發送完畢后,recv先檢查套接字sockfd的接收緩沖區,如果sockfd的接收緩沖區中沒有數據或者協議正在接收數據,那么recv就一起等待,直到把數據接收完畢。當協議把數據接收完畢,recv函數就把s的接收緩沖區中的數據copy到buff中(注意協議接收到的數據可能大于buff的長度,所以在這種情況下要調用幾次recv函數才能把sockfd的接收緩沖區中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的)
3) recv函數返回其實際copy的字節數,如果recv在copy時出錯,那么它返回SOCKET_ERROR。如果recv函數在等待協議接收數據時網絡中斷了,那么它返回0。
4) 在unix系統下,如果recv函數在等待協議接收數據時網絡斷開了,那么調用 recv的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。
默認 socket 是阻塞的 解阻塞與非阻塞recv返回值沒有區分,都是 <0 出錯 ,=0 連接關閉 ,>0 接收到數據大小,特別
返回值<0時并且(errno==EINTR || errno==EWOULDBLOCK || errno==EAGAIN)的情況下認為連接是正常的,繼續接收。
只是阻塞模式下recv會阻塞著接收數據,非阻塞模式下如果沒有數據會返回,不會阻塞著讀,因此需要循環讀?。?。
返回說明:
成功執行時,返回接收到的字節數。
另一端已關閉則返回0。
失敗返回-1,
errno被設為以下的某個值
int close(int sockfd); //返回成功為0,出錯為-1
sockfd: 要關閉的套接字描述符
close 一個套接字的默認行為是把套接字標記為已關閉,然后立即返回到調用進程,該套接字描述符不能再由調用進程使用,也就是說它不能再作為send或recv的第一個參數,然而TCP將嘗試發送已排隊等待發送到對端的任何數據,發送完畢后發生的是正常的TCP連接終止序列。
在多進程并發服務器中,父子進程共享著套接字,套接字描述符引用計數記錄著共享著的進程個數,當父進程或某一子進程close掉套接字時,描述符引用計數會相應的減一,當引用計數仍大于零時,這個close調用就不會引發TCP的四路握手斷連過程。
以上都是個人為了以后學習使用方便,對網絡函數做的一些整理,由于剛學習Linux網絡編程,難免有錯誤的地方,如果有歡迎指出,謝謝。