關(guān)于同步、異步,阻塞、非阻塞的解釋
在
windows socket api
下:
異步
方式
指的是發(fā)送方不等接收方響應(yīng),便接著發(fā)下個數(shù)據(jù)包的通信方式;而
同步
指發(fā)送方發(fā)出數(shù)據(jù)后,等收到接收方發(fā)回的響應(yīng),才發(fā)下一個數(shù)據(jù)包的通信方式。
阻塞套接字
是指執(zhí)行此套接字的網(wǎng)絡(luò)調(diào)用時,直到成功才返回,否則一直阻塞在此網(wǎng)絡(luò)調(diào)用上,比如調(diào)用
recv()
函數(shù)讀取網(wǎng)絡(luò)緩沖區(qū)中的數(shù)據(jù),如果沒有數(shù)據(jù)到達(dá),將一直掛在
recv()
這個函數(shù)調(diào)用上,直到讀到一些數(shù)據(jù),此函數(shù)調(diào)用才返回;而非阻塞套接字是指執(zhí)行此套接字的網(wǎng)絡(luò)調(diào)用時,不管是否執(zhí)行成功,都立即返回。比如調(diào)用
recv()
函數(shù)讀取網(wǎng)絡(luò)緩沖區(qū)中數(shù)據(jù),不管是否讀到數(shù)據(jù)都立即返回,而不會一直掛在此函數(shù)調(diào)用上。在實(shí)際
Windows
網(wǎng)絡(luò)通信軟件開發(fā)中,異步非阻塞套接字是用的最多的。
(同步阻塞、異步非阻塞)
1
、默認(rèn)用作同步阻塞方式,那就是當(dāng)你從不調(diào)用
WSAIoctl()
和
ioctlsocket()
來改變
Socket IO
模式,也從不調(diào)用
WSAAsyncSelect()
和
WSAEventSelect()
來選擇需要處理的
Socket
事件。正是由于函數(shù)
accept()
,
WSAAccept()
,
connect()
,
WSAConnect()
,
send()
,
WSASend()
,
recv()
,
WSARecv()
等函數(shù)被用作阻塞方式,所以可能你需要放在專門的線程里,這樣以不影響主程序的運(yùn)行和主窗口的刷新。
2
、如果作為異步非阻塞方式用,那么程序主要就是要處理事件。它有兩種處理事件的辦法:
???
第一種,它常關(guān)聯(lián)一個窗口,也就是異步
Socket
的事件將作為消息發(fā)往該窗口,這是由
WinSock
擴(kuò)展規(guī)范里的一個函數(shù)
WSAAsyncSelect()
來實(shí)現(xiàn)和窗口關(guān)聯(lián)。最終你只需要處理窗口消息,來收發(fā)數(shù)據(jù)。
??
第二種,用到了擴(kuò)展規(guī)范里另一個關(guān)于事件的函數(shù)
WSAEventSelect()
,它是用事件對象的方式來處理
Socket
事件,也就是,你必須首先用
WSACreateEvent()
來創(chuàng)建一個事件對象,然后調(diào)用
WSAEventSelect()
來使得
Socket
的事件和這個事件對象關(guān)聯(lián)。最終你將要在一個線程里用
WSAWaitForMultipleEvents()
來等待這個事件對象被觸發(fā)。這個過程也稍顯復(fù)雜。
?
要點(diǎn)一、 UNIIX BSD 下 SOCKET
【
UNIIX BSD
下
SOCKET
主要是同步的
,但有阻塞和非阻塞兩種方式。阻塞方式定義與前面定義相同,要解決阻塞有兩種方法:
一種是設(shè)置
SOCKET
屬性
,設(shè)置為非阻塞(
fcntl()
函數(shù)),
sockfd?=?socket(AF_INET,?SOCK_STREAM,?0);?
fcntl(sockfd,?F_SETFL,?O_NONBLOCK);?
通過設(shè)置套接字為非阻塞,你能夠有效地
"
詢問
"
套接字以獲得信息。如果嘗試著從一個非阻塞的套接字讀信息并且沒有任何數(shù)據(jù),它不允許阻
?
塞,它將返回
?-1?
并將
?errno?
設(shè)置為
?EWOULDBLOCK
。
?
但是一般說來,這種詢問不是個好主意。如果讓程序在忙等狀
?
態(tài)查詢套接字的數(shù)據(jù),將浪費(fèi)大量的
?CPU?
時間。更好的解決之道是用
?
下一章講的
?select()?
去查詢是否有數(shù)據(jù)要讀進(jìn)來。
另一種是使用
select()
函數(shù)
,
同步方式中解決
recv
,
send
阻塞問題
采用
select
函數(shù)解決,在收發(fā)前先檢查讀寫可用狀態(tài)。
A
、讀
例子:
TIMEVAL tv01 = {0, 1};//1ms
鐘延遲
,
實(shí)際為
0-10
毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdr = {1, sConnect};
nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//
檢查可讀狀態(tài)
if(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select read status errorcode=%d",nErrorCode);
::closesocket(sConnect);
goto 重新連接(客戶方),或服務(wù)線程退出(服務(wù)方) ;
}
if(nSelectRet==0)//
超時發(fā)生,無可讀數(shù)據(jù)
{
繼續(xù)查讀狀態(tài)或向?qū)Ψ街鲃影l(fā)送
}
else
{
讀數(shù)據(jù)
}?
B
、寫
TIMEVAL tv01 = {0, 1};//1ms
鐘延遲
,
實(shí)際為
9-10
毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdw = {1, sConnect};
nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//
檢查可寫狀態(tài)
if(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select write status errorcode=%d",nErrorCode);
::closesocket(sConnect);
//goto 重新連接(客戶方),或服務(wù)線程退出(服務(wù)方) ;
}
if(nSelectRet==0)//
超時發(fā)生,緩沖滿或網(wǎng)絡(luò)忙
{
//
繼續(xù)查寫狀態(tài)或查讀狀態(tài)
}
else
{
//
發(fā)送
}?
對于
Windows
這種非搶先多任務(wù)操作系統(tǒng)來說,這兩種工作方式都是很難以接受的,為此,
WINSOCK
在盡量與
BSD Socket
保持一致外,又對它作了必要的擴(kuò)充
】
?
附:
改變
TCP
收發(fā)緩沖區(qū)大小
系統(tǒng)默認(rèn)為
8192
,利用如下方式可改變。
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int nrcvbuf=1024*20;
int err=setsockopt(
sConnect,
SOL_SOCKET,
SO_SNDBUF,// 寫緩沖,讀緩沖為 SO_RCVBUF
(char *)&nrcvbuf,
sizeof(nrcvbuf));
if (err != NO_ERROR)
{
TRACE("setsockopt Error!\n");
}
?
在設(shè)置緩沖時,檢查是否真正設(shè)置成功用
int getsockopt(
SOCKET s,
int level,
int optname,
char FAR *optval,
int FAR *optlen
);?
服務(wù)方同一端口多 IP 地址的 bind 和 listen
在可靠性要求高的應(yīng)用中,要求使用雙網(wǎng)和多網(wǎng)絡(luò)通道,再服務(wù)方很容易實(shí)現(xiàn),用如下方式可建立客戶對本機(jī)所有
IP
地址在端口
3024
下的請求服務(wù)。
SOCKET hServerSocket_DS=INVALID_SOCKET;
struct sockaddr_in HostAddr_DS;//
服務(wù)器主機(jī)地址
LONG lPort=3024;
HostAddr_DS.sin_family=AF_INET;
HostAddr_DS.sin_port=::htons(u_short(lPort));
HostAddr_DS.sin_addr.s_addr=htonl(INADDR_ANY);
hServerSocket_DS=::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);
if(hServerSocket_DS==INVALID_SOCKET)
{
AfxMessageBox(" 建立數(shù)據(jù)服務(wù)器 SOCKET 失敗 !");
return FALSE;
}
if(SOCKET_ERROR==::bind(hServerSocket_DS,(struct
sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))
{
int nErrorCode=WSAGetLastError ();
TRACE("bind error=%d\n",nErrorCode);
AfxMessageBox("Socket Bind 錯誤 !");
return FALSE;
}
?
if(SOCKET_ERROR==::listen(hServerSocket_DS,10))//10
個客戶
{
AfxMessageBox("Socket listen 錯誤 !");
return FALSE;
}
?
AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL);?
要點(diǎn)二、 windows socket api
WINSOCK
對
BSD Socket
的擴(kuò)充主要是在基于消息、對網(wǎng)絡(luò)事件的異步存取接口上。下表列出了
WINSOCK
擴(kuò)充的函數(shù)功能。
??????
函
??
數(shù)
??
名
|
????????
功
???????????
能
|
WSAAsyncGetHostByAddr()
|
標(biāo)準(zhǔn)
Berkeley
函數(shù)
getXbyY
的異步版本,例
|
WSAAsyncGetHostByName()
|
如:函數(shù)
WSAAsyncGetHostByName()
就是提
|
WSAAsyncGetProtoByName()
|
供了標(biāo)準(zhǔn)
Berkeley
函數(shù)
gethostbyname
的一
|
WSAAsyncGetProtoByNumber()
|
種基于消息的異步實(shí)現(xiàn)。
|
WSAAsyncGetServByName()
|
|
WSAAsyncGetServByPort()
|
|
WSAAsyncSelect()
|
函數(shù)
select()
的異步版本
|
WSACancelAsyncRequest()
|
取消函數(shù)
WSAAsyncGetXByY
執(zhí)行中的實(shí)例
|
WSACancelBlockingCall()
|
取消一個執(zhí)行中的
“
阻塞
”API
調(diào)用
|
WSACleanup()
|
終止使用隱含的 Windows Sockets DLL
|
WSAGetLastError()
|
獲取
Windows Sockets API
的最近錯誤號
|
WSAIsBlocking()
|
檢測隱含的
Windows Sockets DLL
是否阻塞了一個當(dāng)前線索的調(diào)用
|
WSASetBlockingHook()
|
設(shè)置應(yīng)用程序自己的
“
阻塞
”
處理函數(shù)
|
WSASetLastError()
|
設(shè)置
Windows Sockets API
的最近錯誤號
|
WSAStartup()
|
初始化隱含的 Windows Sockets DLL
|
WSAUnhookBlockingHook()
|
恢復(fù)原來的
“
阻塞
”
處理函數(shù)
|
從表1可以看出,
WINSOCK
的擴(kuò)充功能
可以分為如下幾類:
???
(1)異步選擇機(jī)制:
???
異步選擇函數(shù)
WSAAsyncSelect()
允許應(yīng)用程序提名一個或多個感興趣的網(wǎng)絡(luò)事件,
所有阻塞的網(wǎng)絡(luò)
I/O
例程(如
send()
和
resv()
),不管它是已經(jīng)使用還是即將使用,都可作為
WSAAsyncSelect()
函數(shù)選擇的候選。當(dāng)被提名的網(wǎng)絡(luò)事件發(fā)生時,
Windows
應(yīng)用程序的窗口函數(shù)將收到一個消息,消息附帶的參數(shù)指示被提名過的某一網(wǎng)絡(luò)事件。
???
(2)異步請求例程:
???
異步請求例程允許應(yīng)用程序用異步方式獲取請求的信息,如
WSAAsyncGetXByY()
類函數(shù)允許用戶請求異步服務(wù),這些功能在使用標(biāo)準(zhǔn)
Berkeley
函數(shù)時是阻塞的。函數(shù)
WSACancelAsyncRequest()
允許用戶終止一個正在執(zhí)行的異步請求。
???
(3)阻塞處理方法:
??? WINSOCK
在調(diào)用處于阻塞時進(jìn)入一個叫
“Hook”
的例程,它負(fù)責(zé)處理
Windows
消息,使得
Windows
的消息循環(huán)能夠繼續(xù)。
WINSOCK
還提供了兩個函數(shù)(
WSASetBlockingHook()
和
WSAUnhookBlockingHook()
)讓用戶能夠設(shè)置和取消自己的阻塞處理例程。另外,函數(shù)
WSAIsBlocking()
可以檢測調(diào)用是否阻塞,函數(shù)
WSACancelBlockingCall()
可以取消一個阻塞的調(diào)用。
???
(4)出錯處理:
??
?
為了和以后的多線索環(huán)境(如
Windows/NT
)兼容,
WINSOCK
提供了兩個出錯處理函數(shù)
WSAGetLastError()
和
WSASetLastError()
來獲取和設(shè)置本線索的最近錯誤號。
???
(5)啟動與終止:
??? WINSOCK
的應(yīng)用程序在使用上述
WINSOCK
函數(shù)前,必須先調(diào)用
WSAStartup()
函數(shù)對
Windows Sockets DLL
進(jìn)行初始化,以協(xié)商
WINSOCK
的版本支持,并分配必要的資源。在應(yīng)用程序退出之前,應(yīng)該先調(diào)用函數(shù)
WSAClearnup()
終止對
Windows Sockets DLL
的使用,并釋放資源,以利下一次使用。
???
在這些函數(shù)中,實(shí)現(xiàn)
Windows
網(wǎng)絡(luò)實(shí)時通信的關(guān)鍵是異步選擇函數(shù)
WSAAsyncSelect()
的使用
,其原型如下:
int PASCAL FAR WSAAsyncSelect(SOCTET s,HWND hWnd,unsigned int wMsg,long lEvent);
它請求
Windows Sockets DLL
在檢測到在套接字
s
上發(fā)生的
lEvent
事件時,向窗口
hWnd
發(fā)送一個消息
wMsg
。它自動地設(shè)置套接字
s
處于非阻塞工作方式。參數(shù)
lEvent
由下列事件的一個或多個組成:
??????
值
????????
????
含
??????
義
??????? FD_READ??????
希望在套接字
s
收到數(shù)據(jù)(即讀準(zhǔn)備好)時接到通知
??????? FD_WRITE?????
希望在套接字
s
可發(fā)送數(shù)據(jù)(即寫準(zhǔn)備好)時接到通知
??????? FD_OOB???????
希望在套接字
s
上有帶外數(shù)據(jù)到達(dá)時接到通知
???????
FD_ACCEPT????
希望在套接字
s
上有外部連接到來時接到通知
??????? FD_CONNECT
希望在套接字
s
連接建立完成時接到通知
??????? FD_CLOSE?????
希望在套接字
s
關(guān)閉時接到通知
?
?????
表2
.??
異步選擇網(wǎng)絡(luò)事件表
???
例如,我們要在套接字
s
讀準(zhǔn)備好或?qū)憸?zhǔn)備好時接到通知,可以使用下面的語句:
??? rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ | FD_WRITE);
當(dāng)套接字
s
上被提名的一個網(wǎng)絡(luò)事件發(fā)生時,窗口
hWnd
將收到消息
wMsg
,變量
lParam
的低字指示網(wǎng)絡(luò)發(fā)生的事件,高字指示錯誤碼。應(yīng)用程序就可以通過這些信息來決定自己的下一步動作。
【理解:基于消息驅(qū)動的可稱之為
“
異步
”
。】
[ 引 1 : ]
熟悉
WINSOCK
編程的讀者一定會覺得奇怪吧,為什么
INDY
是是完全基于
SOCKET
阻塞工作模式的呢?異步模式(非阻塞模式)是
WINSOCK
的一大特點(diǎn),為什么不用呢?
其實(shí),之所以大多數(shù)
WINDOWS
下的
INTERNET
程序都使用異步模式,這和
WINSOCK
的歷史有關(guān)。當(dāng)
WINSOCK
被移植到
WINDOWS
的時候,當(dāng)時的
WINDOWS
操作系統(tǒng)還是
WINDOWS 3.1
,而
WINDOWS 3.1
是不支持多線程的,不象
UNIX
下可以使用
FORK
來運(yùn)行多進(jìn)程。在
WINDOWS 3.1
下,如果使用阻塞模式,在通訊時會鎖定用戶界面使程序沒有響應(yīng),為了避免這種情況,
WINSOCK
就引入異步模式這個新特性。而使用異步模式來編制
INTERNET
程序也就成了
WINDOWS
程序員的經(jīng)典教條。但是,隨著新的
WINDOWS
操作系統(tǒng)的出現(xiàn),如
WINDOWS 95
、
NT
、
98
、
ME
、
2000
等,這些操作系統(tǒng)開始支持多線程。異步模式這個教條仍然深入人心,使很多程序員會下意識的拒絕使用阻塞模式。
事實(shí)上,
UNIX
下的
SOCKET
只支持阻塞模式(現(xiàn)在
UNXI
的
SOCKET
有了一些新的非阻塞特性,不過絕大多數(shù)應(yīng)用仍然使用阻塞模式)
。阻塞模式具有以下幾個比異步模式優(yōu)越的特點(diǎn):
編程更簡單,可以把所有處理
SOCKET
的代碼放在一起,順序執(zhí)行,而不用分散在不同的事件處理代碼段里。
更容易移植到
UNIX
,使用
INDY
的
DELPHI
程序,可以不做太多(甚至不做)修改,就可以把
WINDOWS
的
DELPHI
源代碼拿到
LINUX
下,用
Kylix
來編譯成
LINUX
下的網(wǎng)絡(luò)程序。
[ 引 2 : ]
Windows Sockets API
是
Microsoft Windows
的網(wǎng)絡(luò)程序設(shè)計(jì)接口,它在繼承了
Berkeley Sockets
主要特征的基礎(chǔ)上,又對它進(jìn)行了重要擴(kuò)充。這些擴(kuò)充主要是提供了一些異步函數(shù),并增加了符合
Windows
消息驅(qū)動特性的網(wǎng)絡(luò)事件異步選擇機(jī)制
。這些擴(kuò)充有利于應(yīng)用程序開發(fā)者編制符合
Windows
編程模式的軟件,它使在
Windows
下開發(fā)高性能的網(wǎng)絡(luò)通信程序成為可能