網絡模型第六講Select模型
一丶Select模型是什么
以前我們講過一個迭代模型.就是只服務一個客戶端連接.但是實際網絡編程中.復雜的很多. 比如一個 C/S架構程序 (客戶端/服務端) 客戶端很多的情況下.都要連接服務器.
不可能一個服務器只服務一個客戶端. 就像現在很火的一款游戲 .PUBG. 絕地求生. 他就是 CS結構程序. 玩的人很多.不可能買了很多很多服務器吧.所以我們就要寫模型. 來管理這些客戶端的Socket
並對去進行讀寫操作. 當前 Select模型只針對小網絡程序使用. 不可能應用到游戲上. 因為它能管理的Socket 實在有限. 如果是Windows的話可能以后會接觸到事件模型.消息模型.以及IOCP模型.
其實說白了就是對Socket進行管理.有效的進行讀寫.減少開銷. 隨着模型等級越高.所需要的知識點就越多.就越來越困難.
二丶Select 方法
socket 套接字為我們提供了一個Select 方法.
int WSAAPI select( int nfds, //默認為0 fd_set *readfds, //一個指針集合.. 針對讀操作 accept,recg fd_set *writefds, //針對寫操作 connect send fd_set *exceptfds, //針對出現的異常 const timeval *timeout //超時設置,為NULL就是一直等待結果. );
返回值:
1超時返回0
2.出錯返回 SOCKET_ERROR
其中FD_SET是一個結構體.
typedef struct fd_set { u_int fd_count; /* how many are SET? */ 有多少套接字 SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ 套接字的數據 } fd_set; FD_SETSIZE = 64 所以說不能針對大網絡.因為只有64個.當然你可以更改.不過更改之后還不如用更好的模型.
常用的方法;
FD_ZERO 清零
FD_SET 添加套接字 也就是往數組里面添加指針
FD_ISSET 校驗函數.如果參數是集合的成員.則返回 非0 ,否則返回0
int getsockopt( SOCKET s, //查詢的套接字 int level, //選項的等級. SOL_SOCKET IPPROTO_TCP 原始套接字還是 int optname, //SOCKET選項名字 SO_ERROR SO_ACCEPTCONN char *optval, //用於接受數據的緩沖區 int *optlen //緩沖區大小 ); 函數作用: 查看套接字的狀態
返回值: 成功0 失敗 SOCKET_ERROR
具體作用可以查詢MSDN
select 函數的作用:
上面說了怎么多.可能大家就很暈.該如何編寫代碼. 編寫代碼前說一下.或許能豁然開朗.
其實我們 定義了數組(集合) 當有事件來的時候.select會返回. 返回的時候.會把我們集合里面的值進行設置. 這樣我們可以對集合的值進行判斷.如果是accept 則調用accept.
代碼如下:
// Server.cpp : 定義控制台應用程序的入口點。 // #include "stdafx.h" #include <WinSock2.h> #pragma comment(lib,"ws2_32.lib") int main() { WSADATA wsaData; int iResult; u_long iMode = 0; iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != NO_ERROR) printf("Error at WSAStartup()\n"); //------------------------- // Create a SOCKET object. SOCKET m_socket; m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_socket == INVALID_SOCKET) { printf("Error at socket(): %ld\n", WSAGetLastError()); WSACleanup(); return 0; } sockaddr_in hServerAddr; hServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); hServerAddr.sin_family = AF_INET; hServerAddr.sin_port = htons(8564); bind(m_socket, (sockaddr *)&hServerAddr, sizeof(hServerAddr)); //監聽 listen(m_socket, 1); //主要是進行連接的時候.代碼不一樣了.因為是非阻塞.所以返回的錯誤是資源暫時不可用. sockaddr_in hClientAddr; int nAddrSize = sizeof(hClientAddr); SOCKET hClientSocket; fd_set Read, Write, Except; //定義三個集合.並且清零 FD_ZERO(&Read); FD_ZERO(&Write); FD_ZERO(&Except); //添加SOCKET 到集合中 FD_SET(m_socket, &Read); FD_SET(m_socket, &Write); FD_SET(m_socket, &Except); //使用Select 進行監聽 無限監聽 int nRet = select(0, &Read, &Write, &Except,NULL); //如果有讀操作.則Read集合則被改變. nRet = FD_ISSET(m_socket, &Read);//只判斷讀 char szBuf[0x1000] = { 0 }; if (SOCKET_ERROR != nRet) { //我們就可以進行接收了.因為讀操作只有接收.不可能是Recv hClientSocket = accept(m_socket, (sockaddr*)&hClientAddr, &nAddrSize); //繼續進行清零.看看是否是讀操作還是寫操作. recv(hClientSocket, szBuf, 0x1000, 0); FD_ZERO(&Read); FD_ZERO(&Write); FD_ZERO(&Except); //講客戶端套接字跟服務端套接字都放到集合中. FD_SET(m_socket, &Read); FD_SET(m_socket, &Write); FD_SET(m_socket, &Except); FD_SET(hClientSocket, &Read); FD_SET(hClientSocket, &Write); // 如果都設置了.那么在此進行Select監聽的時候.服務端套Read集合則為2.因為要接受.客戶端的Write則為1.因為他發送了send FD_SET(hClientSocket, &Except); nRet = select(0, &Read, &Write, &Except, NULL); nRet = FD_ISSET(hClientSocket, &Write); //客戶端的寫操作回來.所以影響的是客戶端的write 服務端的不影響. int nRet2 = FD_ISSET(m_socket, &Write); } return -1; }
當有客戶端連接的時候.我們的集合就重置了.
例如下圖:
可以看到套接字是一個f4 有一個.所以下方我們進行判斷是否是讀操作.如果是讀操作我們就進行接受連接
接受連接之后.我們把客戶端的套接字也設置到集合中.當監聽客戶端操作的時候.寫操作就會來了.
總結一下:
1.send recv connect accept 都是一個狀態.
2.slecet 會根據這些來設置我們集合中數組的狀態.
3.我們根據判斷集合中數組的狀態.對其進行操作即可.
如果出現異常.我們就需要用
getsockopt 來檢索錯誤值了.
因為我們使用的FD_xxx都是宏. 如果在使用GetLastError 則會出現錯誤.結果不准確. 所以直接使用這個函數進行錯誤代碼的獲取.
例子:
if (FD_ISSET(hSocket,&Except)) //如果出現異常了. { int nErrCode; int nErrCodeLen; getsocket(hSocket,SOL_SOCKET,SO_ERROR,&nErrcode,&nErrcodelen); }