通信网络程序设计(王晓东西电版)第8章多线程程序设计技术

上传人:xiao****017 文档编号:16357187 上传时间:2020-09-28 格式:PPT 页数:125 大小:395KB
返回 下载 相关 举报
通信网络程序设计(王晓东西电版)第8章多线程程序设计技术_第1页
第1页 / 共125页
通信网络程序设计(王晓东西电版)第8章多线程程序设计技术_第2页
第2页 / 共125页
通信网络程序设计(王晓东西电版)第8章多线程程序设计技术_第3页
第3页 / 共125页
点击查看更多>>
资源描述
,第8章 多线程程序设计技术,8.1 服务器线程模型 8.2 多线程应用环境 8.3 线程基本操作函数 8.4 线程同步 8.5 并发线程模型服务器设计 8.6 完成端口服务器设计 小结,通过前面几章的学习,读者已经基本掌握了在主要几种网络协议下,进程之间进行网络数据通信的程序设计的基本方法。然而,实际的网络服务程序的开发设计要复杂得多,需要运用一些辅助技术,例如处理并发服务的多线程技术、快速开发网络程序的封装技术等。因此,从本章开始将陆续介绍一些网络编程的相关技术。在C/S工作模式下,总是少量的服务器为众多的、数量不可预测的多个客户端服务。客户端服务请求的到达不但难以预测,而且具有显著的并发性,为此服务器必须设计成能够服务于并发请求,且具有伸缩性。,本章将从多线程概念入手,介绍可伸缩服务,继而介绍线程同步的方法以及完成端口技术。本章内容多使用在服务器端,因此在程序设计的方法介绍上更加侧重于服务器编程。,网络服务器设计可以采用两种线程模型,即串行模型和并发模型。串行模型采用单个线程等待客户端的请求,当请求到来时,该线程醒来并处理请求,这种模型可以应用于简单的服务器程序,其优点是服务器接受的请求比较少,而且请求能被很快地处理。,8.1 服务器线程模型,串行模型的缺点也十分明显,当多个客户端同时向服务器发出请求时,这些请求必须依次被接受,并且后一个请求总是在前一个请求处理完毕后才能被接受。然而,在网络服务应用程序工作中,随时可能面临为多个不同时间到达的请求提供服务的局面(互联网上几乎所有服务程序都面临这样的问题),显然,这种局面是串行模型无法应对的。为了解决串行模型存在的问题,可以使用并发模型。并发模型使用单个线程等待客户端请求,当请求到来时,创建新线程来处理请求。等待客户请求的线程继续等待另一个客户端请求,新线程完成请求处理。当新线程处理完客户端请求后,随即退出。,由于并发模型为每个客户端都会创建一个新的线程,客户端请求能够很快地被处理,并且每个客户端请求都有自己的线程,因此服务效率要明显好于串行模型,且基于并发模型的服务器程序具有良好的伸缩性,当升级硬件时,服务器程序的性能可以得到提高。下面以WinSock的面向连接服务器编程为例,说明并发模型的工作原理(如图8-1所示)。首先安排主线程作为监听线程(执行路径),创建一个套接字监听客户端的连接请求。当有一个连接请求到达时,让主线程创建一个新的套接字。,监听套接字将接受的客户端连接递交给新的套接字,然后创建一个线程并由该线程提供具体的服务,而主线程继续监听来自客户端的连接请求。线程服务结束后,释放资源。当主线程再次收到下一个连接请求时(前面线程的服务不一定结束),再创建一个新套接字接收连接请求,并创建相应线程提供服务。依次类推,从而实现服务器的并行服务。这样不但满足了少量服务器端为多个客户端服务的需求,也为各个用户数据传输的独立性提供了保证,可以说是对并发服务请求问题的初步解决(更深层次的问题讨论参见8.6节)。,图8-1 并发模型工作原理图,将一个接受的连接递交给另外一个套接字的方法很简单,只要利用accept()函数的返回值即可,程序代码如下:SOCKET m_socket, /the socket for waiting connection AcceptSocket; /the socket for accepting a connectionAcceptSocket=accept(m_socket,NULL,NULL);程序中的m_socket用于主线程监听网络连接,AcceptSocket用于接收一个m_socket已经接受的网络连接,更详细的代码可以参考8.5节中的程序示例。,多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的执行路径(线程)来执行不同的任务,也就是说,允许单个程序创建多个并行执行的子程序来完成各自的任务。例如,一个公司里有很多各司其职的职员,那么可以认为这个正常运作的公司就是一个进程,而公司里的职员就是线程。公司(进程)作为一个功能整体完成某一宏观任务,而员工在这个整体框架下,各负其责完成自己的工作。,8.2 多线程应用环境,一个公司至少得有一个职员,同理,一个进程至少包含一个线程(事实上目前几乎已经没有单线程的网络商业应用软件),浏览器就是一个很好的多线程的例子。在浏览器中,用户可以同时完成下载Java小应用程序或图像、滚动页面、播放动画和声音、打印文件等,就是基于多线程技术实现的。多线程的使用可以提高CPU的利用率,这是因为在多线程程序中,当一个线程必须等待时,CPU可以运行其他的线程而不是等待,从而大大提高了程序的效率。多线程程序设计通过分割、组织工作任务,合理利用了任务的阻塞时间,有效地提高了程序的运行效率,但是本质上它并没有提升硬件设备本身的性能,因此并不是任何场合都适用,归纳起来它能够适合以下几种编程环境:,(1) 通过网络(例如,与Web服务器、数据库或远程对象)进行通信;(2) 执行需要较长时间因而可能导致 UI 冻结的本地操作;(3) 区分各种优先级的任务;(4) 提高应用程序启动和初始化的性能。进行多线程程序设计时应当注意:线程越多,占用内存也越多,线程之间需要协调和管理,对共享资源的访问会相互影响,太多的线程会导致控制复杂和系统不稳定(线程之间的切换会耗费大量的CPU计算时间)。这些都是使用多线程时的不利因素,因此应当慎重。,线程在操作系统中的存在有多种状态,好比人的生老病死,相对应于初始态、可运行态、阻塞/非可运行态和死亡态等四种状态。程序设计中,进入或改变这些状态需要一些函数的支持,学会运用这些函数也就掌握了多线程的编程技术。Windows为用户提供了完备的基本线程操作,下面具体来看一下这些基本线程操作函数。,8.3 线程基本操作函数,8.3.1 创建线程函数创建函数用来创建一个新的线程,使得进程进入初始态(也可通过设定参数直接进入可运行态),该函数的原型为HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, /线程安全属性DWORD dwStackSize, /初始线程堆栈大小LPTHREAD_START_ROUTINE lpStartAddress, /线程函数指针LPVOID lpParameter, /传递入线程的参数,DWORD dwCreationFlags, /线程创建标记LPDWORD lpThreadId); /返回线程ID其中,参数lpThreadAttributes只在Windows NT下有用,dwStackSize如果为0,则与进程主线程栈相同;lpStartAddress指定线程开始运行的地址;dwCreateFlages可以设定是否创建后挂起线程,若挂起后调用ResumeThread继续执行。除了CreateThread,创建线程的函数还有CreateRemoteThread等。,8.3.2 设置线程的优先级函数在多线程编程中,每一个线程都有一个优先级来确定该线程在进程中的优先地位。通常,一个进程的每个优先级包含了五个线程的优先级水平。当两个线程在使用CPU资源发生冲突时,执行优先级高的线程,例如:Normal级的线程可以被除了Idle级以外的任意线程抢占。在进程的优先级类确定之后,还可以改变线程的优先级水平。具体可使用SetThreadPriority设置线程优先级,函数的原型为BOOL SetThreadPriority( HANDLE hThread,int nPriority);,其中,参数hThread为所操作的线程对象句柄;参数nPriority为需要设定的线程优先级,可参考表8-1。,表8-1 线程的优先级,8.3.3 挂起/恢复线程Windows使用ResumeThread()函数使线程进入可运行态,函数原型如下:DWORD ResumeThread(HANDLE hThread); /恢复运行线程句柄对于进入运行态的线程可以使用SuspendThread函数退出运行态(好比按下录音机的暂停键),函数原型如下:DWORD SuspendThread(HANDLE hThread); /挂起线程句柄对于挂起的线程,其现场数据会被很好地保存,只要调用ResumeThread函数就可恢复运行。,8.3.4 等待函数Win32提供了一组等待函数用来让一个线程阻塞自己的执行,从而进入阻塞/非可运行态,以等待某种条件的满足。这些函数对于后续介绍的线程同步很有用。等待函数主要分为以下三类。1等待单个对象 这类函数包括SignalObjectAndWait()、WaitForSingleObject(),以及在这些函数名基础上用“WSA”开头或“Ex”结尾的扩展函数等。列举这类函数的代表性函数的原型如下:,DWORD SignalObjectAndWait(HANDLE hObjectToSignal,/置位对象句柄HANDLE hObjectToWaitOn,/等待对象句柄DWORD dwMilliseconds, /等待时间BOOL bAlertable ); /完成例程并加入队列时是否返回DWORD WaitForSingleObject(HANDLE hHandle, /等待对象DWORD dwMilliseconds ); /等待时间,上述函数的等待对象句柄可以指向Change notification、Console input、Event、Job、Mutex、Process、Semaphore、Thread、Waitable timer多种类型。另外,等待时间的设置直接影响着函数的返回,在等待时间达到后返回。如果等待时间不限制,则只有同步对象获得信号才返回;如果等待时间为0,则在测试了同步对象的状态之后马上返回。,2等待多个对象这类函数包括WaitForMultipleObjects()和MsgWaitForMultipleObjects(),以及在这些函数名基础上用“WSA”开头或“Ex”结尾的扩展函数等函数,实际上在6.6.3节我们已经接触过WSA WaitForMultipleEvents()函数,用于Winsock I/O事件的监控。这类函数的代表性函数的原型如下:DWORD WaitForMultipleObjects( DWORD nCount, /等待对象的数量CONST HANDLE *lpHandles,/等待对象句柄数据组,BOOL fWaitAll, /等待标志DWORD dwMilliseconds ); /等待时间DWORD MsgWaitForMultipleObjects( DWORD nCount, /等待对象数量LPHANDLE pHandles, /等待对象句柄数据组BOOL fWaitAll, /等待标志DWORD dwMilliseconds,/等待时间DWORD dwWakeMask ); /输入等待事件类型,它们的参数包括同步对象的句柄,等待时间,等待一个还是多个同步对象,等等。,3可以发出提示的等待函数这类函数包括MsgWaitForMultipleObjectsEx()、SignalObjectAndWait()、WaitForMultipleObjectsEx()、WaitForSingleObjectEx(),这些函数主要用于重叠(Overlapped)的I/O操作,函数原型略,可参考MSDN。等待函数会自动将等待对象设置为“置位”状态,使用时要引起注意。,8.3.5 终止一个线程函数当一个线程任务完成或出现问题时,可以对其实施终止操作从而使其进入死亡态。终止一个线程可以参用以下几个方法:(1) 调用ExitThread()函数,函数的原型为VOID ExitThread( DWORD dwExitCode); /线程退出号可以使用GetExitCodeThread()函数检索线程的退出号。(2) 调用TerminateThread()强行终止线程运行函数,函数的原型为,BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode);当用TerminateThread终止线程时,dll的入口函数DllMain()不会被执行(如果有dll的话)。每一个线程都不再使用局部存储数据时,线程释放它分配的动态内存。此外,终止一个线程的方法还有:引起主线程返回,从而导致ExitProcess被调用,进而导致ExitThread被调用;直接调用ExitProcess导致进程的所有线程终止;调用TerminateProcess终止一个进程时,导致其所有线程终止。,如1.2.4节所述,线程作为进程中的一个执行流,虽然每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区(还有全局数据区)是共享进程所共同拥有的,特别是相同优先级的线程在访问共同的数据区时,可能产生竞争和冲突。,8.4 线 程 同 步,例如:线程A试图给一个公共int变量m实施加1操作,而线程B却试图给m实施减1操作。如果任一个线程的操作实施成功,就会导致另外一个线程产生错误的计算结果,从而导致严重的后果。因此必须采用有效的方法协调线程对公共资源的访问,即进行线程同步。下面介绍网络程序设计中常用的四种同步对象,分别是临界区同步、事件同步、互斥同步、信号量同步。,8.4.1 临界区同步临界区是一段独占对某些共享资源进行访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后,其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。,以临界区对象来保持线程同步用到的函数主要有InitializeCriticalSection()、EnterCriticalSection()、LeaveCriticalSection()等,分别完成初始化、进入、退出临界区的功能。函数的原型为VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);,临界区对象控制线程同步的方法为:首先,定义一个关键段对象;然后,初始化该对象并把对象设置为NOT_SINGALED,表示允许线程使用资源;如果一段程序代码需要对某个资源进行同步保护,则这是一段临界区代码,在进入该临界区代码前调用函数EnterCriticalSection(),这样其他线程都不能执行该段代码,若它们试图执行就会被阻塞;完成关键段的执行之后,调用函数LeaveCriticalSection();其他的线程就可以继续执行该段代码。如果该函数不被调用,则其他线程将无限期地等待,例程如下:,CRITICAL_SECTION cs; /临界区结构对象char abc;/共享资源 UINT ThreadProc1(LPVOID pParam)EnterCriticalSection( /对共享资源进行写入操作,Sleep(30);LeaveCriticalSection( /对共享资源进行写入操作 /与ThreadProc1一致, /略去ThreadProc3-9UINT ThreadProc10(LPVOID pParam) /与ThreadProc1一致abc = j; /对共享资源进行写入操作 /与ThreadProc1一致,int main(int argc, char* argv) 创建线程hThread1=CreateThread(NULL,NULL,ThreadProc1,NULL, CREATE_SUSPENDED, ,hThreadN=CreateThread(NULL,NULL,ThreadProc10,NULL,C REATE_SUSPENDED, /等待计算完毕,printf(“%c/n”, abc); /报告计算结果return 0;这里仅给出了线程(主要是线程ThreadProc1)和主线程的部分代码,并假设各个线程都试图修改公共变量abc(下面同步方法的介绍都是基于此问题进行说明),程序打印出abc的最终结果。,8.4.2 事件同步事件对象对线程的控制是通过对事件对象位状态的控制来实现的。如果事件为置位状态,则线程可以对共享资源进行操作;如果为复位状态,则不能。事件对线程保持同步的控制原理如图8-2所示。线程B在执行到事件控制部分时,由于事件已经被A复位,因此将会发生阻塞,而A线程此时则可以在没有B线程干扰的情况下对共享资源进行处理,并在处理完成后对事件进行置位,使其被释放。B因而得以对A先前已处理完毕的共享资源进行操作(B线程开始执行时对事件进行复位)。,图8-2 事件对线程保持同步的操作原理,事件内核对象控制线程同步的方法为:首先,调用CreateEvent()函数创建一个事件对象,该函数的返回值为一个事件句柄hEvent;然后,线程函数则通过WaitForSingleObject()等待函数无限等待hEvent的置位,只有在事件置位时,WaitForSingleObject()才会返回,被保护的代码将得以执行;WaitForSingleObject()等到hEvent置位后就会立即将其复位,然后开始执行保护代码。复位有自动复位和人工复位两种形式,可在创建事件对象时指定复位形式。,自动复位是指当对象获得信号后,就释放下一个可用线程(优先级别最高的线程,如果优先级别相同,则等待队列中的第一个线程被释放);人工复位是指当对象获得信号后,就释放所有可利用线程。线程执行完保护代码后,将事件对象置位,例程如下:HANDLE hEvent = NULL; /事件句柄char abc; /共享资源 UINT ThreadProc1(LPVOID pParam),WaitForSingleObject(hEvent,INFINITE); /等待事件置位abc = a; /对共享资源进行写入操作Sleep(30);SetEvent(hEvent); /处理完成后即将事件对象置位return 0;,/ ThreadProc2-10略int main(int argc, char* argv) hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); /创建事件SetEvent(hEvent); /事件置位,/创建线程hThread1=CreateThread(NULL,NULL,ThreadProc1,NULL, CREATE_SUSPENDED, /初始化临界区,ResumeThread(hThread1) ; /启动线程ResumeThread(hThreadN);Sleep(300); /等待计算完毕printf( %c/n, abc); /报告计算结果return 0;,互斥对象在MFC中可以通过CEvent类进行表述,方法与上述程序类似,叙述从略。使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。,8.4.3 互斥同步互斥是一种用途非常广泛的内核对象,能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限。由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,,并由操作系统来管理,操作系统甚至还允许其进行一些其他内核对象所不能进行的非常规操作。为便于理解,可参照图8-3给出的互斥内核对象的工作模型。图8-3(a)中的箭头为要访问资源(矩形框)的线程,但只有第二个线程拥有互斥对象(黑点)并得以进入到共享资源,而其他线程则会被排斥在外(如图8-3(b)所示)。当此线程处理完共享资源并准备离开此区域时将把其所拥有的互斥对象交出(如图8-3(c)所示),其他任何一个试图访问此资源的线程都有机会得到此互斥对象。,图8-3 互斥内核对象的工作模型,以互斥对象来保持线程同步用到的函数主要有CreateMutex()、OpenMutex()、ReleaseMutex()等。CreateMutex()、OpenMutex()、ReleaseMutex()的原型为HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,/安全属性指针BOOL bInitialOwner, /互斥对象所有者标识LPCTSTR lpName ); /互斥对象名称指针HANDLE OpenMutex( DWORD dwDesiredAccess, /互斥对象访问标识BOOL bInheritHandle, /返回对象继承性LPCTSTR lpName ); /互斥对象名称指针,BOOL ReleaseMutex( HANDLE hMutex ); /互斥对象句柄互斥对象控制线程同步的具体方法为:首先要通过CreateMutex()(或OpenMutex()创建或打开一个互斥对象。然后调用等待函数,可以的话利用关键资源;最后,调用RealseMutex()释放互斥对象,例程如下:HANDLE hMutex = NULL;/互斥对象char abc; UINT ThreadProc1(LPVOID pParam),WaitForSingleObject(hMutex,INFINITE); /等待互斥对象通知abc = a; /对共享资源进行写入操作Sleep(30);ReleaseMutex(hMutex); /释放互斥对象return 0;/ ThreadProc2-10略,int main(int argc, char* argv) hMutex = CreateMutex(NULL, FALSE, NULL); /创建互斥对象或使用OpenMutex打开存在的互斥对象/创建线程hThread1=CreateThread(NULL,NULL,ThreadProc1,NULL, CREATE_SUSPENDED, ,hThreadN=CreateThread(NULL,NULL,ThreadProc10,NULL,C REATE_SUSPENDED, /等待计算完毕,printf(%c/n, abc); /报告计算结果return 0;与临界区对象不同,互斥对象可以在进程间使用,而临界区对象只能用于同一进程的线程之间。,8.4.4 信号量同步信号量是一个可以控制多个进程存取共享资源的计数器,经常作为一种锁定机制,以防止当一个进程正在存取共享资源时,另一个进程也存取同一资源。在Win32中,当信号量的数值变为0时再给以信号。在有多个资源需要管理时可以使用信号量对象。信号量内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对资源的控制如图8-4所示。,图8-4 信号量对象对资源的控制,图中分别以灰色阴影箭头和白色箭头表示共享资源所允许的最大资源计数和当前可用资源计数。初始如图8-4(a)所示,最大资源计数和当前可用资源计数均为4,此后每增加一个对资源进行访问的线程(用灰色阴影箭头表示),当前资源计数就会相应减1,图8-4(b)所示为3个线程对共享资源进行访问时的状态。当进入线程数达到4个时,将如图8-4(c)所示,此时已达到最大资源计数,而当前可用资源计数也已减到0,其他线程无法对共享资源进行访问。在当前占有资源的线程处理完毕而退出后,将会释放出空间,图8-4(d)已有两个线程退出对资源的占有,当前可用计数为2,可以再允许2个线程进入到对资源的处理。,以信号量对象来实现线程同步用到的函数有CreateSemaphore()、OpenSemaphore()和ReleaseSemaphore()函数。它们的原型为HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,/安全属性指针LONG lInitialCount, /初始数量LONG lMaximumCount, /最大数量LPCTSTR lpName ); /信号量指针名HANDLE OpenSemaphore( DWORD dwDesiredAccess, /访问标识,BOOL bInheritHandle, /对象继承性LPCTSTR lpName ); /信号量指针名BOOL ReleaseSemaphore( HANDLE hSemaphore, /信号量对象LONG lReleaseCount, /要增加的数量LPLONG lpPreviousCount ); /用于接受先前计数器的指针信号量控制线程同步的具体方法为:首先,调用函数CreateSemaphore()创建一个信号量(或调用函数OpenSemaphore()打开一个信号量)。,一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不再允许其他线程的进入,此时的信号量信号将无法发出。然后,调用等待函数,若允许,则线程就可以利用共享资源;线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数,加1,例程如下:HANDLE hSemaphore;/信号量对象句柄UINT ThreadProc1(LPVOID pParam)WaitForSingleObject(hSemaphore, INFINITE); /试图进入信号量关口 /线程任务处理ReleaseSemaphore(hSemaphore,1,NULL); /释放1个信号量计数,return 0;/ ThreadProc2-10略int main(int argc, char* argv) hSemaphore = CreateSemaphore(NULL, 2, 2, NULL); /创建信号量对象/创建线程,hThread1=CreateThread(NULL,NULL,ThreadProc1,NULL, CREATE_SUSPENDED, /启动线程,ResumeThread(hThreadN);return 0;区别于前面三种线程之间相互排斥的同步方法,信号量同步允许一定数量的线程共同工作,因此使其更适用于对Socket(套接字)程序中线程的同步,达到能够控制服务器线程池中工作线程的数量的目的。应当注意,在任何时候,当前可用资源计数决不可能大于最大资源计数。线程同步的方法不仅限于上述四种,还有等待定时器(waitable timer)、更改通知(change notification)、控制台输入(consle input)、作业(job)等同步对象可供使用。,通过上述学习,我们对多线程的编程技术有了一定的了解。结合WinSock的阻塞操作,程序员在编写网络程序时完全可以利用多线程技术,编写出面向多用户、集成化服务、可伸缩、非持续性的服务器程序,如大型FTP、HTTP服务器等,以及多种具有强大功能的网络程序。本节以一个简单的并发线程模型FTP服务器为例介绍具体的程序设计方法。#include StdAfx.h,8.5 并发线程模型服务器设计,#include #define MAX_FILESIZE 32*1024struct Filedatachar ffname30;char ffdataMAX_FILESIZE;int len;DataPacket;DWORD GetFile(char * fname,char * thrname),int i;FILE * fp;int Filesize;int count,total=0;char buffer100;char SenddataMAX_FILESIZE;fp=fopen(fname,r);if(fp=NULL)printf(cannot open %s for %sn,fname,thrname);return(0);,i=0;Filesize=0;memset(Senddata,0,MAX_FILESIZE);while(!feof(fp)count=fread(buffer,sizeof(char),100,fp);if(ferror(fp)printf(read file for %s error,thrname);break;,Filesize+=count;if(FilesizeMAX_FILESIZE)printf(the file for %s is too bign,thrname);fclose(fp);return(0);memcpy(,fclose(fp);Senddatai=0;strcpy(DataPacket.ffname,fname);memcpy(DataPacket.ffdata,Senddata,Filesize);DataPacket.len=Filesize;return 1;DWORD WINAPI AnswerThread(LPVOID lparam),SOCKET ClientSocket=(SOCKET)(LPVOID)lparam; int bytesRecv;char user1024=;char pass1024=;char sendbuf1024=;char recvbuf1024=;bytesRecv=SOCKET_ERROR;ZeroMemory(recvbuf,1024);/FTP会话略,while(1)/发送文件清单选择strcpy(sendbuf,available files list:nC:1.txt;nC:2.txt;nC:3.txt;nInput your filename to download:n);if(send(ClientSocket,sendbuf,strlen(sendbuf),0)=SOCKET_ERROR),printf(Error at socket():%ldn,WSAGetLastError();ZeroMemory(recvbuf,strlen(recvbuf); /接收文件if(recv(ClientSocket,recvbuf,1024,0)=SOCKET_ERROR)printf(Error %s at socket():%ldn,user,WSAGetLastError();if(strcmp(recvbuf,QUIT)=0),send(ClientSocket,221 GOOD BYE.,strlen(221 GOOD BYE.),0);break;if(GetFile(recvbuf,user)=0) /读文件ZeroMemory(sendbuf,strlen(sendbuf);strcpy(recvbuf,553);int j=send(ClientSocket,recvbuf,strlen(recvbuf),0);,continue;if(send(ClientSocket,(char *),if(recv(ClientSocket,recvbuf,1024,0)!=SOCKET_ERROR) printf(%s for %sn,recvbuf,user);return 0;int main(int argc,char* argv)WSADATA wsaData; /初始化WinSock,int iRet=WSAStartup(MAKEWORD(2,2), /创建一个套接字if(m_socket=INVALID_SOCKET),printf(Error at socket():%ldn,WSAGetLastError();WSACleanup();return 0;char szHostName256=; sockaddr_in service;service.sin_family=AF_INET;,service.sin_addr.s_addr=inet_addr(127.0.0.1);service.sin_port=htons(23);if(bind(m_socket,(SOCKADDR*),else printf(bind OK.n);if(listen(m_socket,20)=SOCKET_ERROR)printf(Error listening on socket.n); /监听套接字elseprintf(listening ok.n);printf(Waiting for a client to connect.n);,while(1)SOCKET AcceptSocket=SOCKET_ERROR;/接收连接请求while(AcceptSocket=SOCKET_ERRO AcceptSocket=accept(m_socket,NULL,NULL);DWORD dwThreadId; /创建服务线程HANDLE hThread;,hThread=CreateThread(NULL,NULL,AnswerThread,(LPVOID)AcceptSocket,0,本程序实现了一个简单的并发线程模型的FTP服务器(FTP协议介绍见12.3.1节)。服务器主线程首先打开21端口进行监听,当一个连接请求到达后,与客户端进行FTP协议对话(实际的FTP对话要复杂的多,详见12.3.1节),如果通过对话,则将该连接交给一个新的套接字,并在该套接字的基础上创建一个线程对此连接提供服务,主线程继续监听连接请求。服务线程提供的服务是由用户选择服务器中的一个本地文件下载到客户端的。因为没有提供客户端对文件的写操作,因此不会导致数据冲突,所以这里没有使用同步技术。,完成端口是微软在WinSock2中引入的一个新概念,目前已经是开发高效服务器程序的程序员必须掌握的方法。,8.6 完成端口服务器设计,8.6.1 完成端口概念并发线程模型虽初步解决了并发服务请求的问题,但是对于大规模互联网服务器的设计而言,还存在着效率问题。本节介绍的完成端口技术是目前解决该问题的最佳方法。,1并发模型缺陷分析通过深入分析并发模型,可以发现这种模型依然存在着不足之处。必须明确:并不是为每个客户端请求都创建一个线程,就一定会提高套接字应用程序的性能。并发模型在接受客户端请求后,会创建一个新的线程,当服务结束后,使线程退出;当新的客户端请求到来时,再创建新的线程,频繁地创建和销毁线程,会增加系统的开销。根据操作系统的工作原理,这种方法在系统内核进行线程上下文切换时会很费时,从而导致线程没有足够的时间为客户端提供服务。,2完成端口的定义为了解决并发模型的缺陷,微软提出了完成端口的概念。完成端口(I/O Completion Port,IOCP)是一个异步I/O的API。它将一个套接字与一个完成端口关联起来,并建立基于事件机制的WinSock操作。当一个事件发生的时候,完成端口就被操作系统加入到一个队列中。然后应用程序可以对核心层进行查询以得到此完成端口。完成端口可以高效地将I/O事件通知给应用程序,从而提高I/O效率。其实可以把完成端口看成系统维护的一个队列,操作系统把重叠I/O操作完成的事件通知放到该队列里,,由于暴露给客户端的是“操作完成”的事件通知,因此命名为“完成端口”(Completion Ports)。在完成端口理想模型中,每个线程都可以从系统获得一个“原子”性的时间片,轮番运行并检查完成端口。,3完成端口的作用完成端口的目标是实现高效的服务器程序,它克服了并发模型的不足。为了达到此目标,完成端口采用了两种方法:一是为完成端口指定并发线程的数量;二是在初始化套接字时(不是在客户请求到达时才创建)创建一定数量的服务线程,即所谓的线程池,当客户端请求到来时,这些线程立即为之服务,免去了线程创建与上下文切换所花费的时间。,之所以可以达到CPU工作效率最大化的目标,是因为完成端口的理论基础基于“并行运行的线程数量必须有一个上限”,而这个数值就是CPU的个数。如果一台机器有两个CPU,那么多于两个可以运行的线程就没有意义了。因为一旦运行线程数目超过CPU数目,系统就不得不花费时间来进行线程上下文件的切换。如图8-5所示,完成端口事先开好N个(与CPU数目一致)线程,再堵塞它们,这样就可以将所有用户的请求都投递到一个消息队列中去,然后那N个线程逐一从消息队列中取出消息并加以处理,这样做不仅减少了线程的资源,也提高了线程的利用率。,图8-5 完成端口工作模型,总之,完成端口为套接字应用程序管理线程池,避免反复创建线程的开销,同时根据CPU的数量决定并发线程数量,减少线程调度,从而提高服务器的程序性能。这种技术全面支持可伸缩(scalable)系统架构。这里的可伸缩系统是指随着RAM、磁盘空间或者CPU个数的增加而能够提升应用程序效能的一种系统。,8.6.2 完成端口函数实现完成端口需要以下三个函数的支持。1完成端口对象创建函数要在应用程序中利用完成端口模型,就必须首次创建完成端口对象,CreateIoCompletion-Port()函数可实现此功能,该函数的声明如下: HANDLE CreateIoCompletionPort ( HANDLE FileHandle, /文件句柄HANDLE ExistingCompletionPort, /存在的完成端口句柄,ULONG_PTR CompletionKey, /保存与套接字相关的信息的/完成健DWORD NumberOfConcurrentThreads ) /完成端口并发线程数如果CreateIoCompletionPort()函数调用成功,则返回完成端口的句柄;如果调用失败,则返回NULL。,2提取通知包操作函数GetQueuedCompletionStatus()函数试图从指定的完成端口中提取一个I/O操作完成通知包,该函数的原型如下:BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, /完成端口句柄 LPDWORD lpNumberOfBytes, /I/O操作完成时,返回实 /际传输数据的字节数 PULONG_PTR lpCompletionKey, /完成键指针,LPOVERLAPPED *lpOverlapped, /指向重叠结构的指针 DWORD dwMilliseconds); /完成端口上等待的时间该函数返回值有下面几种情况: 如果在完成端口上提取了一个成功的I/O操作完成通知包,则该函数返回TRUE。此时lpNumberOfBytesTransferred、lpCompletionKey和lpOverlappde参数返回相关信息。 如果在完成端口上提取了一个失败的I/O操作完成通知包,则该函数返回FALSE,此时lpOverlappde参数不等于NULL。 如果该函数调用超过时,返回值为FALSE,错误代码为WAIT_TIMEOUT。,3投递通知包操作函数PostQueuedCompletionStatus()函数试图投递I/O操作完成通知包到完成端口,该函数的原型如下:BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, /完成端口句柄 DWORD dwNumberOfBytesTransferred, /设定回传字节数 DWORD dwCompletionKey, /设定完成键值 LPOVERLAPPED lpOverlapped); /设定指向重叠结构,/指针的指针投递成功后,函数返回非零值;否则返回0。,8.6.3 完成端口程序设计完成端口对象取代了WSAAsyncSelect中的消息驱动和WSAEventSelect中的事件对象,当然完成端口模型的内部机制要比WSAAsyncSelect和WSAEventSelect模型复杂得多。,1完成端口使用步骤利用完成端口模型开发套接字应用程序,须经过创建完成端口、创建服务线程(通常服务线程数量为CPU数量的2倍)、将套接字与完成端口关联在一起、调用输入输出函数发起重叠I/O操作、在服务线程中的完成端口上等待重叠I/O的操作结果。进一步细化编程过程可以归纳为以下九个步骤:(1) 创建一个完成端口。一般一个应用程序只能创建一个完成端口。(2) 判断系统内到底安装了多少个处理器。每个处理器一次只允许执行一个工作者线程。,(3) 创建工作者线程,根据步骤(2)得到的处理器信息,在完成端口上为已完成的I/O请求提供服务。(4) 准备好一个监听套接字。(5) 使用accept()函数接受进入的连接请求。 (6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。(7) 调用CreateIoCompletionPort()将自accept()返回的新套接字句柄与完成端口关联到一起,通过完成键(CompletionKey)参数,将句柄数据结构传递给CreateIoCompletionPort()。,(8) 开始在已接受的连接上进行I/O操作。在此,通过重叠I/O机制在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,随后便会在步骤(3)指定的工作者例程中。(9) 重复步骤(5)(8),直到服务器终止。,2完成端口程序设计本程序完成端口的简约设计,对线程的服务部分进行了简化,重点突出完成端口的过程设计。本程序的设计涉及两个重要的数据结构,即“单I/O数据”和“单句柄数据”(扩展的OVERLAPPED结构)和两个重要的API:CreateIoCompletionPort和GetQueuedCompletionStatus。#include stdafx.h#include #include ,#pragma comment(lib,ws2_32.lib)#define BUFFER_SIZE 1024#define OP_READ 1;DWORD StartSock()WSADATA WSAData; if (WSAStartup(MAKEWORD(2,2),return(-1); if (LOBYTE(WSAData.wVersion)!=2|HIBYTE(WSAData.wVersion)!=2) /监测Winsock版本printf(the version of winsock is too low to support IOCP!n);,WSACleanup( );return 0; return 0;typedef struct _BUFFER_OBJ /单I/O数据OVERLAPPED ol;char bufBUFFER_SIZE;,int nOperatorType;BUFFER_OBJ,*PBUFFER_OBJ;typedef struct SOCKET s;SOCKET_OBJ,*PSOCKET_OBJ; /单句柄数据DWORD WINAPI ThreadProc(LPVOID lpParameter)HANDLE hCompletion = (HANDLE)lpParameter;PSOCKET_OBJ pSocket;PBUFFER_OBJ pBuffer;DWORD dwTrans;,while(true) BOOL ok=:GetQueuedCompletionStatus(hCompletion,GlobalFree(pBuffer); else if(dwTrans=0) printf(closen);closesocket(pSocket-s);GlobalFree(pSocket);GlobalFree(pBuffer); else,pBuffer-bufdwTrans=0;printf(%sn,pBuffer-buf);WSABUF buf;buf.buf=pBuffer-buf;buf.len=BUFFER_SIZE;DWORD dwBytes;DWORD dwFlags=0;if(SOCKET_ERROR=WSARecv(pSocket-s,int main(int argc, char* argv)StartSock();HANDLE hCompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);/步骤(1)SYSTEM_INFO SystemInfo; GetSystemInfo(,/步骤(2)for(int i=0;i(int)SystemInfo.dwNumberOfProcessors*2;i+)/步骤(3),创建规模为CPU/数两倍的线程池HANDLE ThreadHandle; /创建一个工作线程,并将完成端口作为参数传递给它if(ThreadHandle=CreateThread(NULL,0,ThreadProc,hCompletionPort,0,NULL)= NULL),printf(CreateThread() failed with error %dn, GetLastError();return 0;CloseHandle(ThreadHandle); /关闭线程句柄,SOCKET sListen=socket(AF_INET,SOCK_STREAM,0); /步骤(4)sockaddr_in addr; addr.sin_addr.S_un.S_addr=INADDR_ANY;addr.sin_family=AF_INET;addr.sin_port=htons(4567);bind(sListen,(sockaddr*)while(true),sockaddr_in remote;int len=sizeof(sockaddr_in);SOCKET client=accept(sListen,(sockaddr*) /步骤(6),pSoc
展开阅读全文
相关资源
相关搜索

最新文档


当前位置:首页 > 图纸专区 > 课件教案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!