windows多线程和网络编程

第 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   
#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  
#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,用于存放可以发向服务器的信息。为“发送数
据”按钮映射消息函数 OnSend。
(3)  在 StdAfx.h 中#endif 前添加下面的代码。
#include  
#pragma comment(lib,"ws2_32.lib") 
(4)  在 AutoReplyClientDlg.h 中加入如下的宏定义。
#define  CONNECT_PORT  8080 
(5)  为 CAutoReplyClientDlg 添加两个成员变量。
SOCKET  m_hSocket; 
sockaddr_in  m_saAddr; 
(6)  在 CAutoReplyClientDlg 的构造函数中加入以下语句。
m_hSocket=NULL; 
(7)  在 OnInitDialog( )函数中为列表框进行初始化,代码如下。
m_list.AddString("Hello Server"); 
m_list.AddString("First From Client"); 
m_list.AddString("Second From Client"); 
m_list.AddString("GoodBye"); 
m_list.SetCurSel(0);         //默认选中第一项
(8)  为 OnSend( )函数编写代码如下。
void CAutoReplyClientDlg::OnSend()  

 UpdateData(); 
 int sel; 
    //取得列表框中的选项
 sel=m_list.GetCurSel(); 
    if(sel==-1) 
  return; 
 m_list.GetText(sel,sendMess); 
 //设置客户端要进行连接服务器的地址结构第 10 章  多线程与网络编程初步  ·289·
·289·
 m_saAddr.sin_family=AF_INET; 
 m_saAddr.sin_port=htons(CONNECT_PORT); 
 m_saAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); 
 //连接本机上的服务器
 m_hSocket=NULL; 
 WORD version; 
 WSADATA wsaData; 
 int err; 
 version=MAKEWORD(2,0); 
 //加载所需的 Winsock 版本  
 err=WSAStartup(version,&wsaData); 
 if(err) 
 { 
  AfxMessageBox("加载 Winsock DLL 出错"); 
  return; 
 } 
  //创建套接字
 m_hSocket=socket(AF_INET,SOCK_STREAM,0); 
 if(m_hSocket==INVALID_SOCKET) 
 { 
  AfxMessageBox("创建 Socket 出错"); 
  return; 
 } 
 ASSERT(m_hSocket!=NULL);        
 if(connect(m_hSocket,(sockaddr *)&m_saAddr,sizeof(SOCKADDR))== 
SOCKET_ERROR) 
 { 
  AfxMessageBox("连接服务器失败"); 
  return ; 
 } 
 CString sendMess; 
 int nByteSent; 
 //将列表中选中的信息向服务器发送
 nByteSent=send(m_hSocket,sendMess,sendMess.GetLength(),0); 
 if(nByteSent==sendMess.GetLength()) 
 { 
  AfxMessageBox("发送数据成功"); 
  char sRecv[256]; 
        int nBytes; 
  //取得服务器自动回复的信息
  if((nBytes=recv(m_hSocket,sRecv,255,0))==SOCKET_ERROR) 
  { 
   AfxMessageBox("接受数据失败"); 
   return; 
  } 
  sRecv[nBytes]='\0'; 
  //显示回复信息
  AfxMessageBox(sRecv); 
 } ·290· Visual C++程序设计教程与上机指导
·290·
 if(closesocket(m_hSocket)==SOCKET_ERROR) 
 { 
  AfxMessageBox("关闭连接失败"); 
  m_hSocket=NULL; 
  return; 
 } 
   

该程序的运行效果如图 10.6 和 10.7 所示。
图 10.6   自动回复系统客户程序在发送数据后收到自动回复的信息运行效果
注意:客户程序必须在服务器程序运行并开启服务器后,才可以发送数据,否则会发生
错误。
图 10.7   自动回复系统在开启服务器后的运行效果图第 10 章  多线程与网络编程初步  ·291·
·291·
10.7   习        题
1.  填空题
(1) winsock 有两种类型,它们是____________和____________。
(2)  创建一个新进程需要使用函数____________________。
(3)  终止其他线程需要使用函数______________________。
(4)  在使用流式套接字建立服务器应用程序时,需要使用函数 socket、_______、listen、
_____________及 send 和 recv 等。
2.  选择题
(1)  下列(    )函数是使用流式套接字创建客户应用程序所不需要的。
A. connect( )     B. bind( )       C. recv( )       D. socket( ) 
(2)  下列(    )方法不是创建新线程的方法。
A. CreateThread     B. AfxBeginThread  C. CreateProcess 
(3) http 协议使用的端口是(    ) 
A. 80                      B. 21                      C. 23                    D. 25 
3.  简答题
(1)  简述流式套接字与数据报套接字的区别。
(2)  简述 ExitProcess 和 TerminateProcess 的异同。
(3)  简述进程的优先级,在 VC 中都用什么来表示。
4.  操作题
(1)  将 10.5 节中的网络通信例子上机实践,并适当完善,使其能够在客户/服务器之间
进行简单的对话。
(2)  上机操作创建新的进程程序,并试着关闭调用进程,看看被创建的进程处于什么
状态。
(3)  将 10.6 节中的实例上机实现,并使用 CSocket 类来重新实现该系统。

你可能感兴趣的:(MFC,windows,via,c++)