




已阅读5页,还剩120页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
,第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 connection AcceptSocket=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-9 UINT 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*1024 struct Filedata char 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;n Input 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(““); 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“); /监听套接字 else printf(“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
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年防城港市金湾小学招聘教师考试笔试试题(含答案)
- 煤炭资源勘探开发合同
- 北京消防安全知识培训课件
- 护理相关知识考核试题及答案
- 2025上半年教资作文真题幼儿园含答案
- 2025《药品网络销售监督管理办法》考核题(含答案)
- 2006年7月国开电大法律事务专科《刑法学(2)》期末纸质考试试题及答案
- 2025年【G1工业锅炉司炉】作业考试题库及G1工业锅炉司炉考试试题(含答案)
- 北京地铁消防知识培训课件
- (2025)全科医学医师考试题库及参考答案
- 城市发展史起源演变和前景概述课件
- 麻醉术后护理业务学习
- 人教版高二语文必修四《中华文化精神》教学设计
- 初中数学-综合与实践 哪一款“套餐”更合适教学课件设计
- 采油采气井控题库
- Cpk 计算标准模板
- 精选浙江省普通高中生物学科教学指导意见(2023版)
- “魅力之光”核电知识竞赛试题答案(二)(110道)
- 外科学课件:食管癌
- 汽机专业设备运行日常点检
- GB/T 2820.12-2002往复式内燃机驱动的交流发电机组第12部分:对安全装置的应急供电
评论
0/150
提交评论