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