windows多執行緒和網路程式設計
阿新 • • 發佈:2019-01-02
第 10 章 多執行緒與網路程式設計初步
教學提示:Windows 是一個支援多工的作業系統。當在一個程式中需要啟動另外一
個程式時,需要用到多程序的程式設計方式。如果一個程序中有一些相似的任務需要同時推進,
可以為每個任務建立一個執行緒,從而形成多執行緒的程式設計。隨著網路技術的廣泛應用,網路
程式設計也越來越受到重視,網路程式設計主要使用 Winsock 技術。
教學目標:掌握程序的建立與終止及相應的管理,瞭解執行緒的基本概念,並掌握執行緒
的建立及使用。能夠使用 Winsock 進行簡單的網路程式設計。
10.1 Windows 的多工
Windows 是一個支援多工的作業系統。現在可以在欣賞電腦播放 CD 音樂的同時,
一邊列印檔案,一邊編輯檔案,這在以前的 DOS 作業系統的時候是不可能的。因為 DOS
是一個單使用者、單任務的作業系統,一個時間段內只能執行一道程式。而 Windows 環境下
卻可以做到這點,這都是得益於 Windows 的多程序處理及多執行緒處理功能。除了上述所說
的多工的優點,再來看一下網路應用盛行的當今時代,多工給我們帶來的益處。作為
一個網路伺服器,比如搜狐網站,每個時刻都要接收來自客戶端的數量巨大的網路服務請
求,如果沒有多工環境的支援,而是處理完一個請求後再處理下一個,這樣大家在上網
時就得在自己的機器前坐等其他的請求處理完後再得到響應。但實際情況卻非如此,我們
可以隨時上網,感覺不到其他人的存在,這就是作業系統的多工也就是多程序、多執行緒
機制所帶來的優越性。
在 VC 中如何設計一個多工程式,甚至如何使用這種技術來實現網路應用,這都是
作為程式設計師首先要關心和掌握的問題。通過本章的學習,相信讀者會達到這個目標。
10.2 Windows 的多程序程式設計
程序是由程式碼,資料和該程序中執行緒可用的其他系統資源,諸如檔案、管道和同步對
象組成。每個程序都有一個私有的虛擬地址空間。一個程序至少包括一個執行緒(稱為主線
程),並且每個程序都由主執行緒開始。在執行過程中可以建立新的執行執行緒。
例如,如果啟動了 Microsoft Word 程式,則在記憶體中就存在了一個以 winword.exe 為
程式碼的程序,如果不關閉當前的 Word 程式,又通過開始選單啟動了 Microsoft Word,則又
開始了一個以 winword.exe 為程式碼的程序。這兩個程序的程式碼雖然一樣,但所處的環境也
就是資料或其他系統資源是不同的,它們是兩個不同的程序。如果再啟動一個記事本程式,
則系統中又多了一個以 notepad.exe 為程式碼的程序,現在系統中已經存在了 3 個使用者程序。第 10 章 多執行緒與網路程式設計初步 ·263·
·263·
它們在同一段時間內都是向前推進的。
本節主要介紹如何在 VC 中進行多程序的程式設計,主要介紹如何建立新程序、終止
已有程序並設定程序的優先順序。
10.2.1 建立新程序
Windows 是以物件的方式來管理程序的,它由 Win32 子系統來建立和維護,並且可以
由 此 進 程 的 句 柄 來 進 行 管 理 。 進 程 的 創 建 一 般 是 在 一 個 進 程 的 線 程 中 調 用 函 數
CreateProcess( )來建立的,這個程序可以和原程序共享資源(例如控制代碼和變數),而且在
Windows 中,這兩個程序不存在的父子關係,即使原程序終止後,這個新程序仍然可以繼
續執行。
在介紹建立函式之前,先來看幾個相關的資料結構。
1. 資料結構
(1) SECURITY_ATTRIBUTES 結構
該 結 構 存 放 一 個 對 象 的 安 全 描 述 符 並 指 定 是 否 繼 承 返 回 的 句 柄 。SECURITY_
ATTRIBUTES 結構定義如下。
typedef struct_SECURITY_ATTRIBUTES{
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
}SECURITY_ATTRIBUTES
其中成員含義如下。
① nLength:指定該結構大小。
② lpSecurityDescriptor:指向一個物件的安全描述符,該安全描述符控制物件的共享。
如果該成員置為 NULL,則該物件使用呼叫程序的預設安全描述符。
③ bInheritHandle:指定新程序被建立時是否繼承返回的控制代碼。若該成員置為 TRUE,
則新程序繼承該控制代碼。
(2) STARTUPINFO
該結構用於指定新程序的主視窗特性。STARTUPINFO 結構定義如下。
typedef struct_STARTUPINFO
{
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute; ·264· Visual C++程式設計教程與上機指導
·264·
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserverd2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
}STARTUPINFO,*LPSTARTUPINFO;
其中成員含義如下。
① cb:指定該結構大小。
② lpReserved:保留,置為 NULL。
③ lpDesktop:指定一個字串,包括該程序的桌面名或視窗位置名。
④ lpTitle:指定控制檯程序建立的新控制檯視窗標題。
⑤ dwX,dwY:指定新視窗左上角的 x 和 y 偏移量(以畫素為單位)。如果 dwFlags 成員
未指定 STARTF_USEPOSITION 標誌,則忽略這兩項。
⑥ dwXSize,dwYSize: 指 定 新 窗 口 的 寬 度 和 高 度 。 如 果 dwFlags 成 員 未 指 定
STARTF_USESIZE 標誌,則忽略這兩個成員。
⑦ dwXCountChars,dwYCountChars:指定新控制檯視窗的螢幕緩衝區的寬度和高度。
如果 dwFlags 成員未指定 STARTF_USECOUNTCHARS 標誌,則忽略這兩成員。
⑧ dwFillAttribute:指定新控制檯視窗的初始文字和背景顏色。如果 dwFlags 成員未
指定 STARTF_USEFILLATTRIBUTE 標誌,則忽略該成員。
⑨ dwFlags:建立視窗標誌。
⑩ wShowWindow: 新 窗 口 的 顯 示 狀 態 。 如 果 dwFlags 成 員 未 指 定 STARTF_
USESHOWWINDOWW 標誌,則忽略該成員。
cbReserved2:保留,必須置為 0。
lpReserved2:保留,必須置為 NULL。
hStdInput:指定一個控制代碼,該控制代碼用作程序的標準輸入控制代碼。如果 dwFlags 成員未
指定 STARTF_USESTDHANDLES 標誌,則忽略該成員。
hStdOutput:指定一個控制代碼,該控制代碼用作程序的標準輸出控制代碼。如果 dwFlags 成員
未指定 STARTF_USESTDHANDLES,則忽略該成員。
hStdError:指定一個控制代碼,該控制代碼用作程序的標準錯誤控制代碼。如果 dwFlags 成員未
指定 STARTF_USESTDHANDLES,則忽略該成員。
(3) PROCESS_INFORMATION 結構
該結構返回有關新程序及其主執行緒的資訊。其結構定義如下。
typedef struct_PROCESS_INFORMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION;
其中成員含義如下。第 10 章 多執行緒與網路程式設計初步 ·265·
·265·
① hProcess:返回新程序的控制代碼。
② hThread:返回主執行緒的控制代碼。
③ dwProcessId:返回一個全域性程序識別符號。該識別符號用於標識一個程序。從程序被
建立到終止,該值始終有效。
④ dwThreadId:返回一個全域性執行緒識別符號。該識別符號用於標識一個執行緒。從執行緒被創
建到終止,該值始終有效。
2.建立程序
可以使用 CreateProcess 函式建立一個新程序和它的主執行緒。該新程序執行指定的可執
行檔案,並且其獨立運行於呼叫程序。
CreateProcess 的函式原型為:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
其中引數含義如下。
(1) lpApplicationName:指定要執行的應用程式的名字,該名字可以是全路徑名。如果
該引數為 NULL,則程式名必須是 lpszCommandLine 指向的字串的第一個識別符號。該參
數通常置為 NULL,而將程式名和引數放在 lpszCommandLine 指定的字串中。
(2) lpCommandLine:是一個以 NULL 結尾的字串的指標,它指向命令列引數。引數
lpApplicationName 和 lpszCommandLine 不允許同時空,否則系統找不見新程序所對應的可
執行程式的檔名。
(3) lpProcessAttributes和lpThreadAttributes:它們指向SECURITY_ATTRIBUTES結構,
分別用來確定待建立的程序和待建立程序的主執行緒的安全屬性。如果使用預設安全屬性,
則該值為 NULL。
(4) bInheritHandles:用來確定新建的程序能否繼承產生它的程序的控制代碼。若它的值為
TRUE,則這個程序和執行緒所建立的控制代碼都可以被這個程序所建立的新程序所繼承,即繼
承的控制代碼和原來的控制代碼有相同的值和存取許可權。
(5) dwCreationFlags:該引數決定新程序產生的方式,它可以用邏輯或(|)的方式把下列
值結合起來。
① CREATE_NEW_CONSOLE:為新程序建立一個新的控制檯視窗。
② DETACHED_PROCESS:在預設情況下,新程序使用的是父程序的控制檯視窗
③ CREATE_NEW_PROCESS_GROUP:這個新程序將是一個新程序組的根,程序組·266· Visual C++程式設計教程與上機指導
·266·
包括該程序的所有子程序。
④ CREATE_SUSPENDED:新程序的主執行緒被建立在掛起狀態,直到 Resume_Thread
函式被呼叫後才執行。
⑤ DEBUG_PROCESS:如果設定該標誌,呼叫該程序被當作除錯者,新程序準備接
收除錯。系統把在程序被除錯時所發生的除錯事件通知給父程序。
還有其他的值,可以在使用時參閱 MSDN 來學習。
(6) lpEnvironment:指向一個用於新程序的環境塊。如果該引數為 NULL,則新程序繼
承呼叫程序的環境。
(7) lpCurrentDirectory:指 向 新 進 程的 當 前 驅 動器 和 目 錄 的字 符 串 。 如果 該 參 數 為
NULL,則使用呼叫程序的當前驅動器和目錄。
(8) lpStartupInfo:指向一個 STARTUPINFO 結構,使用者說明如何顯示新程序的主視窗。
(9) lpProcessInformation:指向一個 PROCESS_INFORMATION 結構,用於接收有關新
程序的標識資訊。
函式 CreateProcess( )呼叫成功,返回值為 TRUE,否則為 FALSE。
10.2.2 程序的管理
程序被建立之後,就要對其進行管理,比如改變程序的優先順序。而要管理程序,首先
要取得這個程序的控制代碼或程序 ID。
1.取得程序的控制代碼或 ID
函式 GetCurrentProcess 可以取得當前程序的控制代碼,其原型為:
HANDLE GetCurrentProcess(VOID)
這個函式返回一個指向當前程序的控制代碼,但這是一個偽控制代碼,即僅僅只能開啟此程序
物件,增加此物件的引用計數,這個偽控制代碼只能在當前程序中使用,而不能在其他程序中
利用此控制代碼對這個程序進行操作。若在別的程序中對當前這個程序進行操作,可呼叫函式
DuplicateHandle( )把這個偽控制代碼轉換成一個真正的控制代碼。
函式 DWORD GetCurrentProcessID(VOID)可以取得當前程序的 ID,有一些 API 函式需
要用到程序 ID。
2. 取得和設定程序的優先順序
Windows 支援 4 種不同的優先順序:實時(Realtime)、高(High)、變通(Normal)和空閒(Idle),
預設情況下程序的優先順序為普通優先順序。
在 程 序 中 可 以 使 用 相 應 的 參 數 來 設 置 進 程 的 優 先 級 , 它 們 是 :HIGH_PRIORITY_
CLASS(高),IDLE_PRIORITY_CLASS(空 閒),NORMAL_PRIORITY_CLASS(普 通),
REALTIME_PRIORITY_CLASS(實時)。
一般程序的優先順序預設為普通級,除非這個程序的父程序的優先順序為空閒。程序優先
級的設定很重要,對於高優先順序,這個程序的執行緒將佔據幾乎所有的 CPU 時間。而對於 一
個空閒優先順序的程序,其執行緒只有當 CPU 空閒時才開始執行,例如螢幕保護程式的優先順序
就為空閒的優先順序,若使用者閒著,則會啟動這個螢幕保護程式。對於實時優先順序程序,一第 10 章 多執行緒與網路程式設計初步 ·267·
·267·
般不作設定,因為在這種情況下,其他程序都不會執行,如果這個程序不結束,其結果和
宕機一樣。
CreateProcess( )函式允許父程序指定其子程序的優先順序類別。在執行過程中可使用
SetPriorityClass( )函式動態改變程序的優先順序。另外可使用 GetPriorityClass( )獲取程序的優
先級。如使用 DWORD m_pri=GetPriorityClass(GetCurrentProcess( )),可取得當前程序的
優先順序。
10.2.3 終止程序
父程序可以使用 ExitProcess( )函式或 TerminateProcess( )函式終止子程序的執行。這兩
者之間的區別是:ExitProcess 函式將通知所有附屬 DLL 終止並保證程序的全部執行緒都終
止,而且只能終止當前程序;而 TerminateProcess 函式在終止程序時,並不通知所屬 DLL,
除非不得已,不要使用它來終止程序,因為它會導致其附屬的 DLL 程式不能完成一些正常
的資料重新整理工作,該函式不僅能終止當前程序,還能終止其他的程序。
值得注意的是,終止一個程序並不會引起子程序的終止,而只是該程序及其所有執行緒
的終止。
ExitProcess( )函式原型為:
void ExitProcess(UNIT uExitCode)
其中 uExitCode 為程序碼。
TerminateProcess( )函式原型為:
BOOL TerminateProcess(HANDLE hProcess,UNIT uExitCode)
其中,引數 hProcess 標識要終止的程序,uExitCode 為程序的退出碼。
應用程式可以使用 GetExitCodeProcess 返回程序的終止狀態。如果程序還在執行,則
終止狀態為 STILL_ACTIVE。如果程序終止,則終止狀態為程序退出碼。
10.2.4 建立程序例項程式
1. 程式功能
該程式是一個基於對話方塊的程式,使用者可在對話方塊的編輯框中輸入要開啟的可執行程
序的檔名,可以使用瀏覽按鈕來查詢可執行檔案,這要用到檔案【開啟】的通用對話方塊。
2. 程式步驟
(1) 新建一個工程,在第 1 步選用 Dialog Based,然後單擊【完成】按鈕。
(2) 設計對話方塊模板,在對話方塊上擺放控制元件,其佈局如圖 10.1 所示。
(3) 為編輯框控制元件對映 CString 型別的變數 m_strFileName 後,為【瀏覽…】按鈕對映
BN_CLICKED 訊息,併為訊息處理程式編寫程式碼,其程式碼如下。
void CMyDlg::OnBrowse()
{
//構造通用“開啟”檔案對話方塊物件,使其能過濾可執行檔案或所有檔案
CFileDialog dlg(TRUE,NULL,NULL,OFN_HIDEREADONLY
|OFN_OVERWRITEPROMPT,"程式檔案|*.exe;*.com;*.bat|所有檔案(*.*)|*.*||"); ·268· Visual C++程式設計教程與上機指導
·268·
if(dlg.DoModal()==IDOK)
{
m_strFileName=dlg.GetPathName(); //將使用者指定的檔名存入編輯框變數中
UpdateData(FALSE); //在編輯框控制元件中顯示檔名
}
}
圖 10.1 建立程序例項對話方塊模板
(4) 為【執行】按鈕對映 BN_CLICKED 訊息,在其訊息處理程式中建立新程序,執行
使用者指定的程式檔案。
void CMyDlg::OnRun()
{
STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInformation;
//設定程序視窗資訊結構 STARTUPINFO
StartupInfo.cb=sizeof(STARTUPINFO);
StartupInfo.lpReserved=NULL;
StartupInfo.lpDesktop=NULL;
StartupInfo.lpTitle=NULL;
StartupInfo.dwFlags=STARTF_USESHOWWINDOW;
StartupInfo.cbReserved2=0;
StartupInfo.lpReserved2=NULL;
StartupInfo.wShowWindow=SW_SHOWNORMAL; //正常尺寸顯示視窗
char filename[255];
sprintf(filename,"%s",m_strFileName);
//建立以 filename 為名的可執行程式
BOOL bReturn=CreateProcess(NULL,filename,NULL,NULL,FALSE,0,NULL,NULL,
&StartupInfo,&ProcessInformation);
if(!bReturn)
{
MessageBox("建立失敗");
(GetDlgItem(IDC_FILENAME))->SetFocus();
}
}
該程式的執行效果見圖 10.2。第 10 章 多執行緒與網路程式設計初步 ·269·
·269·
圖 10.2 建立程序執行效果圖
圖中編輯框中是要執行的程式名稱,下方是單擊【執行】按鈕後執行的記事本程式,
當使用【關閉】按鈕關閉建立程序程式時,開啟的記事本不會被關閉。
10.3 Windows 的多執行緒程式設計
10.3.1 執行緒概念
執行緒是 Windows 引入的先進技術之一。執行緒是 Windows 的唯一執行單位,是 Windows
為程式分配 CPU 時間的基本實體。每個程序都由一個或多個執行緒組成,由各執行緒協同完成
指定操作。
執行緒是程序內部的可獨立執行的單元,它是系統分配 CPU 時間資源的基本單元。執行緒
的概念與子程式的概念類似,是一個可獨立執行的子程式。一個應用程式可以建立多個線
程,多個不同的執行流,並同時執行這些執行緒。
多執行緒提高了系統響應能力及平滑的後臺處理。例如,一個字處理程式(程序)可以通
過使用多執行緒來加強操作並簡化與使用者的互動。該應用程式可以包含 3 個執行緒,第 1 個線
程可以用於響應使用者的鍵盤輸入訊息,將字元放入文件中;第 2 個執行緒可以執行拼寫檢查
及分頁等後臺操作;第 3 個執行緒可以在後臺將文件送到印表機列印。
雖然多執行緒會給應用開發帶來許多好處,但並非任何情況下都要使用多執行緒,這要依
據實際需要來綜合考慮。一般在以下情況下可以考慮使用多執行緒。
(1) 完成多個相同或類似的任務,如伺服器接收客戶端請求,將網頁傳送給客戶等。
(2) 處理多個視窗的輸入。
(3) 管理來自多個通訊裝置的輸入。
(4) 需要執行不同優先順序的任務。·270· Visual C++程式設計教程與上機指導
·270·
對於應用程式來說,一般通過在一個程序中建立多個執行緒來完成多工更有效,而不
是通過建立多個程序來完成多工。
MFC 支援多執行緒應用程式的開發,應用程式的每個執行緒都是一個 CWinThread 物件,
MFC 將執行緒劃分為兩種型別:工作者執行緒(Worker Thread)和使用者介面執行緒(User_interface
Thread),這兩種型別都基於 CWinThread。
如果執行緒需要執行後臺計算而不需要與使用者互動,那麼該執行緒為工作者執行緒。工作者
執行緒沒有訊息迴圈,不處理視窗訊息,用於在後臺執行任務。該類執行緒是最常用的型別。
如果要處理使用者輸入並響應由使用者產生的事件和訊息,那麼應該建立一個使用者介面線
程,它是通過自己的訊息泵獲取從系統接收訊息。主執行緒本身就是一個使用者介面執行緒,這
是因為 CWinApp 派生於 CWinThread。使用者可從 CWinThread 派生出自己的類來實現使用者
介面執行緒。
10.3.2 建立執行緒
建立執行緒主要有以下 3 種方法:
(1) Windows 的 API 函式 CreateThread;
(2) MFC 全域性函式 AfxBeginThread;
(3) MFC 的 CWinThread 類的 CreateThread 成員函式。
以下就具體介紹這 3 種執行緒的建立方法。
1.使用 API 的 CreateThread( )函式
CreateThread( )函式建立程序的一個新執行緒。該函式的原型為:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
其中引數含義如下。
(1) lpThreadAttributes:指向一個 SECURITY_ATTRIBUTES 結構,用於指定執行緒的安
全屬性。如果使用預設安全屬性,則置為 NULL。
(2) dwStackSize:指定執行緒用於堆分配堆疊的大小。如果為 0,則堆疊大小預設為和該
程序的主執行緒的堆疊大小相同。
(3) lpStartAddress:指向新執行緒執行程式碼的開始地址,通常為包含執行緒程式碼的執行緒函
數名。
(4) lpParameter:指定傳遞給執行緒函式的 32 位引數值。
(5) dwCreationFlags:執行緒建立標誌。如果為 CREATE_SUSPENDED,則該執行緒建立
在掛起狀態,直至呼叫 ResumeThread 函式後才執行;如果為 0,則建立後立即執行。
(6) lpThreadId:指向一個 32 位變數,用於接收該執行緒的識別符號。
如果該函式呼叫成功,則返回新執行緒的控制代碼,否則返回 NULL。第 10 章 多執行緒與網路程式設計初步 ·271·
·271·
【例 10.1】 使用 CreateThread( )函式建立執行緒例項。
//執行緒主體函式
UNIT ThreadProc(LPVOID pParam)
{//執行緒的實際程式碼
return 0;
}
HANDLE hThread;
DWORD dwThreadID;
DWORD dwParam;
hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,
wParam,0,&dwThreadID);
if(hThread= =NULL)
MessageBox("建立執行緒錯誤");
2. 使用 AfxBeginThread
使用 AfxBeginThread 函式既可建立工作者執行緒又可建立使用者介面執行緒。該函式有兩種
格式,第一種格式用於建立工作者執行緒,其中引數 pfnThreadProc 指向執行緒函式,pParam
為傳遞給執行緒函式的引數。第二種格式用於建立使用者介面執行緒,其中引數 pThreadClass 為
CwinThread 派生物件的 RUNTIME_CLASS。
格式 1:
CWinThread *AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
);
格式 2:
CWinThread *AfxBeginThread(
CRuntimeClass *pThreadClass,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
);
該函式的返回值為指向新建執行緒物件的指標。例 10.2 為使用 AfxBeginThread 來建立
程序的例項。
【例 10.2】 使用 AfxBeginThread 來建立工作者執行緒和使用者介面執行緒。
//執行緒函式
UNIT ThreadProc(LPVOID pParam)
{ //執行緒程式碼
return 0;
}
//在呼叫程序中建立工作者執行緒·272· Visual C++程式設計教程與上機指導
·272·
CWinThread *pThread;
DWORD dwParam;
pThread=AfxBeginThread(ThreadProc,&dwParam);
if(pThread==NULL)
{ MessageBox("建立錯誤");
//錯誤處理 }
//建立使用者介面執行緒例
//定義 CWinThread 執行緒派生類
class CTestThread:public CWinThread
{ // }
//在呼叫程序中建立使用者介面執行緒
CTestThread *pTestThread;
pTestThread=(CTestThread
*)AfxBeginThread(RUNTIME_CLASS(CTestThread),
THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);
3.使用 CWinThread 類
CWinThread 是 MFC 提供的執行緒物件類,包括建立、管理和刪除的一系列成員變數和
成員函式。CWinApp 是 CWinThread 的派生類。
CWinThread 類支援工作者執行緒和使用者介面執行緒。可以將一個指向 CWinThread 派生類
的 CRuntimeClass 的指標作為引數傳遞給 AfxBeginThread 函式以建立一個使用者介面執行緒。
CWinThread 類的 CreateThread 成員函式建立一個在呼叫程序的地址空間中執行的線
程。該函式原型為:
BOOL CreateThread(DWORD dwCreateFlags=0,UINT nStackSize=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);
若該函式成功建立了執行緒,返回非 0,否則返回 0。
【例 10.3】 使用 CWinThread 類來建立執行緒。
CWinThread thread; //建立 CWinThread 物件
thread.m_bAutoDelete=FALSE; //執行緒終止時不自動刪除該物件
thread.m_pfnThreadProc=ThreadProc; //設定執行緒函式
thread.m_pThreadParams=&dwParam; //傳遞給執行緒函式的引數
thread.CreateThrerad(); //建立執行緒,引數使用預設值。
10.3.3 掛起執行緒
使用 SuspendThread 和 ResumeThread 函式(Windows API 或 CWinThread 類成員函式),
執行緒可以掛起或恢復另一個執行緒的執行。當執行緒處於掛起狀態時,執行緒不會被排程執行。
SuspendThread 函式將當前執行緒的掛起次數加 1。若該值的掛起次數大於 0,則該執行緒不
執行。ResumeThread 函式將當前執行緒的掛起次數減 1。當該值為 0 時,執行緒恢復執行,否
則執行緒仍處於掛起狀態。如果將執行緒建立在掛起狀態,那麼在呼叫 ResumeThread 恢復執
行之前可以完成對執行緒狀態的初始化工作。
另外可通過呼叫 Sleep 或 SleepEx 函式暫時掛起當前執行緒一段指定時間。它常用於線
程與使用者的互動中,通過延遲執行執行緒足夠長的時間讓使用者觀察其結果。在睡眠期間執行緒
不會被排程執行。第 10 章 多執行緒與網路程式設計初步 ·273·
·273·
10.3.4 終止執行緒
可以呼叫ExitThread函式或TerminateThread函式終止執行緒的執行。與程序的終止相似,
一 般 情 況 下 使 用 ExitThread 函 數 來 終 止 線 程 , 只 有 在 不 得 已 的 情 況 下 才 使 用
TerminateThread 來終止執行緒。
可 以 調 用 全域性 函 數 AfxEndThread 或 者 使 用 return 語 句 來終 止 自 己 所在 的 線 程 。
AfxEndThread 函式的原型為 AfxEndThread(UINT nExitCode),其中引數 nExitCode 為執行緒
的退出碼,這個值為 0,表示成功,如果為其他值,那麼表示各種不同型別的錯誤。可以
通過函式 GetExitCodeThread 來獲取執行緒的退出碼。
10.4 Winsock 網路程式設計介面
10.4.1 WinSock 概述
Winsock 是一套開放的、支援多種協議的 Windows 下網路程式設計介面,是 Windows 網路
程式設計上的標準介面。應用程式通過呼叫 Winsock 的 API 實現相互之間的通訊,而 Winsock
利用下層的網路通訊協議功能和作業系統呼叫實現實際的通訊工作。
套接字(Sockets)是通訊端點的一種抽象,是支援 TCP/IP 協議網路通訊的基本操作單
元,它提供了一種傳送和接收資料的機制。在開發伺服器/客戶端應用程式時,可以利用
Sockets 實現資料結構或資料包的交換,以完成應用程式之間的通訊。
套接字一般有兩種型別:流套接字和資料報套接字。
流套接字提供雙向的、有序的、無重複並且無記錄邊界的資料流服務,它適用於處理
大量資料。流套接字是面向連線的,通訊雙方進行資料交換之前,必須建立一條路徑,類
似於打電話,首先要雙方能連線,才能繼續通話。這樣既確定了它們之間存在的路由,又
保證了雙方都是活動的、可彼此響應的。在資料傳輸過程中,如果連線斷開,則應用程式
會被通知,此時應用程式可以根據中斷原因作相應處理,在實際中,由於其可靠性高,流
式 Sockets 得到了廣泛應用。但在通訊雙方之間建立一個通訊通道需要很多開支。除此以
外,大部分面向連線的協議為保證傳送無誤,可能會需要執行額外的計算來驗證正確性,
因此會進一步增加開支。
資料報套接字支援雙向的資料流,但並不保證資料傳輸的可行性、有序性和無重複性。
也就是說,一個從資料報套接字接收資訊的程序有可能被發現資訊重複,或者和發出時的
順序不同的情況。此外,資料報套接字的一個重要特點是它保留了記錄邊界。資料報套接
字是無連線的,它不保證接收端是否正在偵聽,類似於郵政服務:發信人把信裝入郵箱即
可,至於收信人是否能收到這封信或郵局是否會因為暴風雨未能按時將信件投遞到收信人
處等,發信人都不得而知。因此,資料報並不十分可靠,需要程式設計師負責管理資料報的排
序和可靠性。應用程式具體可以採用的技術有:通過加流水號方式實現資料包的不丟失傳
輸,通過對資料包校驗實現正確傳輸,當出現傳輸錯誤時採用重發技術。當然讀者可以採
用自己的獨特方法來保證資料的穩定可靠傳輸。資料包的一個優點是:它提供了向多個目·274· Visual C++程式設計教程與上機指導
·274·
標地址傳送廣播資料包的功能。
10.4.2 Winsock 程式設計原理
1. 簡單客戶機/伺服器
進入 20 世紀 90 年代以後,隨著計算機和網路技術的發展,很多資料處理系統都採用
開放系統結構的客戶機/伺服器(Client/Server)網路模型,即客戶機向伺服器提出請求,服務
器對請求做相應的處理並執行被請求的任務,然後將結果返回給客戶機。
客戶機/伺服器模型工作時要求有一套為客戶機和伺服器所共識的慣例來保證服務能
夠被提供(或被接受),這一套慣例包含一套協議,它必須在通訊的兩端都被實現。根據不
同的實際情況,協議可能是對稱的或是非對稱的。在對稱的協議中,每一方都有可能扮演
主從角色,如 Internet 協議中的 Telnet 協議;在非對稱協議中,一方不可改變地被認為是
主機,而另一方是從機,如 Internet 中的 Http 協議。無論具體的協議是對稱的還是非對稱
的,當服務被提供時必須存在客戶程序和服務程序。
一個服務程式通常在一個眾所周知的地址監聽客戶對服務的請求,也就是說,服務進
程一直處於休眠狀態,直到一個客戶對這個服務提出了連線請求。在這個時刻,服務程式
被“驚醒”並且為客戶提供服務——對客戶的請求作出適當的反應。
2.Winsock 的啟動和終止
由於 Winsock 的服務是以動態連結庫 Winsock DLL 形式實現的,所以必須先呼叫
WSAStartup 函式對 Winsock DLL 進行初始化,協商 Winsock 的版本支援,並分配必要的
資源。如果在呼叫 Winsock 函式之前,沒有載入 Winsock 庫,則會返回 SOCKET_ERROR
錯誤,錯誤的資訊是 WSANOTINITIALIZED。WSAStartup 函式原型為:
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中,引數 wVersionRequested 指定應用要使用的 Windows Sockets 最高版本,其中高
位位元組表示輔版本號,低位位元組表示主版本號。目前使用最廣泛的是 Windows Sockets 1.1
版本,最高版本已經是 2.0 版本。一般用巨集 MAKEWORD(X,Y)獲得 wVersionRequested 的
正確值。如 MAKEWORD(2,0)表示使用 Windows Sockets2.0 版本。
引數 lpWSAData 指向 WSADATA 結構的指標。該結構包含了載入的庫版本的有關的
資訊。
該函式成功則返回 0,失敗則返回如下可能值。
(1) WSASYSNOTREADY:表示網路裝置沒有準備好。
(2) WSAVERNOTSUPPORTED:Winsock 的版本資訊號不支援。
(3) WSAEINPROGRESS:一個阻塞式的 Winsock1.1 存在於程序中。
(4) WSAEPROCLIM:已經達到 Winsock 使用量的上限。
(5) WSAEFAULT:lpWSAData 不是一個有效的指標。
此外,在應用程式關閉套接字後,還應呼叫 WSACleanup 函式終止對 Winsock DLL 的
使用,並釋放資源,以備下一次使用。WSACleanup 函式的原型為:
int WSACleanup(void); 第 10 章 多執行緒與網路程式設計初步 ·275·
·275·
該函式不帶任何引數,若呼叫成功則返回 0,否則返回錯誤。
3.錯誤的檢查和控制
錯誤檢查和控制對於編寫成功的 Winsock 應用程式是至關重要的。事實上,對 Winsock
API 函式來說,返回錯誤是很常見的,但是多數情況下,這些錯誤都是無關緊要的,通訊
仍可在套接字上進行。儘管返回的值並非一成不變,但不成功的 Winsock 呼叫返回的最常
見的值是 SOCKET_ERROR。SOCKET_ERROR 是值為-1 的常量。如果錯誤情況發生了,
就可用 WSAGetLastError 函式來獲得一段程式碼,這段程式碼明確地表明產生錯誤的原因。該
函式的原型為:
int WSAGetLastError(void);
WSAGetLastError 函式返回的錯誤都是預宣告的常量值,根據 Winsock 版本的不同,
這些值的宣告不在 Winsock1.h 中就會在 Winsock2.h 中。為各種錯誤程式碼宣告的常量一般
都以 WSAE 開頭。
4. Winsock 程式設計模型
不論是流套接字還是資料報套接字程式設計,一般都採用客戶機/伺服器方式,它們的過程
基本類似,下面著重介紹流套接字的程式設計模型。
(1) 流套接字程式設計模型
考慮使用電話進行通訊的過程:如果想要使用電話進行通話,首先雙方必須安裝電話
機,並由一方撥號與另一方建立連線,然後可以通過電話聽取對方的聲音,或者向對方講
話,最後關閉連線。流套接字的過程與打電話的過程非常相似,服務程序和客戶程序在通
信前必須建立各自的套接字並建立連線,然後才能對相應的套接字進行讀、寫操作,以實
現資料的傳輸。具體程式設計步驟如下。
① 伺服器程序建立套接字。服務程序總是先於客戶程序啟動,服務程序首先呼叫
socket 函式建立一個流套接字,socket 函式的原型為:
SOCKET socket(int af,int type,int protocol);
其中引數 af 指定網路地址型別,一般都取 AF_INET,表示是在 Internet 上的 Socket。
引數 type 用於指定套接字型別,當採用流連線方式時用 SOCK_STREAM,用資料報方式
時用 SOCK_DGRAM。protocol 用於指定網路協議,一般都為 0,表示用對流套接字採用默
認的 TCP 協議,資料報套接字採用預設的 UDP 協議。函式的返回值是 Winsock 定義的一
種資料型別 SOCKET,它實際就是個整型資料,在 Socket 建立成功時,代表 Winsock 分配
給程式的 Socket 編號,後面呼叫傳輸函式時,就可以把它像檔案指標一樣引用。如果 Socket
建立失敗,返回值為 INVALID_SOCKET。
② 將本地地址繫結到所建立的套接字上以使在網路上標識該套接字。在成功建立了
Socket 之後,就應該選定通訊的物件。首先是自己的程式要與網上的哪臺計算機通話;其
次,在多工系統下,該臺計算機上可能會有幾個程式在工作,必須指出要與哪個程式通
信。前者可以通過 Internet 的網路 IP 地址來確定,而後者則由埠號來確定。用埠號來
表示同一臺計算機上不同的應用程式,埠號可以為 0~65535,不同功能的通訊程式使用
不同的埠號,如 pop3 協議使用 110 埠,http 協議使用 80 埠等,這樣一臺計算機上·276· Visual C++程式設計教程與上機指導
·276·
可以有幾個程式同時使用一個 IP 地址通訊而不互相干擾,IP 地址與埠號的關係好像電
話總機號碼與分機號碼的關係一樣。因為一些常用的網路服務往往佔據了 1024 以下的埠
號,所以編制自己的通訊程式時,應指定大於 1024 的埠號。
一般該過程是通過函式 bind 來完成的,該函式的原型為:
int bind(SOCKET s,struct sockaddr_in * name,int namelen);
其中引數 s 是已經建立好的套接字。name 是指向描述通訊物件地址資訊的結構體的指
針,namelen 是該結構體的長度。結構體 sockaddr_in 的定義如下。
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中,sin_family 是指一套地址族,通常被設為 AF_INET;sin_port 是指埠號;sin_addr
是指 IP 地址;sin_zero[8]主要是使該結構的大小和 SOCKADDR 大小相同(SOCKADDR 結
構由一個無符號 short 型和一個長度為 14 的 char 型陣列構成,這個結構一共是 16 個位元組),
在 sockaddr_in 中新增這個長度為 8 的陣列,使 sockaddr_in 的長度也為 16(2+2+4+8),這 樣
做的目的是使地址操作更方便。該函式如果呼叫失敗,會返回 SOCKET_ERROR。對 bind
來說,最常見的錯誤是 WSAEADDRINUSE。如果使用的是 TCP/IP,那麼該錯誤表示另一
個 進 程 已 經 同 本 地 IP 接 口 和 端 口 號 綁 定 到 了 一 起 , 或 者 那 個 IP 接 口 和 端 口 號 處 於
TIME_WAIT 狀態。假如對一個已經繫結的套接字呼叫 bind,便會返回 WSAEFFAULT
錯誤。
③ 將套接字置入監聽模式並準備接受連線請求。bind 函式的作用只是將一個套接字
和一個指定的地址關聯在一起,讓一個套接字等候進入連線的 API 函式是 listen,其原
型為:
int listen(SOCKET s,int backlog);
其中引數 s 標識一個已繫結但未連線套接字的描述字。backlog 引數用於指定正在等待
連線的最大佇列長度,這個引數非常重要,因為完全可能同時出現幾個伺服器連線請求。
例如,假定 backlog 引數為 2,如果 3 個客戶機同時發出請求,那麼頭兩個會被放在一個等
待佇列中,以便應用程式依次為它們提供服務,而第 3 個連線請求(佇列已滿)會造成一個
WSAECONNREFUSED 錯誤,一旦伺服器接受了一個連線,那個連線請求就會被從佇列中
刪除,以便別人可繼續發出請求,backlog 引數本身是由基層的協議提供者決定的,如果出
現非法值,那麼會用與之最接近的一個合法值來取代。
如果無錯誤發生,listen 函式返回 0,若失敗則返回 SOCKET_ERROR 錯誤,最常見的
錯誤是 WSAFINVAL,該錯誤通常表示套接字在 listen 前沒有呼叫 bind。
進入監聽狀態之後,通過呼叫 accept 函式使套接字作好接受客戶連線的準備。Accept( )
函式的原型為:
SOCKET accept(SOCKET s,struct sockaddr *addr,int *addrlen); 第 10 章 多執行緒與網路程式設計初步 ·277·
·277·
其中引數 s 是處於監聽模式的套接字描述字。第 2 個引數是一個有效的 SOCKADDR
_IN 結構的地址,而 addrlen 是 SOCKADDR_IN 結構的長度。這樣,伺服器便可為等待連
接佇列中的第一個連線請求提供服務了。accept 函式返回,addr( )引數變數中會包含發出連
接請求的那個客戶機的 IP 地址資訊,而 addrlen 引數則指出該結構的長度,並返回一個新
的套接字描述字,它對應於已經接受的那個客戶機連線。對於該客戶機後續的所有操作,
都應使用這個新套接字,至於原來的那個監聽套接字,它仍然用於接受其他客戶機連線,
而且仍處於監聽模式。如果無連線請求,服務程序將被阻塞。
④ 客戶程序呼叫 socket 函式建立客戶端套接字。
⑤ 客戶向服務程序發出連線請求。通過呼叫 connect( )函式可以建立一個到服務程序
的連線。其中 s 是剛建立的套接字描述字,name 與 namelen 的含義和使用方法與 bind( )相
同,用來指定通訊物件。如果連線失敗,該函式會返回 SOCKET_ERROR。如果欲連線的
計算機沒有偵聽指定埠的這一程序,connect呼叫就會失敗,併發生錯誤 WSAECONNREF
USED。另一個常見的錯誤是 WSAETIMEOUT,表示連線超時。
⑥ 當連線請求到來後,被阻塞服務程序的 accept( )函式如③中所述即生成一個新的套
接字與客戶套接字建立連線,並向客戶返回接收訊號。
⑦ 一旦客戶機的套接字接收到來自伺服器的訊號,則表示客戶機與伺服器已實現連
接,即可以進行資料傳輸了。senD. recv 函式是進行資料收發的函式。它們的函式原型是:
int send(SOCKET s,char *buf,int len,int flags);
int recv(SOCKET s,char *buf,int len,int flags);
s 是已建立連線的套接字的描述字。buf 和 len 是傳送或接收的資料包及其長度,引數
flags 一般取 0。recv( )函式實際上是讀取 send( )函式發過來的一個數據包。當讀到的資料
位元組少於規定接收的數目時,就把資料全部接收,並返回實際收到的位元組數;當讀到的數
據多於規定值時,在流方式下剩餘的資料由下個 recv( )讀出。這兩個函式在出錯時都返回
SOCKET_ERROR。
⑧ 關閉套接字。一旦任務完成,就必須關閉連線,以釋放套接字佔用的所有資源。通
常呼叫 closesocket 函式即可達到目的,但 closesocket 可能會導致資料的丟失,因此應該在
呼叫該函式之前,先呼叫 shutdown 函式從容地中斷連線,即傳送端通知接收端“不再發送
資料”或接收端通知傳送端“不再接收資料”。
shutdown( )函式的原型為:
int shutdown(SOCKET s,int how);
其中,how 引數用於描述禁止哪些操作,它可取的值有:SD_RECEIVE、SD_SEND
或 SD_BOTH。如果是 SD_RECEIVE,就表示不允許再呼叫接收函式,這對底部的協議層
沒有影響;如果選擇 SD_SEND,表示不允許再呼叫傳送函式;如果指定 SD_BOTH,則表
示取消連線兩端的收發操作。如果沒有錯誤發生,則返回 0,否則返回 SOCKET_ERROR。
shutdown( )函式並不關閉套接字,且套接字所佔用的資源將被一起保持到closesocket( )
函式呼叫。closesocket( )函式的原型為:
int closesocket(SOCKET s);
其中,引數 s 是要關閉的套接字描述字,再利用套接字執行呼叫就會失敗,並出現·278· Visual C++程式設計教程與上機指導
·278·
WSAE_OTSOCK 錯誤。
圖 10.3 列出了流套接字程式設計的時序流程圖。
socket( )
bind( )
listen( )
socket( )
connect( )
recv( )
阻塞,等待客戶資料
recv( ) send( )
send( )
closesocket( ) closesocket( )
客戶端
建立連線
請求資料
應答資料
伺服器
accept( )
圖 10.3 流套接字程式設計時序流程圖
(2) 資料報套接字程式設計模型
資料報套接字是無連線的,它的程式設計過程比流套接字要簡單一些。
對於伺服器端,先用 socket( )函式建立套接字,再通過 bind( )函式進行繫結,但不需
要呼叫 listen( )和 accept( )函式,只需等待接收資料。由於它是無連線的,因此它可以接收
網路上任何一臺機器所發的資料包。常用的接收資料函式是 recvfrom( ),傳送函式是
sendto( ),它們的原型為:第 10 章 多執行緒與網路程式設計初步 ·279·
·279·
int recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr
*from,int *fromlen);
int sendto(SOCKET s,char *buf,int len,int flags,struct sockaddr_into,
int *tolen);
其中 recvfrom( )函式前 4 個引數和 recv( )函式一樣,而引數 from 是一個 SOCKADDR
結構指標,fromlen 引數是帶有指向地址結構長度的指標。當它返回資料時,SOCKADDR
結構內便填入傳送資料端的地址。Sendto( )函式的引數除了 buf 是指向傳送資料緩衝,len
是指傳送資料長度,sockaddr_into 是指接收資料端的地址外,其他與 recvfrom 相似。
10.4.3 用流套接字進行通訊的簡單例子
本節是使用流套接字進行簡單的網路通訊程式設計的例項。它主要建立一個伺服器程式和
一個客戶端程式,在建立連線後,由客戶端向伺服器發出訊息“來自伺服器”,伺服器在
收到訊息後顯示,並向客戶端傳送訊息“來自伺服器”,客戶端在接收後顯示。
1.伺服器程式的實現
該程式使用阻塞模式套接字實現,其步驟為如下。
(1) 建立一個基於對話方塊的 MFC AppWizard 工程。
(2) 在檔案 StdAfx.h 中的#endif 前面一行加入如下兩行程式碼以包含 Winsock 相關頭文
件及連線相應的庫檔案。
#include <winsock.h>
#pragma comment(lib,"wsock32")
(3)在 對 話 框 類 的 OnInitDialog( )函 數 中 初 始 化 Winsock, 將 下 面 代 碼 加 入 到
Cdialog::OnInitDialog( )下面。
WSADATA wsaData;
WORD version=MAKEWORD(2,0); //設定 winsock 版本為 2.0
int ret=WSAStartup(version,&wsaData); //初始化 Socket
if(ret!=0)
TRACE("initialize error.!");
(4) 為 OK 按鈕對映成員函式 OnOK( ),將本伺服器程式的主要通訊工作填加到該函式
中,其程式碼如下。
void CSocketAPIServerDlg::OnOK()
{
// TODO: Add extra validation here
SOCKET m_hSocket;
m_hSocket=socket(AF_INET,SOCK_STREAM,0); //建立套接字
//設定繫結地址
sockaddr_in m_addr;
m_addr.sin_family=AF_INET;
m_addr.sin_port=htons(5050);
m_addr.sin_addr.S_un.S_addr=INADDR_ANY;
·280· Visual C++程式設計教程與上機指導
·280·
int error=0;
//繫結套接字到本機
int ret;
ret=bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));
if(ret==SOCKET_ERROR)
{
TRACE("Bind Error:%d\n",(error=WSAGetLastError()));
return;
}
//開始一個偵聽過程,等待客戶的連線
ret=listen(m_hSocket,2); //最多支援客戶連線數為 2
if(ret==SOCKET_ERROR)
{
TRACE("Listen Error:%d\n",(error=WSAGetLastError()));
return;
}
//該函式阻塞,等待客戶的連線
SOCKET s=accept(m_hSocket,NULL,NULL);
if(ret==SOCKET_ERROR)
{
TRACE("Accept Error:%d\n",(error=WSAGetLastError()));
return;
}
//一旦有使用者連線,就等待使用者發來的請求資訊,該函式也阻塞
char buff[256];
ret=recv(s,buff,256,0);
if(ret==0||ret==SOCKET_ERROR)
{
TRACE("recv Error:%d\n",(error=WSAGetLastError()));
return;
}
buff[ret]='\0';
AfxMessageBox(buff);
//向客戶傳送訊息
ret=send(s,"來自伺服器",10,0);
if(ret==10)
AfxMessageBox("伺服器向客戶機發送成功");
else
{
TRACE("Send Error:%d\n",(error=WSAGetLastError()));
return;
}
CDialog::OnOK(); //此行程式碼為函式中原有程式碼
}
2. 客戶端程式的實現
該程式與伺服器程式一樣,必須做前 3 步的準備工作,接下來為 OK 按鈕對映成員函
數 OnOK( ),為其編寫程式碼如下。
void CSocketAPIClientDlg::OnOK() 第 10 章 多執行緒與網路程式設計初步 ·281·
·281·
{
SOCKET m_hSocket;
m_hSocket=socket(AF_INET,SOCK_STREAM,0);
ASSERT(m_hSocket!=NULL);
sockaddr_in m_addr;
m_addr.sin_family=AF_INET;
//改變埠號的資料格式,此埠號要與服務程式的埠口號一樣
m_addr.sin_port=htons(5050);
m_addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); //使用本機 IP 地址
int ret=0;
int error=0;
//連線伺服器
ret=connect(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));
if(ret==SOCKET_ERROR)
{
//連線失敗
TRACE("Connect Error:%d\n",(error=WSAGetLastError()));
if(error==10061) //該錯誤碼錶示伺服器沒有正常工作
AfxMessageBox(_T("請確認伺服器確實已開啟並工作在同樣的埠"));
return;
}
//向伺服器傳送資料
ret=send(m_hSocket,"來自客戶機",10,0);
if(ret==10)
AfxMessageBox("客戶端向伺服器傳送資訊成功");
else
{
TRACE("Send data error:%d\n",WSAGetLastError());
return;
}
char buff[256];
//從伺服器端接收資料
ret=recv(m_hSocket,buff,256,0);
if(ret==0)
{
TRACE("Recv data error:%d\n",WSAGetLastError());
return;
}
buff[ret]='\0';
AfxMessageBox(buff);
CDialog::OnOK();
}
該例項執行效果如圖 10.4 和 10.5 所示。·282· Visual C++程式設計教程與上機指導
·282·
圖 10.4 伺服器程式接收到來自客戶機資料執行效果圖
圖 10.5 客戶程式接收來自伺服器資料執行效果圖
10.5 MFC Socket 類
為了方便開發人員輕鬆開發網路應用程式,Visual C++MFC提供了相應的Socket類庫,
主要包括 CAsyncSocket 類、CSocket 類和 CSocketFile 類。
10.5.1 CAsyncSocket 類
CAsyncSocket 物件表示一個 Windows Socket,用於表示網路通訊。CAsyncSocket 類封
裝了 Windows Sockets API,使用面向物件技術,方便與 MFC 其他類庫一起程式設計。
1.建立 CAsyncSocket 物件
建立 CAsyncSocket 物件分為兩步:首先呼叫建構函式建立一個空白的 Socket 物件,
然後呼叫 Create 成員函式建立 SOCKET 資料結構並繫結地址。要注意的是,在伺服器端應第 10 章 多執行緒與網路程式設計初步 ·283·
·283·
用程式的接受請求處理函式中,偵聽 Socket 建立一個通訊 Socket 時,無須再呼叫 Create( )
函式。
Create 函式原型為
BOOL Create(UINT nSocketPort=0,int nSocketType=SOCK_STREAM,long lEvent
=FD_READ|FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT|FD_CLOSE,
LPCTSTR lpszSocketAddress=NULL);
其中,引數 nSocketPort 用於指定分配給 Socket 的埠號,預設為 0,表示由系統自動
分配;引數 nSocketType 預設值為流式 Socket;引數 lEvent 為位遮蔽碼,指定將為應用程
序生成通知的事件集合,預設情況下所有事件都會通知;引數 lpszSocketAddress 為繫結地
址,既可以設定計算機名,也可以設定 IP 地址,還可以是 DNS 地址,預設為本機地址。
該函式如果呼叫成功,則返回 TRUE;否則返回 FALSE。應用程式可以通過呼叫
GetLastError 函式獲取錯誤描述。
建立一個 Socket 之後,需呼叫 bind 成員函式將 Socket 與一個本地地址繫結。對於服
務器端偵聽 Socket 在接受客戶端連線請求前必須選擇一個埠號並繫結。
2.虛擬成員函式
為支援 lEvent 所代表的通知事件,MFC 通過提供虛擬成員函式輕鬆實現程式設計。例如網
絡應用程式要處理 FD_ACCEPT 事件,只需過載 OnAccept 函式即可。
(1) OnAccept:對應於 FD_ACCEPT,表示一個新的連線請求等待被接受。通過呼叫
Accept 成員函式對客戶端的連線請求進行響應。
(2) OnClose:對應於 FD_CLOSE,表示 Socket 已關閉。
(3) OnConnect:對應於 FD_CONNECT,表示 Socket 連線已完成,不論成功與否。它
在 Connect 成員函式被呼叫之後才被呼叫。
(4) OnOutOfBandData:對應於 FD_OOB,表示接收到帶外資料,該資料通常為緊急
資料。
(5) OnReceive:對應於 FD_READ,表示接收到新的資料,等待被裝入。通過呼叫 Receive
成員函式接收資料。
(6) OnSend:對應於 FD_WRITE,表示資料已準備好以進行傳送。通過呼叫 Send 成員
函式傳送資料。
3.建立連線
與 Windows Sockets API 建立網路應用程式相同,建立並繫結一個 Socket 之後,就需
要建立客戶端與伺服器之間的連線。
(1) 客戶端
對於流式 Socket 客戶端,使用 Connect 成員函式提出連線請求。它有以下兩種函式
原型。
BOOL Connect(LPCTSTR lpszHostAddress,UINT nHostPort);
BOOL Connect(const SOCKADDR *lpSockAddr,int nSockAddrLen);
其中,第一種形式的 Connect 函式引數 lpszHostAddress 為繫結地址,它是一個 ASCII·284· Visual C++程式設計教程與上機指導
·284·
字串,例如“sunzhiyue、122.1.6.20、ftp.microsoft.com”,引數 nHostPort 為埠號。第
二種形式的 Connect 函式引數與 Windows Sockets API 的 connect 函式含義相同。
該函式如果呼叫成功,則返回 TRUE;否則返回 FALSE。應用程式可以通過呼叫
GetLastError 函式以獲取錯誤描述。
(2) 伺服器端
當伺服器端建立並繫結偵聽 Socket 之後,就應呼叫 Listen 成員函式開始偵聽客戶端連
接請求。當接收到客戶端請求後,呼叫 Accept 成員函式響應請求。Accept 函式通常在
OnAccept 虛擬成員函式中呼叫。
Accept 函式執行成功,將返回一個用於與客戶端進行通訊的 Socket。
4.收發資料
一旦客戶端與伺服器成功建立 Socket 連線後,就可以進行相互通訊。MFC 提供了 4
個成員函式用於收發資料。
(1) Receive:從 Socket 接收到資料。
(2) ReceiveFrom:接收到一個數據報,並存儲原地址。
(3) Send:向一個 Socket 傳送資料。
(4) SendTo:向一個指定目的地傳送資料。
5.關閉 Socket
當 Socket 使用結束之後,就應該關閉 Socket。
(1) ShutDown:禁止傳送/接收資料。
(2) Close:關閉 Socket。
10.5.2 CSocket 類
CSocket 類是 CAsyncSocket 類的派生類,它繼承了 CAsyncSocket 對 Windows Sockets
API 的封裝。與 CAsyncSocket 物件相比,CSocket 物件代表了 Windows Sockets API 的更高
一級的抽象化,自動為應用程式處理阻塞呼叫。CSocket 與類 CSocketFile 和 CArchive 一起
來管理對資料的傳送和接收。
與 CAsyncSocket 類相同,要建立一個 CSocket 物件,首先需要呼叫建構函式,然後調
用 Create 成員函式建立。使用 CSocket 類實現客戶端和伺服器端建立 Socket 連線,以及數
據收發過程,與 CAsyncSocket 類類似,不同之處如下。
(1) CSocket 物件不再呼叫通知函式 OnConnect,僅僅呼叫 Connect 成員函式。但是調
用 Connect 函式時會發生阻塞,直到成功地建立了連線或有錯誤發生。呼叫 Connect 函式
的執行緒在 Connect 函式發生阻塞時仍能處理 Windows 的其他訊息。
(2) CSocket 物件不再呼叫通知函式 OnSend,僅僅呼叫 Send 成員函式。但是呼叫 Send
函式時會發生阻塞,直到所有資料都發送完畢。呼叫 Send 函式的執行緒在 Send 函式發生阻
塞時仍能處理 Windows 的其他訊息。
(3) 由 於 CSocket 類 是 CAsyncSocket 類 的 派 生 類 , 因 此 CSocket 對 象 也 能 使 用
Receive/Send 和 ReceiveFrom/SendTo 收發資料。除此之外,CSocket 物件可以和 CSocketFile
物件一起使用序列化方法收發資料。第 10 章 多執行緒與網路程式設計初步 ·285·
·285·
10.5.3 CSocketFile 類
CSocketFile 物件是一個用來通過 Windows Sockets 在網路中傳送和接收資料的 CFile
物件。網路應用程式可以通過該類簡化傳送和接收資料流程,這需要以下兩方面工作:一
是將 CSocketFile 物件與一個 CSocket 物件連線,二是將 CSocketFile 物件與一個 CArchive
物件連線。
一旦 CSocketFile 物件與 CSocket 物件和 CArchive 物件建立連線,CSocketFile 物件使
用方法與一般的 CFile 物件使用方法就基本類似。即利用 CArchive 物件來發送和接收資料。
CArchive 物件的操作符(<<和>>)可實現對檔案檔案的讀或寫入資料,即接收或傳送
資料。
10.6 上 機 指 導
程式設計實現一個基於 Client/Server 模式的小型自動回覆系統,包括客戶端和伺服器端兩
部分,使用者通過客戶端傳送訊息,伺服器端在收到訊息後,查詢對應的回覆資訊,發回給
客戶端後斷開連線。
目的:掌握 Winsock API 進行網路通訊,及伺服器端多執行緒技術。
實現步驟如下:
1.伺服器程式的實現
(1) 生成一個基於對話方塊的 MFC AppWizard 工程 AutoReplyServer,在對話方塊上放
置兩個按鈕控制元件,一個為“開啟伺服器”,ID 為 IDC_START;另外一個為“關閉伺服器”,
ID 為 IDC_END。使用 ClassWizard 為這兩個按鈕對映訊息處理函式 OnStart( )和 OnEnd( )。
(2) 在 StdAfx.h 中#endif 前新增下面的程式碼。
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
(3) 在 AutoReplyServerDlg.cpp 檔案中所有函式外,加入以下幾個全域性變數。
SOCKET g_hSocket=NULL;
SOCKET g_hAcceptSocket=NULL;
CMapStringToString mapReply; //存入自動回覆的資訊
(4) 在 AutoReplyServerDlg.h 中加入如下的巨集定義。
#define CONNECT_PORT 8080
(5) 在 CAutoReplyServerApp 類中 InitInstance 函式中新增如下程式碼。
//設定幾個靜態的回覆訊息
mapReply.SetAt("Hello Server","Hello Client");
mapReply.SetAt("First From Client","First From Server");
mapReply.SetAt("Second From Client","Second From Server");
mapReply.SetAt("GoodBye","Bye"); ·286· Visual C++程式設計教程與上機指導
·286·
(6) 在 CAutoReplyServerDlg 類中的 OnStart( )函式中編寫程式碼,程式碼如下。
void CAutoReplyServerDlg::OnStart()
{
//填寫 sockaddr_in 結構
sockaddr_in sa_addr;
sa_addr.sin_family=AF_INET;
sa_addr.sin_port=htons(CONNECT_PORT);
sa_addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
ASSERT(g_hSocket==NULL);
WORD version;
WSADATA wsaData;
int nErr;
version=MAKEWORD(2,0);
//載入所需的 Winsock dll 版本
nErr=WSAStartup(version,&wsaData);
if(nErr)
{
AfxMessageBox("載入 Winscok Dll 出錯");
return;
}
//建立 Socket 套接字
if((g_hSocket=socket(AF_INET,SOCK_STREAM,0))==INVALID_SOCKET)
{
AfxMessageBox("建立 Socket 出錯");
return;
}
//繫結地址
if(bind(g_hSocket,(sockaddr *)&sa_addr,sizeof(SOCKADDR))==
SOCKET_ERROR)
{
AfxMessageBox("bind 函式執行綁定出錯");
return;
}
//監聽客戶端連線請求
if(listen(g_hSocket,5)==SOCKET_ERROR)
{
AfxMessageBox("監聽客戶端連線失敗");
return;
}
//啟動執行緒來處理客戶端的通訊請求
AfxBeginThread(ServerThreadProc,0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
}
(7) 編制伺服器執行緒函式,用來處理與客戶端的通訊,其函式程式碼如下。
UINT ServerThreadProc(LPVOID pParam)
{
sockaddr_in sa_addr;
ASSERT(g_hSocket!=NULL); 第 10 章 多執行緒與網路程式設計初步 ·287·
·287·
int nLen=sizeof(SOCKADDR);
//等待接受客戶端的連線請求
g_hAcceptSocket=accept(g_hSocket,(sockaddr *)&sa_addr,&nLen);
if(g_hAcceptSocket==INVALID_SOCKET)
{
if(WSAGetLastError()!=WSAEINTR)
AfxMessageBox("接受連線失敗");
return 1;
}
//接受到一個客戶端的連線請求後,立即啟動一執行緒重新開始監聽
AfxBeginThread(ServerThreadProc,pParam);
//處理與客戶端的通訊
char sCommand[300];
memset(sCommand,0,300);
int nRecv;
//從客戶端接收資料
if((nRecv=recv(g_hAcceptSocket,sCommand,300,0))==SOCKET_ERROR)
{
AfxMessageBox("接收資料失敗");
return 1;
}
if(nRecv==0) return 1;
sCommand[nRecv]='\0';
CString strCommand;
strCommand.Format("%s",sCommand);
CString Reply;
//根據接收到的客戶端資訊查詢其回覆資訊
mapReply.Lookup(strCommand,Reply);
char sBuff[100];
sprintf(sBuff,"%s",Reply);
int nByteSent;
//將回覆信息傳送給客戶端
nByteSent=send(g_hAcceptSocket,sBuff,Reply.GetLength(),0);
if(nByteSent==SOCKET_ERROR)
{
AfxMessageBox("傳送資料失敗");
return 1;
}
//關閉套接字
if(closesocket(g_hAcceptSocket)==SOCKET_ERROR)
{
AfxMessageBox("關閉連線失敗");
g_hAcceptSocket=NULL;
return 1;
}
return 0;
} ·288· Visual C++程式設計教程與上機指導
·288·
(8) 為“關閉伺服器”按鈕對映訊息,並編寫程式碼如下。
void CAutoReplyServerDlg::OnEnd()
{
// TODO: Add your control notification handler code here
if(g_hSocket= =NULL) return;
VERIFY(closesocket(g_hSocket)!=SOCKET_ERROR);
g_hSocket=NULL;
}
2. 客戶端程式實現
(1) 利用 AppWizard 生成一個基於對話方塊的工程,工程名為 AutoReplyClient。
(2) 在對話方塊上放置一個列表框控制元件和一個按鈕“傳送資料”,使用 ClassWizard 為列
表框控制元件對映 CListBox 型別的變數 m_list,用
教學提示:Windows 是一個支援多工的作業系統。當在一個程式中需要啟動另外一
個程式時,需要用到多程序的程式設計方式。如果一個程序中有一些相似的任務需要同時推進,
可以為每個任務建立一個執行緒,從而形成多執行緒的程式設計。隨著網路技術的廣泛應用,網路
程式設計也越來越受到重視,網路程式設計主要使用 Winsock 技術。
教學目標:掌握程序的建立與終止及相應的管理,瞭解執行緒的基本概念,並掌握執行緒
的建立及使用。能夠使用 Winsock 進行簡單的網路程式設計。
10.1 Windows 的多工
Windows 是一個支援多工的作業系統。現在可以在欣賞電腦播放 CD 音樂的同時,
一邊列印檔案,一邊編輯檔案,這在以前的 DOS 作業系統的時候是不可能的。因為 DOS
是一個單使用者、單任務的作業系統,一個時間段內只能執行一道程式。而 Windows 環境下
卻可以做到這點,這都是得益於 Windows 的多程序處理及多執行緒處理功能。除了上述所說
的多工的優點,再來看一下網路應用盛行的當今時代,多工給我們帶來的益處。作為
一個網路伺服器,比如搜狐網站,每個時刻都要接收來自客戶端的數量巨大的網路服務請
求,如果沒有多工環境的支援,而是處理完一個請求後再處理下一個,這樣大家在上網
時就得在自己的機器前坐等其他的請求處理完後再得到響應。但實際情況卻非如此,我們
可以隨時上網,感覺不到其他人的存在,這就是作業系統的多工也就是多程序、多執行緒
機制所帶來的優越性。
在 VC 中如何設計一個多工程式,甚至如何使用這種技術來實現網路應用,這都是
作為程式設計師首先要關心和掌握的問題。通過本章的學習,相信讀者會達到這個目標。
10.2 Windows 的多程序程式設計
程序是由程式碼,資料和該程序中執行緒可用的其他系統資源,諸如檔案、管道和同步對
象組成。每個程序都有一個私有的虛擬地址空間。一個程序至少包括一個執行緒(稱為主線
程),並且每個程序都由主執行緒開始。在執行過程中可以建立新的執行執行緒。
例如,如果啟動了 Microsoft Word 程式,則在記憶體中就存在了一個以 winword.exe 為
程式碼的程序,如果不關閉當前的 Word 程式,又通過開始選單啟動了 Microsoft Word,則又
開始了一個以 winword.exe 為程式碼的程序。這兩個程序的程式碼雖然一樣,但所處的環境也
就是資料或其他系統資源是不同的,它們是兩個不同的程序。如果再啟動一個記事本程式,
則系統中又多了一個以 notepad.exe 為程式碼的程序,現在系統中已經存在了 3 個使用者程序。第 10 章 多執行緒與網路程式設計初步 ·263·
·263·
它們在同一段時間內都是向前推進的。
本節主要介紹如何在 VC 中進行多程序的程式設計,主要介紹如何建立新程序、終止
已有程序並設定程序的優先順序。
10.2.1 建立新程序
Windows 是以物件的方式來管理程序的,它由 Win32 子系統來建立和維護,並且可以
由 此 進 程 的 句 柄 來 進 行 管 理 。 進 程 的 創 建 一 般 是 在 一 個 進 程 的 線 程 中 調 用 函 數
CreateProcess( )來建立的,這個程序可以和原程序共享資源(例如控制代碼和變數),而且在
Windows 中,這兩個程序不存在的父子關係,即使原程序終止後,這個新程序仍然可以繼
續執行。
在介紹建立函式之前,先來看幾個相關的資料結構。
1. 資料結構
(1) SECURITY_ATTRIBUTES 結構
該 結 構 存 放 一 個 對 象 的 安 全 描 述 符 並 指 定 是 否 繼 承 返 回 的 句 柄 。SECURITY_
ATTRIBUTES 結構定義如下。
typedef struct_SECURITY_ATTRIBUTES{
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
}SECURITY_ATTRIBUTES
其中成員含義如下。
① nLength:指定該結構大小。
② lpSecurityDescriptor:指向一個物件的安全描述符,該安全描述符控制物件的共享。
如果該成員置為 NULL,則該物件使用呼叫程序的預設安全描述符。
③ bInheritHandle:指定新程序被建立時是否繼承返回的控制代碼。若該成員置為 TRUE,
則新程序繼承該控制代碼。
(2) STARTUPINFO
該結構用於指定新程序的主視窗特性。STARTUPINFO 結構定義如下。
typedef struct_STARTUPINFO
{
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute; ·264· Visual C++程式設計教程與上機指導
·264·
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserverd2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
}STARTUPINFO,*LPSTARTUPINFO;
其中成員含義如下。
① cb:指定該結構大小。
② lpReserved:保留,置為 NULL。
③ lpDesktop:指定一個字串,包括該程序的桌面名或視窗位置名。
④ lpTitle:指定控制檯程序建立的新控制檯視窗標題。
⑤ dwX,dwY:指定新視窗左上角的 x 和 y 偏移量(以畫素為單位)。如果 dwFlags 成員
未指定 STARTF_USEPOSITION 標誌,則忽略這兩項。
⑥ dwXSize,dwYSize: 指 定 新 窗 口 的 寬 度 和 高 度 。 如 果 dwFlags 成 員 未 指 定
STARTF_USESIZE 標誌,則忽略這兩個成員。
⑦ dwXCountChars,dwYCountChars:指定新控制檯視窗的螢幕緩衝區的寬度和高度。
如果 dwFlags 成員未指定 STARTF_USECOUNTCHARS 標誌,則忽略這兩成員。
⑧ dwFillAttribute:指定新控制檯視窗的初始文字和背景顏色。如果 dwFlags 成員未
指定 STARTF_USEFILLATTRIBUTE 標誌,則忽略該成員。
⑨ dwFlags:建立視窗標誌。
⑩ wShowWindow: 新 窗 口 的 顯 示 狀 態 。 如 果 dwFlags 成 員 未 指 定 STARTF_
USESHOWWINDOWW 標誌,則忽略該成員。
cbReserved2:保留,必須置為 0。
lpReserved2:保留,必須置為 NULL。
hStdInput:指定一個控制代碼,該控制代碼用作程序的標準輸入控制代碼。如果 dwFlags 成員未
指定 STARTF_USESTDHANDLES 標誌,則忽略該成員。
hStdOutput:指定一個控制代碼,該控制代碼用作程序的標準輸出控制代碼。如果 dwFlags 成員
未指定 STARTF_USESTDHANDLES,則忽略該成員。
hStdError:指定一個控制代碼,該控制代碼用作程序的標準錯誤控制代碼。如果 dwFlags 成員未
指定 STARTF_USESTDHANDLES,則忽略該成員。
(3) PROCESS_INFORMATION 結構
該結構返回有關新程序及其主執行緒的資訊。其結構定義如下。
typedef struct_PROCESS_INFORMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION;
其中成員含義如下。第 10 章 多執行緒與網路程式設計初步 ·265·
·265·
① hProcess:返回新程序的控制代碼。
② hThread:返回主執行緒的控制代碼。
③ dwProcessId:返回一個全域性程序識別符號。該識別符號用於標識一個程序。從程序被
建立到終止,該值始終有效。
④ dwThreadId:返回一個全域性執行緒識別符號。該識別符號用於標識一個執行緒。從執行緒被創
建到終止,該值始終有效。
2.建立程序
可以使用 CreateProcess 函式建立一個新程序和它的主執行緒。該新程序執行指定的可執
行檔案,並且其獨立運行於呼叫程序。
CreateProcess 的函式原型為:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
其中引數含義如下。
(1) lpApplicationName:指定要執行的應用程式的名字,該名字可以是全路徑名。如果
該引數為 NULL,則程式名必須是 lpszCommandLine 指向的字串的第一個識別符號。該參
數通常置為 NULL,而將程式名和引數放在 lpszCommandLine 指定的字串中。
(2) lpCommandLine:是一個以 NULL 結尾的字串的指標,它指向命令列引數。引數
lpApplicationName 和 lpszCommandLine 不允許同時空,否則系統找不見新程序所對應的可
執行程式的檔名。
(3) lpProcessAttributes和lpThreadAttributes:它們指向SECURITY_ATTRIBUTES結構,
分別用來確定待建立的程序和待建立程序的主執行緒的安全屬性。如果使用預設安全屬性,
則該值為 NULL。
(4) bInheritHandles:用來確定新建的程序能否繼承產生它的程序的控制代碼。若它的值為
TRUE,則這個程序和執行緒所建立的控制代碼都可以被這個程序所建立的新程序所繼承,即繼
承的控制代碼和原來的控制代碼有相同的值和存取許可權。
(5) dwCreationFlags:該引數決定新程序產生的方式,它可以用邏輯或(|)的方式把下列
值結合起來。
① CREATE_NEW_CONSOLE:為新程序建立一個新的控制檯視窗。
② DETACHED_PROCESS:在預設情況下,新程序使用的是父程序的控制檯視窗
③ CREATE_NEW_PROCESS_GROUP:這個新程序將是一個新程序組的根,程序組·266· Visual C++程式設計教程與上機指導
·266·
包括該程序的所有子程序。
④ CREATE_SUSPENDED:新程序的主執行緒被建立在掛起狀態,直到 Resume_Thread
函式被呼叫後才執行。
⑤ DEBUG_PROCESS:如果設定該標誌,呼叫該程序被當作除錯者,新程序準備接
收除錯。系統把在程序被除錯時所發生的除錯事件通知給父程序。
還有其他的值,可以在使用時參閱 MSDN 來學習。
(6) lpEnvironment:指向一個用於新程序的環境塊。如果該引數為 NULL,則新程序繼
承呼叫程序的環境。
(7) lpCurrentDirectory:指 向 新 進 程的 當 前 驅 動器 和 目 錄 的字 符 串 。 如果 該 參 數 為
NULL,則使用呼叫程序的當前驅動器和目錄。
(8) lpStartupInfo:指向一個 STARTUPINFO 結構,使用者說明如何顯示新程序的主視窗。
(9) lpProcessInformation:指向一個 PROCESS_INFORMATION 結構,用於接收有關新
程序的標識資訊。
函式 CreateProcess( )呼叫成功,返回值為 TRUE,否則為 FALSE。
10.2.2 程序的管理
程序被建立之後,就要對其進行管理,比如改變程序的優先順序。而要管理程序,首先
要取得這個程序的控制代碼或程序 ID。
1.取得程序的控制代碼或 ID
函式 GetCurrentProcess 可以取得當前程序的控制代碼,其原型為:
HANDLE GetCurrentProcess(VOID)
這個函式返回一個指向當前程序的控制代碼,但這是一個偽控制代碼,即僅僅只能開啟此程序
物件,增加此物件的引用計數,這個偽控制代碼只能在當前程序中使用,而不能在其他程序中
利用此控制代碼對這個程序進行操作。若在別的程序中對當前這個程序進行操作,可呼叫函式
DuplicateHandle( )把這個偽控制代碼轉換成一個真正的控制代碼。
函式 DWORD GetCurrentProcessID(VOID)可以取得當前程序的 ID,有一些 API 函式需
要用到程序 ID。
2. 取得和設定程序的優先順序
Windows 支援 4 種不同的優先順序:實時(Realtime)、高(High)、變通(Normal)和空閒(Idle),
預設情況下程序的優先順序為普通優先順序。
在 程 序 中 可 以 使 用 相 應 的 參 數 來 設 置 進 程 的 優 先 級 , 它 們 是 :HIGH_PRIORITY_
CLASS(高),IDLE_PRIORITY_CLASS(空 閒),NORMAL_PRIORITY_CLASS(普 通),
REALTIME_PRIORITY_CLASS(實時)。
一般程序的優先順序預設為普通級,除非這個程序的父程序的優先順序為空閒。程序優先
級的設定很重要,對於高優先順序,這個程序的執行緒將佔據幾乎所有的 CPU 時間。而對於 一
個空閒優先順序的程序,其執行緒只有當 CPU 空閒時才開始執行,例如螢幕保護程式的優先順序
就為空閒的優先順序,若使用者閒著,則會啟動這個螢幕保護程式。對於實時優先順序程序,一第 10 章 多執行緒與網路程式設計初步 ·267·
·267·
般不作設定,因為在這種情況下,其他程序都不會執行,如果這個程序不結束,其結果和
宕機一樣。
CreateProcess( )函式允許父程序指定其子程序的優先順序類別。在執行過程中可使用
SetPriorityClass( )函式動態改變程序的優先順序。另外可使用 GetPriorityClass( )獲取程序的優
先級。如使用 DWORD m_pri=GetPriorityClass(GetCurrentProcess( )),可取得當前程序的
優先順序。
10.2.3 終止程序
父程序可以使用 ExitProcess( )函式或 TerminateProcess( )函式終止子程序的執行。這兩
者之間的區別是:ExitProcess 函式將通知所有附屬 DLL 終止並保證程序的全部執行緒都終
止,而且只能終止當前程序;而 TerminateProcess 函式在終止程序時,並不通知所屬 DLL,
除非不得已,不要使用它來終止程序,因為它會導致其附屬的 DLL 程式不能完成一些正常
的資料重新整理工作,該函式不僅能終止當前程序,還能終止其他的程序。
值得注意的是,終止一個程序並不會引起子程序的終止,而只是該程序及其所有執行緒
的終止。
ExitProcess( )函式原型為:
void ExitProcess(UNIT uExitCode)
其中 uExitCode 為程序碼。
TerminateProcess( )函式原型為:
BOOL TerminateProcess(HANDLE hProcess,UNIT uExitCode)
其中,引數 hProcess 標識要終止的程序,uExitCode 為程序的退出碼。
應用程式可以使用 GetExitCodeProcess 返回程序的終止狀態。如果程序還在執行,則
終止狀態為 STILL_ACTIVE。如果程序終止,則終止狀態為程序退出碼。
10.2.4 建立程序例項程式
1. 程式功能
該程式是一個基於對話方塊的程式,使用者可在對話方塊的編輯框中輸入要開啟的可執行程
序的檔名,可以使用瀏覽按鈕來查詢可執行檔案,這要用到檔案【開啟】的通用對話方塊。
2. 程式步驟
(1) 新建一個工程,在第 1 步選用 Dialog Based,然後單擊【完成】按鈕。
(2) 設計對話方塊模板,在對話方塊上擺放控制元件,其佈局如圖 10.1 所示。
(3) 為編輯框控制元件對映 CString 型別的變數 m_strFileName 後,為【瀏覽…】按鈕對映
BN_CLICKED 訊息,併為訊息處理程式編寫程式碼,其程式碼如下。
void CMyDlg::OnBrowse()
{
//構造通用“開啟”檔案對話方塊物件,使其能過濾可執行檔案或所有檔案
CFileDialog dlg(TRUE,NULL,NULL,OFN_HIDEREADONLY
|OFN_OVERWRITEPROMPT,"程式檔案|*.exe;*.com;*.bat|所有檔案(*.*)|*.*||"); ·268· Visual C++程式設計教程與上機指導
·268·
if(dlg.DoModal()==IDOK)
{
m_strFileName=dlg.GetPathName(); //將使用者指定的檔名存入編輯框變數中
UpdateData(FALSE); //在編輯框控制元件中顯示檔名
}
}
圖 10.1 建立程序例項對話方塊模板
(4) 為【執行】按鈕對映 BN_CLICKED 訊息,在其訊息處理程式中建立新程序,執行
使用者指定的程式檔案。
void CMyDlg::OnRun()
{
STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInformation;
//設定程序視窗資訊結構 STARTUPINFO
StartupInfo.cb=sizeof(STARTUPINFO);
StartupInfo.lpReserved=NULL;
StartupInfo.lpDesktop=NULL;
StartupInfo.lpTitle=NULL;
StartupInfo.dwFlags=STARTF_USESHOWWINDOW;
StartupInfo.cbReserved2=0;
StartupInfo.lpReserved2=NULL;
StartupInfo.wShowWindow=SW_SHOWNORMAL; //正常尺寸顯示視窗
char filename[255];
sprintf(filename,"%s",m_strFileName);
//建立以 filename 為名的可執行程式
BOOL bReturn=CreateProcess(NULL,filename,NULL,NULL,FALSE,0,NULL,NULL,
&StartupInfo,&ProcessInformation);
if(!bReturn)
{
MessageBox("建立失敗");
(GetDlgItem(IDC_FILENAME))->SetFocus();
}
}
該程式的執行效果見圖 10.2。第 10 章 多執行緒與網路程式設計初步 ·269·
·269·
圖 10.2 建立程序執行效果圖
圖中編輯框中是要執行的程式名稱,下方是單擊【執行】按鈕後執行的記事本程式,
當使用【關閉】按鈕關閉建立程序程式時,開啟的記事本不會被關閉。
10.3 Windows 的多執行緒程式設計
10.3.1 執行緒概念
執行緒是 Windows 引入的先進技術之一。執行緒是 Windows 的唯一執行單位,是 Windows
為程式分配 CPU 時間的基本實體。每個程序都由一個或多個執行緒組成,由各執行緒協同完成
指定操作。
執行緒是程序內部的可獨立執行的單元,它是系統分配 CPU 時間資源的基本單元。執行緒
的概念與子程式的概念類似,是一個可獨立執行的子程式。一個應用程式可以建立多個線
程,多個不同的執行流,並同時執行這些執行緒。
多執行緒提高了系統響應能力及平滑的後臺處理。例如,一個字處理程式(程序)可以通
過使用多執行緒來加強操作並簡化與使用者的互動。該應用程式可以包含 3 個執行緒,第 1 個線
程可以用於響應使用者的鍵盤輸入訊息,將字元放入文件中;第 2 個執行緒可以執行拼寫檢查
及分頁等後臺操作;第 3 個執行緒可以在後臺將文件送到印表機列印。
雖然多執行緒會給應用開發帶來許多好處,但並非任何情況下都要使用多執行緒,這要依
據實際需要來綜合考慮。一般在以下情況下可以考慮使用多執行緒。
(1) 完成多個相同或類似的任務,如伺服器接收客戶端請求,將網頁傳送給客戶等。
(2) 處理多個視窗的輸入。
(3) 管理來自多個通訊裝置的輸入。
(4) 需要執行不同優先順序的任務。·270· Visual C++程式設計教程與上機指導
·270·
對於應用程式來說,一般通過在一個程序中建立多個執行緒來完成多工更有效,而不
是通過建立多個程序來完成多工。
MFC 支援多執行緒應用程式的開發,應用程式的每個執行緒都是一個 CWinThread 物件,
MFC 將執行緒劃分為兩種型別:工作者執行緒(Worker Thread)和使用者介面執行緒(User_interface
Thread),這兩種型別都基於 CWinThread。
如果執行緒需要執行後臺計算而不需要與使用者互動,那麼該執行緒為工作者執行緒。工作者
執行緒沒有訊息迴圈,不處理視窗訊息,用於在後臺執行任務。該類執行緒是最常用的型別。
如果要處理使用者輸入並響應由使用者產生的事件和訊息,那麼應該建立一個使用者介面線
程,它是通過自己的訊息泵獲取從系統接收訊息。主執行緒本身就是一個使用者介面執行緒,這
是因為 CWinApp 派生於 CWinThread。使用者可從 CWinThread 派生出自己的類來實現使用者
介面執行緒。
10.3.2 建立執行緒
建立執行緒主要有以下 3 種方法:
(1) Windows 的 API 函式 CreateThread;
(2) MFC 全域性函式 AfxBeginThread;
(3) MFC 的 CWinThread 類的 CreateThread 成員函式。
以下就具體介紹這 3 種執行緒的建立方法。
1.使用 API 的 CreateThread( )函式
CreateThread( )函式建立程序的一個新執行緒。該函式的原型為:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
其中引數含義如下。
(1) lpThreadAttributes:指向一個 SECURITY_ATTRIBUTES 結構,用於指定執行緒的安
全屬性。如果使用預設安全屬性,則置為 NULL。
(2) dwStackSize:指定執行緒用於堆分配堆疊的大小。如果為 0,則堆疊大小預設為和該
程序的主執行緒的堆疊大小相同。
(3) lpStartAddress:指向新執行緒執行程式碼的開始地址,通常為包含執行緒程式碼的執行緒函
數名。
(4) lpParameter:指定傳遞給執行緒函式的 32 位引數值。
(5) dwCreationFlags:執行緒建立標誌。如果為 CREATE_SUSPENDED,則該執行緒建立
在掛起狀態,直至呼叫 ResumeThread 函式後才執行;如果為 0,則建立後立即執行。
(6) lpThreadId:指向一個 32 位變數,用於接收該執行緒的識別符號。
如果該函式呼叫成功,則返回新執行緒的控制代碼,否則返回 NULL。第 10 章 多執行緒與網路程式設計初步 ·271·
·271·
【例 10.1】 使用 CreateThread( )函式建立執行緒例項。
//執行緒主體函式
UNIT ThreadProc(LPVOID pParam)
{//執行緒的實際程式碼
return 0;
}
HANDLE hThread;
DWORD dwThreadID;
DWORD dwParam;
hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,
wParam,0,&dwThreadID);
if(hThread= =NULL)
MessageBox("建立執行緒錯誤");
2. 使用 AfxBeginThread
使用 AfxBeginThread 函式既可建立工作者執行緒又可建立使用者介面執行緒。該函式有兩種
格式,第一種格式用於建立工作者執行緒,其中引數 pfnThreadProc 指向執行緒函式,pParam
為傳遞給執行緒函式的引數。第二種格式用於建立使用者介面執行緒,其中引數 pThreadClass 為
CwinThread 派生物件的 RUNTIME_CLASS。
格式 1:
CWinThread *AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
);
格式 2:
CWinThread *AfxBeginThread(
CRuntimeClass *pThreadClass,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL
);
該函式的返回值為指向新建執行緒物件的指標。例 10.2 為使用 AfxBeginThread 來建立
程序的例項。
【例 10.2】 使用 AfxBeginThread 來建立工作者執行緒和使用者介面執行緒。
//執行緒函式
UNIT ThreadProc(LPVOID pParam)
{ //執行緒程式碼
return 0;
}
//在呼叫程序中建立工作者執行緒·272· Visual C++程式設計教程與上機指導
·272·
CWinThread *pThread;
DWORD dwParam;
pThread=AfxBeginThread(ThreadProc,&dwParam);
if(pThread==NULL)
{ MessageBox("建立錯誤");
//錯誤處理 }
//建立使用者介面執行緒例
//定義 CWinThread 執行緒派生類
class CTestThread:public CWinThread
{ // }
//在呼叫程序中建立使用者介面執行緒
CTestThread *pTestThread;
pTestThread=(CTestThread
*)AfxBeginThread(RUNTIME_CLASS(CTestThread),
THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);
3.使用 CWinThread 類
CWinThread 是 MFC 提供的執行緒物件類,包括建立、管理和刪除的一系列成員變數和
成員函式。CWinApp 是 CWinThread 的派生類。
CWinThread 類支援工作者執行緒和使用者介面執行緒。可以將一個指向 CWinThread 派生類
的 CRuntimeClass 的指標作為引數傳遞給 AfxBeginThread 函式以建立一個使用者介面執行緒。
CWinThread 類的 CreateThread 成員函式建立一個在呼叫程序的地址空間中執行的線
程。該函式原型為:
BOOL CreateThread(DWORD dwCreateFlags=0,UINT nStackSize=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);
若該函式成功建立了執行緒,返回非 0,否則返回 0。
【例 10.3】 使用 CWinThread 類來建立執行緒。
CWinThread thread; //建立 CWinThread 物件
thread.m_bAutoDelete=FALSE; //執行緒終止時不自動刪除該物件
thread.m_pfnThreadProc=ThreadProc; //設定執行緒函式
thread.m_pThreadParams=&dwParam; //傳遞給執行緒函式的引數
thread.CreateThrerad(); //建立執行緒,引數使用預設值。
10.3.3 掛起執行緒
使用 SuspendThread 和 ResumeThread 函式(Windows API 或 CWinThread 類成員函式),
執行緒可以掛起或恢復另一個執行緒的執行。當執行緒處於掛起狀態時,執行緒不會被排程執行。
SuspendThread 函式將當前執行緒的掛起次數加 1。若該值的掛起次數大於 0,則該執行緒不
執行。ResumeThread 函式將當前執行緒的掛起次數減 1。當該值為 0 時,執行緒恢復執行,否
則執行緒仍處於掛起狀態。如果將執行緒建立在掛起狀態,那麼在呼叫 ResumeThread 恢復執
行之前可以完成對執行緒狀態的初始化工作。
另外可通過呼叫 Sleep 或 SleepEx 函式暫時掛起當前執行緒一段指定時間。它常用於線
程與使用者的互動中,通過延遲執行執行緒足夠長的時間讓使用者觀察其結果。在睡眠期間執行緒
不會被排程執行。第 10 章 多執行緒與網路程式設計初步 ·273·
·273·
10.3.4 終止執行緒
可以呼叫ExitThread函式或TerminateThread函式終止執行緒的執行。與程序的終止相似,
一 般 情 況 下 使 用 ExitThread 函 數 來 終 止 線 程 , 只 有 在 不 得 已 的 情 況 下 才 使 用
TerminateThread 來終止執行緒。
可 以 調 用 全域性 函 數 AfxEndThread 或 者 使 用 return 語 句 來終 止 自 己 所在 的 線 程 。
AfxEndThread 函式的原型為 AfxEndThread(UINT nExitCode),其中引數 nExitCode 為執行緒
的退出碼,這個值為 0,表示成功,如果為其他值,那麼表示各種不同型別的錯誤。可以
通過函式 GetExitCodeThread 來獲取執行緒的退出碼。
10.4 Winsock 網路程式設計介面
10.4.1 WinSock 概述
Winsock 是一套開放的、支援多種協議的 Windows 下網路程式設計介面,是 Windows 網路
程式設計上的標準介面。應用程式通過呼叫 Winsock 的 API 實現相互之間的通訊,而 Winsock
利用下層的網路通訊協議功能和作業系統呼叫實現實際的通訊工作。
套接字(Sockets)是通訊端點的一種抽象,是支援 TCP/IP 協議網路通訊的基本操作單
元,它提供了一種傳送和接收資料的機制。在開發伺服器/客戶端應用程式時,可以利用
Sockets 實現資料結構或資料包的交換,以完成應用程式之間的通訊。
套接字一般有兩種型別:流套接字和資料報套接字。
流套接字提供雙向的、有序的、無重複並且無記錄邊界的資料流服務,它適用於處理
大量資料。流套接字是面向連線的,通訊雙方進行資料交換之前,必須建立一條路徑,類
似於打電話,首先要雙方能連線,才能繼續通話。這樣既確定了它們之間存在的路由,又
保證了雙方都是活動的、可彼此響應的。在資料傳輸過程中,如果連線斷開,則應用程式
會被通知,此時應用程式可以根據中斷原因作相應處理,在實際中,由於其可靠性高,流
式 Sockets 得到了廣泛應用。但在通訊雙方之間建立一個通訊通道需要很多開支。除此以
外,大部分面向連線的協議為保證傳送無誤,可能會需要執行額外的計算來驗證正確性,
因此會進一步增加開支。
資料報套接字支援雙向的資料流,但並不保證資料傳輸的可行性、有序性和無重複性。
也就是說,一個從資料報套接字接收資訊的程序有可能被發現資訊重複,或者和發出時的
順序不同的情況。此外,資料報套接字的一個重要特點是它保留了記錄邊界。資料報套接
字是無連線的,它不保證接收端是否正在偵聽,類似於郵政服務:發信人把信裝入郵箱即
可,至於收信人是否能收到這封信或郵局是否會因為暴風雨未能按時將信件投遞到收信人
處等,發信人都不得而知。因此,資料報並不十分可靠,需要程式設計師負責管理資料報的排
序和可靠性。應用程式具體可以採用的技術有:通過加流水號方式實現資料包的不丟失傳
輸,通過對資料包校驗實現正確傳輸,當出現傳輸錯誤時採用重發技術。當然讀者可以採
用自己的獨特方法來保證資料的穩定可靠傳輸。資料包的一個優點是:它提供了向多個目·274· Visual C++程式設計教程與上機指導
·274·
標地址傳送廣播資料包的功能。
10.4.2 Winsock 程式設計原理
1. 簡單客戶機/伺服器
進入 20 世紀 90 年代以後,隨著計算機和網路技術的發展,很多資料處理系統都採用
開放系統結構的客戶機/伺服器(Client/Server)網路模型,即客戶機向伺服器提出請求,服務
器對請求做相應的處理並執行被請求的任務,然後將結果返回給客戶機。
客戶機/伺服器模型工作時要求有一套為客戶機和伺服器所共識的慣例來保證服務能
夠被提供(或被接受),這一套慣例包含一套協議,它必須在通訊的兩端都被實現。根據不
同的實際情況,協議可能是對稱的或是非對稱的。在對稱的協議中,每一方都有可能扮演
主從角色,如 Internet 協議中的 Telnet 協議;在非對稱協議中,一方不可改變地被認為是
主機,而另一方是從機,如 Internet 中的 Http 協議。無論具體的協議是對稱的還是非對稱
的,當服務被提供時必須存在客戶程序和服務程序。
一個服務程式通常在一個眾所周知的地址監聽客戶對服務的請求,也就是說,服務進
程一直處於休眠狀態,直到一個客戶對這個服務提出了連線請求。在這個時刻,服務程式
被“驚醒”並且為客戶提供服務——對客戶的請求作出適當的反應。
2.Winsock 的啟動和終止
由於 Winsock 的服務是以動態連結庫 Winsock DLL 形式實現的,所以必須先呼叫
WSAStartup 函式對 Winsock DLL 進行初始化,協商 Winsock 的版本支援,並分配必要的
資源。如果在呼叫 Winsock 函式之前,沒有載入 Winsock 庫,則會返回 SOCKET_ERROR
錯誤,錯誤的資訊是 WSANOTINITIALIZED。WSAStartup 函式原型為:
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中,引數 wVersionRequested 指定應用要使用的 Windows Sockets 最高版本,其中高
位位元組表示輔版本號,低位位元組表示主版本號。目前使用最廣泛的是 Windows Sockets 1.1
版本,最高版本已經是 2.0 版本。一般用巨集 MAKEWORD(X,Y)獲得 wVersionRequested 的
正確值。如 MAKEWORD(2,0)表示使用 Windows Sockets2.0 版本。
引數 lpWSAData 指向 WSADATA 結構的指標。該結構包含了載入的庫版本的有關的
資訊。
該函式成功則返回 0,失敗則返回如下可能值。
(1) WSASYSNOTREADY:表示網路裝置沒有準備好。
(2) WSAVERNOTSUPPORTED:Winsock 的版本資訊號不支援。
(3) WSAEINPROGRESS:一個阻塞式的 Winsock1.1 存在於程序中。
(4) WSAEPROCLIM:已經達到 Winsock 使用量的上限。
(5) WSAEFAULT:lpWSAData 不是一個有效的指標。
此外,在應用程式關閉套接字後,還應呼叫 WSACleanup 函式終止對 Winsock DLL 的
使用,並釋放資源,以備下一次使用。WSACleanup 函式的原型為:
int WSACleanup(void); 第 10 章 多執行緒與網路程式設計初步 ·275·
·275·
該函式不帶任何引數,若呼叫成功則返回 0,否則返回錯誤。
3.錯誤的檢查和控制
錯誤檢查和控制對於編寫成功的 Winsock 應用程式是至關重要的。事實上,對 Winsock
API 函式來說,返回錯誤是很常見的,但是多數情況下,這些錯誤都是無關緊要的,通訊
仍可在套接字上進行。儘管返回的值並非一成不變,但不成功的 Winsock 呼叫返回的最常
見的值是 SOCKET_ERROR。SOCKET_ERROR 是值為-1 的常量。如果錯誤情況發生了,
就可用 WSAGetLastError 函式來獲得一段程式碼,這段程式碼明確地表明產生錯誤的原因。該
函式的原型為:
int WSAGetLastError(void);
WSAGetLastError 函式返回的錯誤都是預宣告的常量值,根據 Winsock 版本的不同,
這些值的宣告不在 Winsock1.h 中就會在 Winsock2.h 中。為各種錯誤程式碼宣告的常量一般
都以 WSAE 開頭。
4. Winsock 程式設計模型
不論是流套接字還是資料報套接字程式設計,一般都採用客戶機/伺服器方式,它們的過程
基本類似,下面著重介紹流套接字的程式設計模型。
(1) 流套接字程式設計模型
考慮使用電話進行通訊的過程:如果想要使用電話進行通話,首先雙方必須安裝電話
機,並由一方撥號與另一方建立連線,然後可以通過電話聽取對方的聲音,或者向對方講
話,最後關閉連線。流套接字的過程與打電話的過程非常相似,服務程序和客戶程序在通
信前必須建立各自的套接字並建立連線,然後才能對相應的套接字進行讀、寫操作,以實
現資料的傳輸。具體程式設計步驟如下。
① 伺服器程序建立套接字。服務程序總是先於客戶程序啟動,服務程序首先呼叫
socket 函式建立一個流套接字,socket 函式的原型為:
SOCKET socket(int af,int type,int protocol);
其中引數 af 指定網路地址型別,一般都取 AF_INET,表示是在 Internet 上的 Socket。
引數 type 用於指定套接字型別,當採用流連線方式時用 SOCK_STREAM,用資料報方式
時用 SOCK_DGRAM。protocol 用於指定網路協議,一般都為 0,表示用對流套接字採用默
認的 TCP 協議,資料報套接字採用預設的 UDP 協議。函式的返回值是 Winsock 定義的一
種資料型別 SOCKET,它實際就是個整型資料,在 Socket 建立成功時,代表 Winsock 分配
給程式的 Socket 編號,後面呼叫傳輸函式時,就可以把它像檔案指標一樣引用。如果 Socket
建立失敗,返回值為 INVALID_SOCKET。
② 將本地地址繫結到所建立的套接字上以使在網路上標識該套接字。在成功建立了
Socket 之後,就應該選定通訊的物件。首先是自己的程式要與網上的哪臺計算機通話;其
次,在多工系統下,該臺計算機上可能會有幾個程式在工作,必須指出要與哪個程式通
信。前者可以通過 Internet 的網路 IP 地址來確定,而後者則由埠號來確定。用埠號來
表示同一臺計算機上不同的應用程式,埠號可以為 0~65535,不同功能的通訊程式使用
不同的埠號,如 pop3 協議使用 110 埠,http 協議使用 80 埠等,這樣一臺計算機上·276· Visual C++程式設計教程與上機指導
·276·
可以有幾個程式同時使用一個 IP 地址通訊而不互相干擾,IP 地址與埠號的關係好像電
話總機號碼與分機號碼的關係一樣。因為一些常用的網路服務往往佔據了 1024 以下的埠
號,所以編制自己的通訊程式時,應指定大於 1024 的埠號。
一般該過程是通過函式 bind 來完成的,該函式的原型為:
int bind(SOCKET s,struct sockaddr_in * name,int namelen);
其中引數 s 是已經建立好的套接字。name 是指向描述通訊物件地址資訊的結構體的指
針,namelen 是該結構體的長度。結構體 sockaddr_in 的定義如下。
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中,sin_family 是指一套地址族,通常被設為 AF_INET;sin_port 是指埠號;sin_addr
是指 IP 地址;sin_zero[8]主要是使該結構的大小和 SOCKADDR 大小相同(SOCKADDR 結
構由一個無符號 short 型和一個長度為 14 的 char 型陣列構成,這個結構一共是 16 個位元組),
在 sockaddr_in 中新增這個長度為 8 的陣列,使 sockaddr_in 的長度也為 16(2+2+4+8),這 樣
做的目的是使地址操作更方便。該函式如果呼叫失敗,會返回 SOCKET_ERROR。對 bind
來說,最常見的錯誤是 WSAEADDRINUSE。如果使用的是 TCP/IP,那麼該錯誤表示另一
個 進 程 已 經 同 本 地 IP 接 口 和 端 口 號 綁 定 到 了 一 起 , 或 者 那 個 IP 接 口 和 端 口 號 處 於
TIME_WAIT 狀態。假如對一個已經繫結的套接字呼叫 bind,便會返回 WSAEFFAULT
錯誤。
③ 將套接字置入監聽模式並準備接受連線請求。bind 函式的作用只是將一個套接字
和一個指定的地址關聯在一起,讓一個套接字等候進入連線的 API 函式是 listen,其原
型為:
int listen(SOCKET s,int backlog);
其中引數 s 標識一個已繫結但未連線套接字的描述字。backlog 引數用於指定正在等待
連線的最大佇列長度,這個引數非常重要,因為完全可能同時出現幾個伺服器連線請求。
例如,假定 backlog 引數為 2,如果 3 個客戶機同時發出請求,那麼頭兩個會被放在一個等
待佇列中,以便應用程式依次為它們提供服務,而第 3 個連線請求(佇列已滿)會造成一個
WSAECONNREFUSED 錯誤,一旦伺服器接受了一個連線,那個連線請求就會被從佇列中
刪除,以便別人可繼續發出請求,backlog 引數本身是由基層的協議提供者決定的,如果出
現非法值,那麼會用與之最接近的一個合法值來取代。
如果無錯誤發生,listen 函式返回 0,若失敗則返回 SOCKET_ERROR 錯誤,最常見的
錯誤是 WSAFINVAL,該錯誤通常表示套接字在 listen 前沒有呼叫 bind。
進入監聽狀態之後,通過呼叫 accept 函式使套接字作好接受客戶連線的準備。Accept( )
函式的原型為:
SOCKET accept(SOCKET s,struct sockaddr *addr,int *addrlen); 第 10 章 多執行緒與網路程式設計初步 ·277·
·277·
其中引數 s 是處於監聽模式的套接字描述字。第 2 個引數是一個有效的 SOCKADDR
_IN 結構的地址,而 addrlen 是 SOCKADDR_IN 結構的長度。這樣,伺服器便可為等待連
接佇列中的第一個連線請求提供服務了。accept 函式返回,addr( )引數變數中會包含發出連
接請求的那個客戶機的 IP 地址資訊,而 addrlen 引數則指出該結構的長度,並返回一個新
的套接字描述字,它對應於已經接受的那個客戶機連線。對於該客戶機後續的所有操作,
都應使用這個新套接字,至於原來的那個監聽套接字,它仍然用於接受其他客戶機連線,
而且仍處於監聽模式。如果無連線請求,服務程序將被阻塞。
④ 客戶程序呼叫 socket 函式建立客戶端套接字。
⑤ 客戶向服務程序發出連線請求。通過呼叫 connect( )函式可以建立一個到服務程序
的連線。其中 s 是剛建立的套接字描述字,name 與 namelen 的含義和使用方法與 bind( )相
同,用來指定通訊物件。如果連線失敗,該函式會返回 SOCKET_ERROR。如果欲連線的
計算機沒有偵聽指定埠的這一程序,connect呼叫就會失敗,併發生錯誤 WSAECONNREF
USED。另一個常見的錯誤是 WSAETIMEOUT,表示連線超時。
⑥ 當連線請求到來後,被阻塞服務程序的 accept( )函式如③中所述即生成一個新的套
接字與客戶套接字建立連線,並向客戶返回接收訊號。
⑦ 一旦客戶機的套接字接收到來自伺服器的訊號,則表示客戶機與伺服器已實現連
接,即可以進行資料傳輸了。senD. recv 函式是進行資料收發的函式。它們的函式原型是:
int send(SOCKET s,char *buf,int len,int flags);
int recv(SOCKET s,char *buf,int len,int flags);
s 是已建立連線的套接字的描述字。buf 和 len 是傳送或接收的資料包及其長度,引數
flags 一般取 0。recv( )函式實際上是讀取 send( )函式發過來的一個數據包。當讀到的資料
位元組少於規定接收的數目時,就把資料全部接收,並返回實際收到的位元組數;當讀到的數
據多於規定值時,在流方式下剩餘的資料由下個 recv( )讀出。這兩個函式在出錯時都返回
SOCKET_ERROR。
⑧ 關閉套接字。一旦任務完成,就必須關閉連線,以釋放套接字佔用的所有資源。通
常呼叫 closesocket 函式即可達到目的,但 closesocket 可能會導致資料的丟失,因此應該在
呼叫該函式之前,先呼叫 shutdown 函式從容地中斷連線,即傳送端通知接收端“不再發送
資料”或接收端通知傳送端“不再接收資料”。
shutdown( )函式的原型為:
int shutdown(SOCKET s,int how);
其中,how 引數用於描述禁止哪些操作,它可取的值有:SD_RECEIVE、SD_SEND
或 SD_BOTH。如果是 SD_RECEIVE,就表示不允許再呼叫接收函式,這對底部的協議層
沒有影響;如果選擇 SD_SEND,表示不允許再呼叫傳送函式;如果指定 SD_BOTH,則表
示取消連線兩端的收發操作。如果沒有錯誤發生,則返回 0,否則返回 SOCKET_ERROR。
shutdown( )函式並不關閉套接字,且套接字所佔用的資源將被一起保持到closesocket( )
函式呼叫。closesocket( )函式的原型為:
int closesocket(SOCKET s);
其中,引數 s 是要關閉的套接字描述字,再利用套接字執行呼叫就會失敗,並出現·278· Visual C++程式設計教程與上機指導
·278·
WSAE_OTSOCK 錯誤。
圖 10.3 列出了流套接字程式設計的時序流程圖。
socket( )
bind( )
listen( )
socket( )
connect( )
recv( )
阻塞,等待客戶資料
recv( ) send( )
send( )
closesocket( ) closesocket( )
客戶端
建立連線
請求資料
應答資料
伺服器
accept( )
圖 10.3 流套接字程式設計時序流程圖
(2) 資料報套接字程式設計模型
資料報套接字是無連線的,它的程式設計過程比流套接字要簡單一些。
對於伺服器端,先用 socket( )函式建立套接字,再通過 bind( )函式進行繫結,但不需
要呼叫 listen( )和 accept( )函式,只需等待接收資料。由於它是無連線的,因此它可以接收
網路上任何一臺機器所發的資料包。常用的接收資料函式是 recvfrom( ),傳送函式是
sendto( ),它們的原型為:第 10 章 多執行緒與網路程式設計初步 ·279·
·279·
int recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr
*from,int *fromlen);
int sendto(SOCKET s,char *buf,int len,int flags,struct sockaddr_into,
int *tolen);
其中 recvfrom( )函式前 4 個引數和 recv( )函式一樣,而引數 from 是一個 SOCKADDR
結構指標,fromlen 引數是帶有指向地址結構長度的指標。當它返回資料時,SOCKADDR
結構內便填入傳送資料端的地址。Sendto( )函式的引數除了 buf 是指向傳送資料緩衝,len
是指傳送資料長度,sockaddr_into 是指接收資料端的地址外,其他與 recvfrom 相似。
10.4.3 用流套接字進行通訊的簡單例子
本節是使用流套接字進行簡單的網路通訊程式設計的例項。它主要建立一個伺服器程式和
一個客戶端程式,在建立連線後,由客戶端向伺服器發出訊息“來自伺服器”,伺服器在
收到訊息後顯示,並向客戶端傳送訊息“來自伺服器”,客戶端在接收後顯示。
1.伺服器程式的實現
該程式使用阻塞模式套接字實現,其步驟為如下。
(1) 建立一個基於對話方塊的 MFC AppWizard 工程。
(2) 在檔案 StdAfx.h 中的#endif 前面一行加入如下兩行程式碼以包含 Winsock 相關頭文
件及連線相應的庫檔案。
#include <winsock.h>
#pragma comment(lib,"wsock32")
(3)在 對 話 框 類 的 OnInitDialog( )函 數 中 初 始 化 Winsock, 將 下 面 代 碼 加 入 到
Cdialog::OnInitDialog( )下面。
WSADATA wsaData;
WORD version=MAKEWORD(2,0); //設定 winsock 版本為 2.0
int ret=WSAStartup(version,&wsaData); //初始化 Socket
if(ret!=0)
TRACE("initialize error.!");
(4) 為 OK 按鈕對映成員函式 OnOK( ),將本伺服器程式的主要通訊工作填加到該函式
中,其程式碼如下。
void CSocketAPIServerDlg::OnOK()
{
// TODO: Add extra validation here
SOCKET m_hSocket;
m_hSocket=socket(AF_INET,SOCK_STREAM,0); //建立套接字
//設定繫結地址
sockaddr_in m_addr;
m_addr.sin_family=AF_INET;
m_addr.sin_port=htons(5050);
m_addr.sin_addr.S_un.S_addr=INADDR_ANY;
·280· Visual C++程式設計教程與上機指導
·280·
int error=0;
//繫結套接字到本機
int ret;
ret=bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));
if(ret==SOCKET_ERROR)
{
TRACE("Bind Error:%d\n",(error=WSAGetLastError()));
return;
}
//開始一個偵聽過程,等待客戶的連線
ret=listen(m_hSocket,2); //最多支援客戶連線數為 2
if(ret==SOCKET_ERROR)
{
TRACE("Listen Error:%d\n",(error=WSAGetLastError()));
return;
}
//該函式阻塞,等待客戶的連線
SOCKET s=accept(m_hSocket,NULL,NULL);
if(ret==SOCKET_ERROR)
{
TRACE("Accept Error:%d\n",(error=WSAGetLastError()));
return;
}
//一旦有使用者連線,就等待使用者發來的請求資訊,該函式也阻塞
char buff[256];
ret=recv(s,buff,256,0);
if(ret==0||ret==SOCKET_ERROR)
{
TRACE("recv Error:%d\n",(error=WSAGetLastError()));
return;
}
buff[ret]='\0';
AfxMessageBox(buff);
//向客戶傳送訊息
ret=send(s,"來自伺服器",10,0);
if(ret==10)
AfxMessageBox("伺服器向客戶機發送成功");
else
{
TRACE("Send Error:%d\n",(error=WSAGetLastError()));
return;
}
CDialog::OnOK(); //此行程式碼為函式中原有程式碼
}
2. 客戶端程式的實現
該程式與伺服器程式一樣,必須做前 3 步的準備工作,接下來為 OK 按鈕對映成員函
數 OnOK( ),為其編寫程式碼如下。
void CSocketAPIClientDlg::OnOK() 第 10 章 多執行緒與網路程式設計初步 ·281·
·281·
{
SOCKET m_hSocket;
m_hSocket=socket(AF_INET,SOCK_STREAM,0);
ASSERT(m_hSocket!=NULL);
sockaddr_in m_addr;
m_addr.sin_family=AF_INET;
//改變埠號的資料格式,此埠號要與服務程式的埠口號一樣
m_addr.sin_port=htons(5050);
m_addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); //使用本機 IP 地址
int ret=0;
int error=0;
//連線伺服器
ret=connect(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));
if(ret==SOCKET_ERROR)
{
//連線失敗
TRACE("Connect Error:%d\n",(error=WSAGetLastError()));
if(error==10061) //該錯誤碼錶示伺服器沒有正常工作
AfxMessageBox(_T("請確認伺服器確實已開啟並工作在同樣的埠"));
return;
}
//向伺服器傳送資料
ret=send(m_hSocket,"來自客戶機",10,0);
if(ret==10)
AfxMessageBox("客戶端向伺服器傳送資訊成功");
else
{
TRACE("Send data error:%d\n",WSAGetLastError());
return;
}
char buff[256];
//從伺服器端接收資料
ret=recv(m_hSocket,buff,256,0);
if(ret==0)
{
TRACE("Recv data error:%d\n",WSAGetLastError());
return;
}
buff[ret]='\0';
AfxMessageBox(buff);
CDialog::OnOK();
}
該例項執行效果如圖 10.4 和 10.5 所示。·282· Visual C++程式設計教程與上機指導
·282·
圖 10.4 伺服器程式接收到來自客戶機資料執行效果圖
圖 10.5 客戶程式接收來自伺服器資料執行效果圖
10.5 MFC Socket 類
為了方便開發人員輕鬆開發網路應用程式,Visual C++MFC提供了相應的Socket類庫,
主要包括 CAsyncSocket 類、CSocket 類和 CSocketFile 類。
10.5.1 CAsyncSocket 類
CAsyncSocket 物件表示一個 Windows Socket,用於表示網路通訊。CAsyncSocket 類封
裝了 Windows Sockets API,使用面向物件技術,方便與 MFC 其他類庫一起程式設計。
1.建立 CAsyncSocket 物件
建立 CAsyncSocket 物件分為兩步:首先呼叫建構函式建立一個空白的 Socket 物件,
然後呼叫 Create 成員函式建立 SOCKET 資料結構並繫結地址。要注意的是,在伺服器端應第 10 章 多執行緒與網路程式設計初步 ·283·
·283·
用程式的接受請求處理函式中,偵聽 Socket 建立一個通訊 Socket 時,無須再呼叫 Create( )
函式。
Create 函式原型為
BOOL Create(UINT nSocketPort=0,int nSocketType=SOCK_STREAM,long lEvent
=FD_READ|FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT|FD_CLOSE,
LPCTSTR lpszSocketAddress=NULL);
其中,引數 nSocketPort 用於指定分配給 Socket 的埠號,預設為 0,表示由系統自動
分配;引數 nSocketType 預設值為流式 Socket;引數 lEvent 為位遮蔽碼,指定將為應用程
序生成通知的事件集合,預設情況下所有事件都會通知;引數 lpszSocketAddress 為繫結地
址,既可以設定計算機名,也可以設定 IP 地址,還可以是 DNS 地址,預設為本機地址。
該函式如果呼叫成功,則返回 TRUE;否則返回 FALSE。應用程式可以通過呼叫
GetLastError 函式獲取錯誤描述。
建立一個 Socket 之後,需呼叫 bind 成員函式將 Socket 與一個本地地址繫結。對於服
務器端偵聽 Socket 在接受客戶端連線請求前必須選擇一個埠號並繫結。
2.虛擬成員函式
為支援 lEvent 所代表的通知事件,MFC 通過提供虛擬成員函式輕鬆實現程式設計。例如網
絡應用程式要處理 FD_ACCEPT 事件,只需過載 OnAccept 函式即可。
(1) OnAccept:對應於 FD_ACCEPT,表示一個新的連線請求等待被接受。通過呼叫
Accept 成員函式對客戶端的連線請求進行響應。
(2) OnClose:對應於 FD_CLOSE,表示 Socket 已關閉。
(3) OnConnect:對應於 FD_CONNECT,表示 Socket 連線已完成,不論成功與否。它
在 Connect 成員函式被呼叫之後才被呼叫。
(4) OnOutOfBandData:對應於 FD_OOB,表示接收到帶外資料,該資料通常為緊急
資料。
(5) OnReceive:對應於 FD_READ,表示接收到新的資料,等待被裝入。通過呼叫 Receive
成員函式接收資料。
(6) OnSend:對應於 FD_WRITE,表示資料已準備好以進行傳送。通過呼叫 Send 成員
函式傳送資料。
3.建立連線
與 Windows Sockets API 建立網路應用程式相同,建立並繫結一個 Socket 之後,就需
要建立客戶端與伺服器之間的連線。
(1) 客戶端
對於流式 Socket 客戶端,使用 Connect 成員函式提出連線請求。它有以下兩種函式
原型。
BOOL Connect(LPCTSTR lpszHostAddress,UINT nHostPort);
BOOL Connect(const SOCKADDR *lpSockAddr,int nSockAddrLen);
其中,第一種形式的 Connect 函式引數 lpszHostAddress 為繫結地址,它是一個 ASCII·284· Visual C++程式設計教程與上機指導
·284·
字串,例如“sunzhiyue、122.1.6.20、ftp.microsoft.com”,引數 nHostPort 為埠號。第
二種形式的 Connect 函式引數與 Windows Sockets API 的 connect 函式含義相同。
該函式如果呼叫成功,則返回 TRUE;否則返回 FALSE。應用程式可以通過呼叫
GetLastError 函式以獲取錯誤描述。
(2) 伺服器端
當伺服器端建立並繫結偵聽 Socket 之後,就應呼叫 Listen 成員函式開始偵聽客戶端連
接請求。當接收到客戶端請求後,呼叫 Accept 成員函式響應請求。Accept 函式通常在
OnAccept 虛擬成員函式中呼叫。
Accept 函式執行成功,將返回一個用於與客戶端進行通訊的 Socket。
4.收發資料
一旦客戶端與伺服器成功建立 Socket 連線後,就可以進行相互通訊。MFC 提供了 4
個成員函式用於收發資料。
(1) Receive:從 Socket 接收到資料。
(2) ReceiveFrom:接收到一個數據報,並存儲原地址。
(3) Send:向一個 Socket 傳送資料。
(4) SendTo:向一個指定目的地傳送資料。
5.關閉 Socket
當 Socket 使用結束之後,就應該關閉 Socket。
(1) ShutDown:禁止傳送/接收資料。
(2) Close:關閉 Socket。
10.5.2 CSocket 類
CSocket 類是 CAsyncSocket 類的派生類,它繼承了 CAsyncSocket 對 Windows Sockets
API 的封裝。與 CAsyncSocket 物件相比,CSocket 物件代表了 Windows Sockets API 的更高
一級的抽象化,自動為應用程式處理阻塞呼叫。CSocket 與類 CSocketFile 和 CArchive 一起
來管理對資料的傳送和接收。
與 CAsyncSocket 類相同,要建立一個 CSocket 物件,首先需要呼叫建構函式,然後調
用 Create 成員函式建立。使用 CSocket 類實現客戶端和伺服器端建立 Socket 連線,以及數
據收發過程,與 CAsyncSocket 類類似,不同之處如下。
(1) CSocket 物件不再呼叫通知函式 OnConnect,僅僅呼叫 Connect 成員函式。但是調
用 Connect 函式時會發生阻塞,直到成功地建立了連線或有錯誤發生。呼叫 Connect 函式
的執行緒在 Connect 函式發生阻塞時仍能處理 Windows 的其他訊息。
(2) CSocket 物件不再呼叫通知函式 OnSend,僅僅呼叫 Send 成員函式。但是呼叫 Send
函式時會發生阻塞,直到所有資料都發送完畢。呼叫 Send 函式的執行緒在 Send 函式發生阻
塞時仍能處理 Windows 的其他訊息。
(3) 由 於 CSocket 類 是 CAsyncSocket 類 的 派 生 類 , 因 此 CSocket 對 象 也 能 使 用
Receive/Send 和 ReceiveFrom/SendTo 收發資料。除此之外,CSocket 物件可以和 CSocketFile
物件一起使用序列化方法收發資料。第 10 章 多執行緒與網路程式設計初步 ·285·
·285·
10.5.3 CSocketFile 類
CSocketFile 物件是一個用來通過 Windows Sockets 在網路中傳送和接收資料的 CFile
物件。網路應用程式可以通過該類簡化傳送和接收資料流程,這需要以下兩方面工作:一
是將 CSocketFile 物件與一個 CSocket 物件連線,二是將 CSocketFile 物件與一個 CArchive
物件連線。
一旦 CSocketFile 物件與 CSocket 物件和 CArchive 物件建立連線,CSocketFile 物件使
用方法與一般的 CFile 物件使用方法就基本類似。即利用 CArchive 物件來發送和接收資料。
CArchive 物件的操作符(<<和>>)可實現對檔案檔案的讀或寫入資料,即接收或傳送
資料。
10.6 上 機 指 導
程式設計實現一個基於 Client/Server 模式的小型自動回覆系統,包括客戶端和伺服器端兩
部分,使用者通過客戶端傳送訊息,伺服器端在收到訊息後,查詢對應的回覆資訊,發回給
客戶端後斷開連線。
目的:掌握 Winsock API 進行網路通訊,及伺服器端多執行緒技術。
實現步驟如下:
1.伺服器程式的實現
(1) 生成一個基於對話方塊的 MFC AppWizard 工程 AutoReplyServer,在對話方塊上放
置兩個按鈕控制元件,一個為“開啟伺服器”,ID 為 IDC_START;另外一個為“關閉伺服器”,
ID 為 IDC_END。使用 ClassWizard 為這兩個按鈕對映訊息處理函式 OnStart( )和 OnEnd( )。
(2) 在 StdAfx.h 中#endif 前新增下面的程式碼。
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
(3) 在 AutoReplyServerDlg.cpp 檔案中所有函式外,加入以下幾個全域性變數。
SOCKET g_hSocket=NULL;
SOCKET g_hAcceptSocket=NULL;
CMapStringToString mapReply; //存入自動回覆的資訊
(4) 在 AutoReplyServerDlg.h 中加入如下的巨集定義。
#define CONNECT_PORT 8080
(5) 在 CAutoReplyServerApp 類中 InitInstance 函式中新增如下程式碼。
//設定幾個靜態的回覆訊息
mapReply.SetAt("Hello Server","Hello Client");
mapReply.SetAt("First From Client","First From Server");
mapReply.SetAt("Second From Client","Second From Server");
mapReply.SetAt("GoodBye","Bye"); ·286· Visual C++程式設計教程與上機指導
·286·
(6) 在 CAutoReplyServerDlg 類中的 OnStart( )函式中編寫程式碼,程式碼如下。
void CAutoReplyServerDlg::OnStart()
{
//填寫 sockaddr_in 結構
sockaddr_in sa_addr;
sa_addr.sin_family=AF_INET;
sa_addr.sin_port=htons(CONNECT_PORT);
sa_addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
ASSERT(g_hSocket==NULL);
WORD version;
WSADATA wsaData;
int nErr;
version=MAKEWORD(2,0);
//載入所需的 Winsock dll 版本
nErr=WSAStartup(version,&wsaData);
if(nErr)
{
AfxMessageBox("載入 Winscok Dll 出錯");
return;
}
//建立 Socket 套接字
if((g_hSocket=socket(AF_INET,SOCK_STREAM,0))==INVALID_SOCKET)
{
AfxMessageBox("建立 Socket 出錯");
return;
}
//繫結地址
if(bind(g_hSocket,(sockaddr *)&sa_addr,sizeof(SOCKADDR))==
SOCKET_ERROR)
{
AfxMessageBox("bind 函式執行綁定出錯");
return;
}
//監聽客戶端連線請求
if(listen(g_hSocket,5)==SOCKET_ERROR)
{
AfxMessageBox("監聽客戶端連線失敗");
return;
}
//啟動執行緒來處理客戶端的通訊請求
AfxBeginThread(ServerThreadProc,0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
}
(7) 編制伺服器執行緒函式,用來處理與客戶端的通訊,其函式程式碼如下。
UINT ServerThreadProc(LPVOID pParam)
{
sockaddr_in sa_addr;
ASSERT(g_hSocket!=NULL); 第 10 章 多執行緒與網路程式設計初步 ·287·
·287·
int nLen=sizeof(SOCKADDR);
//等待接受客戶端的連線請求
g_hAcceptSocket=accept(g_hSocket,(sockaddr *)&sa_addr,&nLen);
if(g_hAcceptSocket==INVALID_SOCKET)
{
if(WSAGetLastError()!=WSAEINTR)
AfxMessageBox("接受連線失敗");
return 1;
}
//接受到一個客戶端的連線請求後,立即啟動一執行緒重新開始監聽
AfxBeginThread(ServerThreadProc,pParam);
//處理與客戶端的通訊
char sCommand[300];
memset(sCommand,0,300);
int nRecv;
//從客戶端接收資料
if((nRecv=recv(g_hAcceptSocket,sCommand,300,0))==SOCKET_ERROR)
{
AfxMessageBox("接收資料失敗");
return 1;
}
if(nRecv==0) return 1;
sCommand[nRecv]='\0';
CString strCommand;
strCommand.Format("%s",sCommand);
CString Reply;
//根據接收到的客戶端資訊查詢其回覆資訊
mapReply.Lookup(strCommand,Reply);
char sBuff[100];
sprintf(sBuff,"%s",Reply);
int nByteSent;
//將回覆信息傳送給客戶端
nByteSent=send(g_hAcceptSocket,sBuff,Reply.GetLength(),0);
if(nByteSent==SOCKET_ERROR)
{
AfxMessageBox("傳送資料失敗");
return 1;
}
//關閉套接字
if(closesocket(g_hAcceptSocket)==SOCKET_ERROR)
{
AfxMessageBox("關閉連線失敗");
g_hAcceptSocket=NULL;
return 1;
}
return 0;
} ·288· Visual C++程式設計教程與上機指導
·288·
(8) 為“關閉伺服器”按鈕對映訊息,並編寫程式碼如下。
void CAutoReplyServerDlg::OnEnd()
{
// TODO: Add your control notification handler code here
if(g_hSocket= =NULL) return;
VERIFY(closesocket(g_hSocket)!=SOCKET_ERROR);
g_hSocket=NULL;
}
2. 客戶端程式實現
(1) 利用 AppWizard 生成一個基於對話方塊的工程,工程名為 AutoReplyClient。
(2) 在對話方塊上放置一個列表框控制元件和一個按鈕“傳送資料”,使用 ClassWizard 為列
表框控制元件對映 CListBox 型別的變數 m_list,用