天天看點

典型的網絡資料收發舉例

摘要: 本文從解決實際需要出發,通過采用Windows Socket API等網絡程式設計技術實作了在區域網路共享一條電話線的情況下,當伺服器撥号上網時能及時通知各用戶端通過代理伺服器進行上網。本文還特别給出了基于Microsoft Visual C++ 6.0的部分關鍵實作代碼。

一、 問題提出的背景

筆者所使用的區域網路擁有一個伺服器及若幹分布于各辦公室的客戶機,通過網卡相連。伺服器不提供專線上網,但可以撥号上網,而各客戶機可以通過裝在伺服器端的代理伺服器共用一條電話線上網,但前提必須是伺服器已經撥接上網。考慮到經濟原因,伺服器不可能長時間連在網上,是以經常出現由于分布于各辦公室的客戶機不能知道伺服器是否處于連線狀态而造成的想上網時伺服器沒有撥号,或是伺服器已經撥号而客戶機卻并不知曉的情況,這無疑會在工作中帶來極大的不便。而筆者作為一名程式設計人員,有必要利用自己的專業優勢來解決實際工作中所遇到的一些問題。通過對實際情況的分析,可以歸納為一點:當伺服器在進行撥接上網時能及時通知在網絡上的各個客戶機,而各客戶機在收到伺服器發來的消息後可以根據自己的情況來決定是否上網。這樣就可以在同一時間内同時為較多的客戶機提供上網服務,此舉不僅提高了利用效率也大大節省了上網話費。

二、 程式主要設計思路及實作

由于本網絡是通過網卡連接配接的區域網路,是以可以首選Windows Socket API進行套接字程式設計。整個系統分為兩部分:服務端和用戶端。服務端運作于伺服器上負責監視伺服器是否在進行撥接上網,一旦發現馬上通過網絡發送消息通知用戶端;而用戶端軟體則隻需完成同服務端軟體的連接配接并能接收到從服務端發送來的通知消息即可。伺服器端要完成比用戶端更為繁重的任務。下面對這幾部分的實作分别加以描述:

(一)監視撥接上網事件的發生

在采用撥号上網時,首先需要通過撥接上網通過電話線連接配接到ISP上,然後才能享受到ISP所提供的各種網際網路服務。而要捕獲撥接上網發生的事件不能依賴于消息通知,因為此時發出的消息同一個對話框出現在螢幕上時所産生的消息是一樣的。唯一同其他對話框差別的是其标題是固定的"撥接上網",是以在無其他特殊情況下(如其他程式的标題也是"撥接上網"時)可以認定當桌面上的所有程式視窗出現以"撥接上網" 為标題的視窗時,即可認定此時正在進行撥接上網。是以可以通過搜尋并判斷視窗标題的辦法對撥接上網進行監視,具體可以用CWnd類的FindWindows()函數來實作:

CWnd *pWnd=CWnd::FindWindow(NULL,"撥接上網");

第一個參數為NULL,指定對目前所有視窗都進行搜尋。第二個參數就是待搜尋的視窗标題,一旦找到将傳回該視窗的視窗句柄。是以可以在視窗句柄不為空的情況下去通知用戶端伺服器現在正在撥号。由于一般的撥接上網都需要一段時間的連接配接應答後才能登入到ISP上,是以從提高程式運作效率角度出發可以通過定時器的使用來每間隔一段時間(如500毫秒)去搜尋一次,以確定能監視到每一次的撥接上網而又不緻過分加重CPU的負擔。

(二)伺服器端網絡通訊功能的實作

  在此采用的是可靠的有連接配接的流式套接字,并且采用了多線程和異步通知機制能有效避免一些函數如accept()等的阻塞會引起整個程式的阻塞。由于套接字程式設計方面的書籍資料非常豐富,對其進行網絡程式設計做了很詳細的描述,故本文在此隻針對一些關鍵部分做簡要說明,有關套接字網絡程式設計的詳細内容請參閱相關資料。采用流式套接字的伺服器端的主要設計流程可以歸結為以下幾步:

  1. 建立套接字

