C++网络编程概述
Socket有同步阻塞方式和異步非阻塞方式兩種使用,事實上同步和異步在我們編程的生涯中可能遇到了很多,而Socket也沒什么特別。雖然同步好用,不費勁,但不能滿足一些應用場合,其效率也很低。
????也許初涉編程的人不能理解“同步(或阻塞)”和“異步(或非阻塞)”,其實簡單兩句話就能講清楚,同步和異步往往都是針對一個函數來說的,“同步”就是函數直到其要執行的功能全部完成時才返回,而“異步”則是,函數僅僅做一些簡單的工作,然后馬上返回,而它所要實現的功能留給別的線程或者函數去完成。例如,SendMessage就是“同步”函數,它不但發送消息到消息隊列,還需要等待消息被執行完才返回;相反PostMessage就是個異步函數,它只管發送一個消息,而不管這個消息是否被處理,就馬上返回。
一、Socket?API
????首先應該知道,有Socket1.1提供的原始API函數,和Socket2.0提供的一組擴展函數,兩套函數。這兩套函數有重復,但是2.0提供的函數功能更強大,函數數量也更多。這兩套函數可以靈活混用,分別包含在頭文件Winsock.h,Winsock2.h,分別需要引入庫wsock32.lib、Ws2_32.lib。
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()來等待這個事件對象被觸發。這個過程也稍顯復雜。
二、CAsyncSocket
????看類名就知道,它是一個異步非阻塞Socket封裝類,CAsyncSocket::Create()有一個參數指明了你想要處理哪些Socket事件,你關心的事件被指定以后,這個Socket默認就被用作了異步方式。那么CAsyncSocket內部到底是如何將事件交給你的呢?
????CAsyncSocket的Create()函數,除了創建了一個SOCKET以外,還創建了個CSocketWnd窗口對象,并使用WSAAsyncSelect()將這個SOCKET與該窗口對象關聯,以讓該窗口對象處理來自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是簡單地回調CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(),CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虛函數。所以CAsyncSocket的派生類,只需要在這些虛函數里添加發送和接收的代碼。
??
??簡化后,大致的代碼為:
??bool?CAsyncSocket::Create(?long?lEvent?)?file://參/數lEvent是指定你所關心的Socket事件
??{
???m_hSocket?=?socket(?PF_INET,?SOCK_STREAM,?0?);?file://創/建Socket本身
???CSocketWnd*?pSockWnd?=?new?CSocketWnd;?file://創/建響應事件的窗口,實際的這個窗口在AfxSockInit()調用時就被創建了。
???pSockWnd->Create(...);
???WSAAsyncSelect(?m_hSocket,?pSockWnd->m_hWnd,?WM_SOCKET_NOTIFY,?lEvent?);?file://Socket/事件和窗口關聯
??}
??
??static?void?PASCAL?CAsyncSocket::DoCallBack(WPARAM?wParam,?LPARAM?lParam)
??{
???CAsyncSocket?Socket;
???Socket.Attach(?(SOCKET)wParam?);?file://wParam/就是觸發這個事件的Socket的句柄
???int?nErrorCode?=?WSAGETSELECTERROR(lParam);?file://lParam/是錯誤碼與事件碼的合成
???switch?(WSAGETSELECTEVENT(lParam))
???{
???case?FD_READ:
????pSocket->OnReceive(nErrorCode);
????break;
???case?FD_WRITE:
????pSocket->OnSend(nErrorCode);
????break;
???case?FD_OOB:
????pSocket->OnOutOfBandData(nErrorCode);
????break;
???case?FD_ACCEPT:
????pSocket->OnAccept(nErrorCode);
????break;
???case?FD_CONNECT:
????pSocket->OnConnect(nErrorCode);
????break;
???case?FD_CLOSE:
????pSocket->OnClose(nErrorCode);
????break;
???}
??}
??CSocketWnd類大致為:
??BEGIN_MESSAGE_MAP(CSocketWnd,?CWnd)
???ON_MESSAGE(WM_SOCKET_NOTIFY,?OnSocketNotify)
??END_MESSAGE_MAP()
??LRESULT?CSocketWnd::OnSocketNotify(WPARAM?wParam,?LPARAM?lParam)
??{
???CAsyncSocket::DoCallBack(?wParam,?lParam?);?file://收/到Socket事件消息,回調CAsyncSocket的DoCallBack()函數
???return?0L;
??}
??然而,最不容易被初學Socket編程的人理解的,也是本文最要提醒的一點是,客戶方在使用CAsyncSocket::Connect()時,往往返回一個WSAEWOULDBLOCK的錯誤(其它的某些函數調用也如此),實際上這不應該算作一個錯誤,它是Socket提醒我們,由于你使用了非阻塞Socket方式,所以(連接)操作需要時間,不能瞬間建立。既然如此,我們可以等待呀,等它連接成功為止,于是許多程序員就在調用Connect()之后,Sleep(0),然后不停地用WSAGetLastError()或者CAsyncSocket::GetLastError()查看Socket返回的錯誤,直到返回成功為止。這是一種錯誤的做法,斷言,你不能達到預期目的。事實上,我們可以在Connect()調用之后等待CAsyncSocket::OnConnect()事件被觸發,CAsyncSocket::OnConnect()是要表明Socket要么連接成功了,要么連接徹底失敗了。至此,我們在CAsyncSocket::OnConnect()被調用之后就知道是否Socket連接成功了,還是失敗了。
??類似的,Send()如果返回WSAEWOULDBLOCK錯誤,我們在OnSend()處等待,Receive()如果返回WSAEWOULDBLOCK錯誤,我們在OnReceive()處等待,以此類推。
??還有一點,也許是個難點,那就是在客戶方調用Connect()連接服務方,那么服務方如何Accept(),以建立連接的問題。簡單的做法就是在監聽的Socket收到OnAccept()時,用一個新的CAsyncSocket對象去建立連接,例如:
?void?CMySocket::OnAccept(?int?ErrCode?)
?{
???????CMySocket*?pSocket?=?new?CMySocket;
???????Accept(?*pSocket?);
?}
????于是,上面的pSocket和客戶方建立了連接,以后的通信就是這個pSocket對象去和客戶方進行,而監聽的Socket仍然繼續在監聽,一旦又有一個客戶方要連接服務方,則上面的OnAccept()又會被調用一次。當然pSocket是和客戶方通信的服務方,它不會觸發OnAccept()事件,因為它不是監聽Socket。
三、CSocket
???CSocket是MFC在CAsyncSocket基礎上派生的一個同步阻塞Socket的封裝類。它是如何又把CAsyncSocket變成同步的,而且還能響應同樣的Socket事件呢?
??其實很簡單,CSocket在Connect()返回WSAEWOULDBLOCK錯誤時,不是在OnConnect(),OnReceive()這些事件終端函數里去等待。你先必須明白Socket事件是如何到達這些事件函數里的。這些事件處理函數是靠CSocketWnd窗口對象回調的,而窗口對象收到來自Socket的事件,又是靠線程消息隊列分發過來的。總之,Socket事件首先是作為一個消息發給CSocketWnd窗口對象,這個消息肯定需要經過線程消息隊列的分發,最終CSocketWnd窗口對象收到這些消息就調用相應的回調函數(OnConnect()等)。
???所以,CSocket在調用Connect()之后,如果返回一個WSAEWOULDBLOCK錯誤時,它馬上進入一個消息循環,就是從當前線程的消息隊列里取關心的消息,如果取到了WM_PAINT消息,則刷新窗口,如果取到的是Socket發來的消息,則根據Socket是否有操作錯誤碼,調用相應的回調函數(OnConnect()等)。
??大致的簡化代碼為:
??BOOL?CSocket::Connect(?...?)
??{
???if(?!CAsyncSocket::Connect(?...?)?)
???{
????if(?WSAGetLastError()?==?WSAEWOULDBLOCK?)?file://由/于異步操作需要時間,不能立即完成,所以Socket返回這個錯誤
????{
?????file://進/入消息循環,以從線程消息隊列里查看FD_CONNECT消息,直到收到FD_CONNECT消息,認為連接成功。
?????while(?PumpMessages(?FD_CONNECT?)?);
????}
???}
??}
??BOOL?CSocket::PumpMessages(?UINT?uEvent?)
??{
??????CWinThread*?pThread?=?AfxGetThread();
??????while(?bBlocking?)?file://bBlocking/僅僅是一個標志,看用戶是否取消對Connect()的調用
??????{
??????????MSG?msg;
??????????if(?PeekMessage(?&msg,?WM_SOCKET_NOTIFY?)?)
??????????{
?????????????if(?msg.message?==?WM_SOCKET_NOTIFY?&&?WSAGETSELECTEVENT(msg.lParam)?==?uStopFlag?)
?????????????{
?????????????????CAsyncSocket::DoCallBack(?msg.wParam,?msg.lParam?);
?????????????????return?TRUE;
?????????????}?????
?????????}
?????????else
????????{
?????????????OnMessagePending();?file://處/理消息隊列里的其它消息
?????????????pThread->OnIdle(-1);
????????}
?????}
??}
??BOOL?CSocket::OnMessagePending()
??{
??????MSG?msg;
???????if(?PeekMessage(?&msg,?NULL,?WM_PAINT,?WM_PAINT,?PM_REMOVE?)?)
???????{?file://這/里僅關心WM_PAINT消息,以處理阻塞期間的主窗口重畫
???????????::DispatchMessage(?&msg?);
???????????return?FALSE;
???????}
???????return?FALSE;
??}
???其它的CSocket函數,諸如Send(),Receive(),Accept()都在收到WSAEWOULDBLOCK錯誤時,進入PumpMessages()消息循環,這樣一個原本異步的CAsyncSocket,到了派生類CSocket,就變成同步的了。
??明白之后,我們可以對CSocket應用自如了。比如有些程序員將CSocket的操作放入一個線程,以實現多線程的異步Socket(通常,同步+多線程?相似于?異步?)。
四、CSocketFile
??另外,進行Socket編程,不能不提到CSocketFile類,其實它并不是用來在Socket雙方發送文件的,而是將需要序列化的數據,比如一些結構體數據,傳給對方,這樣,程序的CDocument()的序列化函數就完全可以和CSocketFile聯系起來。例如你有一個CMyDocument實現了Serialize(),你可以這樣來將你的文檔數據傳給Socket的另一方:
?CSocketFile?file(?pSocket?);
?CArchive?ar(?&file,?CArchive::store?);
?pDocument->Serialize(?ar?);
?ar.Close();
??同樣,接收一方可以只改變上面的代碼為CArchive?ar(?&file,?CArchive::load?);即可。
???注意到,CSocketFile類雖然從CFile派生,但它屏蔽掉了CFile::Open()等函數,而函數里僅扔出一個例外。那么也就是說,你不能調用CSocketFile的Open函數來打開一個實實在在的文件,否則會導致例外,如果你需要利用CSocketFile來傳送文件,你必須提供CSocketFile類的這些函數的實現。
??再一點,CArchive不支持在datagram的Socket連接上序列化數據
總結
- 上一篇: CSocketFile类
- 下一篇: MFC六大核心机制之一:MFC程序的初始