講一下套接字模式和套接字I/O模型的區別。先說明一下,只針對Winsock,如果你要骨頭里挑雞蛋把UNIX下的套接字概念來往這里套,那就不關我的事。
套接字模式:阻塞套接字和非阻塞套接字。或者叫同步套接字和異步套接字。
套接字模型:描述如何對套接字的I/O行為進行管理。
Winsock提供的I/O模型一共有五種:
select,WSAAsyncSelect,WSAEventSelect,Overlapped,Completion。今天先講解select。
1:select模型(選擇模型)
先看一下下面的這句代碼:
int iResult = recv(s, buffer,1024);
這是用來接收數據的,在默認的阻塞模式下的套接字里,recv會阻塞在那里,直到套接字連接上有數據可讀,把數據讀到buffer里后recv函數才會返回,不然就會一直阻塞在那里。在單線程的程序里出現這種情況會導致主線程(單線程程序里只有一個默認的主線程)被阻塞,這樣整個程序被鎖死在這里,如果永遠沒數據發送過來,那么程序就會被永遠鎖死。這個問題可以用多線程解決,但是在有多個套接字連接的情況下,這不是一個好的選擇,擴展性很差。Select模型就是為了解決這個問題而出現的。
再看代碼:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
這一次recv的調用不管套接字連接上有沒有數據可以接收都會馬上返回。原因就在于我們用ioctlsocket把套接字設置為非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的情況下,recv確實是馬上返回了,但是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操作沒有成功完成??吹竭@里很多人可能會說,那么就重復調用recv并檢查返回值,直到成功為止,但是這樣做效率很成問題,開銷太大。
感謝天才的微軟工程師吧,他們給我們提供了好的解決辦法。
先看看select函數
int select(
int nfds,
fd_set FAR *readfds,
fd_set FAR *writefds,
fd_set FAR *exceptfds,
const struct timeval FAR *timeout
);
第一個參數不要管,會被系統忽略的。第二個參數是用來檢查套接字可讀性,也就說檢查套接字上是否有數據可讀,同樣,第三個參數用來檢查數據是否可以發出。最后一個是檢查是否有帶外數據可讀取。
參數詳細的意思請去看MSDN,這里限于篇幅不詳細解釋了。
最后一個參數是用來設置select等待多久的,是個結構:
struct timeval {
long tv_sec; // seconds
long tv_usec; // and microseconds
};
如果將這個結構設置為(0,0),那么select函數會馬上返回。
說了這么久,select的作用到底是什么?
他的作用就是:防止在在阻塞模式的套接字里被鎖死,避免在非阻塞套接字里重復檢查WSAEWOULDBLOCK錯誤。
他的工作流程如下:
1:用FD_ZERO宏來初始化我們感興趣的fd_set,也就是select函數的第二三四個參數。
2:用FD_SET宏來將套接字句柄分配給相應的fd_set。
3:調用select函數。
4:用FD_ISSET對套接字句柄進行檢查,如果我們所關注的那個套接字句柄仍然在開始分配的那個fd_set里,那么說明馬上可以進行相應的IO操作。比如一個分配給select第一個參數的套接字句柄在select返回后仍然在select第一個參數的fd_set里,那么說明當前數據已經來了,馬上可以讀取成功而不會被阻塞。
下面給出一個簡單的select模型的服務端套接字。
#include “iostream.h”
#include “winsock2.h”
#include “windows.h”
#define InternetAddr "127.0.0.1"
#define iPort 5055
#pragma comment(lib, "ws2_32.lib")
void main()
{
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
SOCKET fdServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(InternetAddr);
server.sin_port = htons(iPort);
int ret = bind(fdServer, (sockaddr*)&server, sizeof(server));
ret = listen(fdServer, 4);
SOCKET AcceptSocket;
fd_set fdread;
timeval tv;
int nSize;
while(1)
{
FD_ZERO(&fdread);//初始化fd_set
FD_SET(fdServer, &fdread);//分配套接字句柄到相應的fd_set
tv.tv_sec = 2;//這里我們打算讓select等待兩秒后返回,避免被鎖死,也避免馬上返回
tv.tv_usec = 0;
select(0, &fdread, NULL, NULL, &tv);
nSize = sizeof(server);
if (FD_ISSET(fdServer, &fdread))//如果套接字句柄還在fd_set里,說明客戶端已經有connect的請求發過來了,馬上可以accept成功
{
AcceptSocket = accept(fdServer,( sockaddr*) &server, &nSize);
break;
}
else//還沒有客戶端的connect請求,我們可以去做別的事,避免像沒有用select方式的阻塞套接字程序被鎖死的情況,如果沒用select,當程序運行到accept的時候客戶端恰好沒有connect請求,那么程序就會被鎖死,做不了任何事情
{
//do something
::MessageBox(NULL, "waiting...", "recv", MB_ICONINFORMATION);//別的事做完后,繼續去檢查是否有客戶端連接請求
}
}
char buffer[128];
ZeroMemory(buffer, 128);
ret = recv(AcceptSocket,buffer,128,0);//這里同樣可以用select,用法和上面一樣
::MessageBox(NULL, buffer, "recv", MB_ICONINFORMATION);
closesocket(AcceptSocket);
WSACleanup();
return;
}
基本上就這樣,個人感覺select模型用處不是很大,我只用過一次,去年寫端口掃描器的時候用select來檢查超時。
感覺講得不是很清楚,雖然東西我很明白,但是要講解出來講解得很清楚真不容易。
不知道大家能不能看懂,如果看得過程中有什么問題就問吧。