sock=socket(AF_INET,SOCK_STREAM,0);

  該函數的第一個參數用于指定位址族,在Windows下僅支援AF_INET(TCP/IP位址);第二個參數用于描述套接字的類型,對于流式套接字提供有SOCK_STREAM;最後一個參數指定套接字使用的協定,一般為0。該函數的傳回值儲存了新套接字的句柄,在程式退出前可以用closesocket()函數來将其釋放。

  2. 綁定套接字

  伺服器方一旦擷取了一個新的套接字後應通過bind()将該套接字與本機上的一個端口相關聯。此時需要預先對一個指向包含有本機IP位址和端口資訊的sockaddr_in結構填充一些必要的資訊,如本地端口号和本地主機位址等。然後就可經過bind()将伺服器程序在網絡上辨別出來。需要注意的是由于1024以内的埠号都是保留的端口号是以如無特别需要一般不能将sockin.sin_port的端口号設定為1024以内的值:

……

sockin.sin_family=AF_INET;

sockin.sin_addr.s_addr=0;

sockin.sin_port=htons(USERPORT);

bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin));

……

  3. 偵聽套接字

listen(sock,1);

  4. 等待客戶機的連接配接

  這裡需要通過accept()調用等待接收用戶端的連接配接以完成連接配接的建立,由于該函數在沒有用戶端進行申請連接配接之前會處于阻塞狀态,是以如果采取通常的單線程模式會導緻整個程式一直處于阻塞狀态而不能響應其他的外界消息,是以為該部分代碼單獨開辟一個線程,這樣阻塞将被限制在該線程内而不會影響到程式整體。

AfxBeginThread(Server,NULL);//建立一個新的線程

……

UINT Server(LPVOID lpVoid)//線程的處理函數

{

//擷取目前視類的指針,以確定通路的是目前的執行個體對象。

CNetServerView* pView=((CNetServerView*)(

(CFrameWnd*)AfxGetApp()-> m_pMainWnd)-> GetActiveView());

while(pView-> nNumConns <1)//目前的連接配接者個數

{

int nLen=sizeof(SOCKADDR);

pView-> newskt= accept(pView-> sock,

(LPSOCKADDR)& pView-> sockin,(LPINT)& nLen);

WSAAsyncSelect(pView-> newskt,

pView-> m_hWnd,WM_SOCKET_MSG,FD_CLOSE);

pView-> nNumConns++;

}

return 1;

}

  這裡在accept ()後使用了WSAAsyncSelect()異步選擇函數。對于網絡事件的響應最好采取異步選擇機制,隻有采取這種方式才可以在由網絡對方所引起的不可預知的網絡事件發生時能馬上在程序中做出及時的響應處理,而在沒有網絡事件到達時則可以處理其他事件,這種效率是很高的,而且完全符合Windows所标榜的消息觸發原則。WSAAsyncSelect()函數便是實作網絡事件異步選擇的核心函數。通過第四個參數FD_CLOSE注冊了應用程式感興取的網絡事件是網絡斷開,當客戶方端開連接配接時該事件會被檢測到,同時會發出由第三個參數指定的自定義消息WM_SOCKET_MSG。

  5. 發送/接收

  當客戶機同伺服器建立好連接配接後就可以通過send()/recv()函數進行發送和接收資料了,對于本程式隻需在監測到有撥接上網事件發生時向客戶機發送通知消息即可:

char buffer[1]={'a'};

send(newskt,buffer,1,0);//向客戶機發送字元a,表示現在伺服器正在撥号。

  6. 關閉套接字

  在全部通訊完成之後,在退出程式之前需要調用closesocket();函數把建立的套接字關閉。

(三)客戶機端的程式設計

  客戶機的程式設計要相對簡單許多,全部通訊過程隻需以下四步:

  1. 建立套接字

  2. 建立連接配接

  3. 發送/接收

  4. 關閉套接字

  具體實作過程同伺服器程式設計基本類似,隻是由于需要接收資料,是以待監測的網絡事件為FD_CLOSE和FD_READ,在消息響應函數中可以通過對消息參數的低位位元組進行判斷而區分出具體發生是何種網絡事件,并對其做出響應的反應。下面結合部分主要實作代碼對實作過程進行解釋:

……

m_ServIP=SERVERIP; //指定伺服器的IP位址

m_Port=htons(USERPORT); //指定伺服器的端口号

if((IPaddr=inet_addr(m_ServIP))==INADDR_NONE) //轉換成網絡位址

return FALSE;

else

{

sock=socket(AF_INET,SOCK_STREAM,0); //建立套接字

sockin.sin_family=AF_INET; //填充結構

sockin.sin_addr.S_un.S_addr=IPaddr;

sockin.sin_port=m_Port;

connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin)); //建立連接配接

//設定異步選擇事件

WSAAsyncSelect(sock,m_hWnd,WM_SOCKET_MSG,FD_CLOSE|FD_READ);

//在這裡可以通過震鈴、彈出對話框等方式通知客戶已經連上伺服器

}

……

//網絡事件的消息處理函數

int message=lParam & 0x0000FFFF;//取消息參數的低位

switch(message) //判斷發生的是何種網絡事件

{

case FD_READ: //讀事件

AfxBeginThread(Read,NULL);

break;

case FD_CLOSE: //伺服器關閉事件

……

break;

}

  在讀事件的消息處理過程中,單獨為讀處理過程開辟了一個線程,在該線程中接收從伺服器發送過來的資訊,并通過震鈴、彈出對話框等方式通知用戶端現在伺服器正在撥号:

……

int a=recv(pView-> sock,cDataBuffer,1,0); //接收從伺服器發送來的消息

if(a> 0)

AfxMessageBox("撥接上網已啟動!"); //通知使用者

……

三、必要的完善

   前面隻是介紹了程式設計的整體架構和設計思路,僅僅是一個雛形,有許多重要的細節沒有完善,不能用于實際使用。下面就對一些完全必要的細節做适當的完善:

   (一) 界面的隐藏

   由于本程式系自動檢測、自動通知,完全不需要人工幹預,是以可以将其視為背景運作的服務程式,是以程式主界面現在已無存在的必要,可以在應用程式類的初始化執行個體函數InitInstance()中将ShowWindow();的參數SW_SHOW改成SW_HIDE即可。當需要有對話框彈出通知使用者時僅對話框出現,主界面仍隐藏,是以是完全可行的。

   (二) 自啟動的實作

   由于服務端軟體需要時刻監視有無進行撥接上網,是以必須具備自啟動的特性。而用戶端軟體由于接收消息和通知客戶都可以自動完成,是以如果能具備自啟動特性則可以完全脫離使用者的幹預而取得較高的自動化程度。設定自啟動的特性,可以從以下幾個途徑加以考慮:

   1. 在"啟動"菜單上添加指向程式的快捷方式。

  

   2. 在Autoexec.bat中添加啟動程式的指令行。

   3. 在Win.ini中的[windows]節的run項目後添加程式路徑。

   4. 修改系統資料庫,添加鍵值的具體路徑為:

"HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Run"

   并将添加的鍵值修改為程式的存放路徑即可。以上幾種方法既可以手工添加,也可以通過程式設計使之自動完成。

   (三) 自動續聯

   對于服務/客戶模式的網絡通訊程式普遍要求服務端要先于用戶端運作,而本系統的客戶、服務端均為自啟動,不能保證伺服器先于客戶機啟動,而且本系統要求隻要客戶機和伺服器連接配接在網絡上就要不間斷保持連接配接,是以需要使客戶和服務端都要具備自動續聯的功能。

   對于伺服器端,當用戶端斷開時,需要關閉目前的套接字,并重新啟動一個新的套接字以等待客戶機的再次連接配接。這可以放在FD_CLOSE事件對應的消息WM_SOCKET_MSG的消息響應函數中來完成。而對于用戶端,如果先于伺服器而啟動,則connect()函數将傳回失敗,是以可以在程式啟動時用SetTimer()設定一個定時器,每隔一段時間(10秒)就試圖連接配接伺服器一次,當connect()函數傳回成功即伺服器已啟動并與之連接配接上之後可以用KillTimer()函數将定時器關閉。另外當伺服器關閉時需要再次開啟定時器,以確定當伺服器再次運作時能與之建立連接配接,可以通過響應FD_CLOSE事件來捕獲該事件的發生。

   小結: 本文通過Windows Sockets API實作了基于TCP/IP協定的面向連接配接的流式套接字的網絡通訊程式的設計,通過網絡通訊程式的支援可以把伺服器捕獲到的撥接上網發生的事件及時通知給用戶端,最後通過對一些必要的細節的完善很好解決了在區域網路上能及時得到伺服器撥接上網的消息通知。本文所述程式在Windows 98 SE下,由Microsoft Visual C++ 6.0編譯通過;使用的代理伺服器軟體為WinGate 4.3.0;上網方式為撥号上網。

繼續閱讀