并发的优点
服务器使用并发有两个主要的原因:
- 并发可以改善观察到的响应时间,从而改善所有客户的总吞吐量
- 并发可以排除潜在的死锁。
其他原因:
- 并发实现使得设计人员易于创建多协议或者多服务的服务器
- 使用多进程实现并发非常灵活,因为这样就可以在多种硬件平台(多处理器/单处理器)上很好的运行
由于客户通常在一个时刻只进行一种活动,因此它似乎不能从并发中受益。客户一旦向服务器发送了一个请求,在收到响应之前不能进行起来活动。此外,客户的效率和死锁问题不如服务器那样严重,因为如果一个客户延缓或者停止执行,只有一个客户受到影响,而其他客户将继续运行。
尽管表面上如此,客户中的并发确实有优点。第一,并发实现更容易编程,因为功能已经被划分为概念上能分开的一些部分。第二,并发实现更易于维护和扩展,因为这使得代码模块化了。第三,并发客户可以在同一时刻联系几个服务器,或者比较响应时间,或者合并服务器返回的结果。第三,并发允许用户改变参数、查询客户状态或者动态的控制处理。
在客户中使用并发的最主要优点是异步性。异步性允许客户同时处理多个请求,而且不严格规定其执行顺序
运用控制的动机
异步
如下情况可能需要使用异步,即需要将控制功能和正常处理分开时。
大部分客户软件仅仅等待响应到达。当然,如果服务器发生故障或者发生死锁,客户将阻塞在那里,试图等待一个永远不会到达的响应。遗憾的是,由于网络时延很大或者服务器超负荷,用户不可能知道是否真的发生了死锁,还是处理得很慢。此外,用户不知道客户是否已经从服务器收到了一些报文。
如果用户不耐烦了,或者判断出一个特定的响应需要太多时间,也只有一种选择:放弃客户程序,以后再重试。在这种情况下,并发是很有帮助的,因为这个设计得当的并发客户可以使得用户在客户等待响应时,继续与客户交互。用户可以发现是否已经收到一些数据,并选择是发送一个不同的请求还是从容的终止通信。
举个例子:并发实现可以在进行数据库查询的同时,从用户的键盘或者鼠标读取和处理客户。因此,用户可以打开菜单,选择一个像status这样的命令,以判断客户是否成功打开了某个服务器的连接,并且是否已经发送了请求。用户可以选择abort停止通信,或者选择newserver命令客户终止现有通信,并尝试与另一个服务器通信。
与多个服务器的并发连续
并发能使得单个客户同时联系几个服务器,而且只要从任何服务器收到响应就向用户报告。比如,TIME服务的并发客户可以发送多个服务器,并且接收第一个等待到达的响应或者取几个响应的平均值。
实现并发客户
与并发服务器类似,大多数客户实现遵从两个基本方法之一:
- 客户分为两个或者多个执行线程,每个处理一个功能
- 客户只含一个线程,它使用select异步的处理多个输入和输出事件
对于Linux系统,因为允许一个进程中的多个线程共享内存,这样就使多线程实现能很好的运行。下图说明了在这样的系统中,如何使用多线程的方法支持面向连接的应用协议。
上图中,多线程允许客户把输入和输出处理分开。该图表示线程如何与若干文件描述符以及一个套接字描述符交互。一个输入线程(input thread)从标准输入读数据,形成请求,并通过TCP连接发送给服务器,而一个独立的输出线程(output thread)从服务器接收响应,并写入到标准输出。同时,第三个控制线程(control thread)从控制处理的用户那里接收命令。
单线程实现
单线程客户向单线程服务器一样,使用异步IO。客户为到多个服务器的连接创建套接字描述符。它还可以有一个或者多个用户获取键盘或者鼠标输入的描述符。客户程序的主体含有一个循环,该循环使用select等待其中的任何一个描述符准备就绪。如果输入描述符已经就绪,客户就读取输入,并且可以将输入存储起来以后再用,也可以立即开始处理输入。如果TCP连接输出就绪,客户就在此TCP连接上准备和发送请求。如果TCP连接输入就绪,客户就读取这个服务器发出的响应并加以处理。
当然,单线程的并发客户与单线程的服务器由很多共同的优点和缺点。客户读取输入或者读取来自服务器的响应,是按照产生这些速率进行的。即使服务器延迟了一小段时间,本地的处理扔继续进行下去。因此,即使服务器出故障不能响应,客户扔会继续读取或者执行控制命令。
如果单线程的客户调用会阻塞额系统功能,它就可能转为死锁状态。因此,程序员必须注意确保避免客户无限期的阻塞-----在那里等待不会发生的事情。这有很多实现方法,比如忽略某些情况,并允许用户检测已发生的死锁问题。
使用ECHO的并发客户例子
如下使用ECHO服务来测量一组机器的网络吞吐量
TCPtecho接收多台机器名作为参数。对每台机器,它打开一个到该机器上ECHO服务器的TCP连接,通过该连接发送ccount个字符(字节),读取从每个服务器上收到的返回字节,并打印完成任务所需的全部时间。因此,TCPtecho可以用于策略到一组机器的当前吞吐量
TCPtecho开始将字符计数变量初始化为默认值CCOUNT。然后它分析其参数,查看用户是否键入了-c选项。如果键入了,TCP就将指明的计数变量转换为一个整数,并将它存入ccount变量从而取代默认值
TCPtecho假定除-c标志外的所有参数指明了一个机器名。对于每个这样的参数,它调用connectTCP形成到使用该名字的机器上的ECHO服务器的一个TCP连接。TCPtecho在hname数组中记录其机器名,并调用FD_SET宏设置描述符掩码中该套接字对应的比特。它还在maxfd中记录最大描述符数
一旦为参数指明的每台机器建立了TCP连接,主程序就调用过程TCPecho处理数据的传输和接收。TCPtecho并发处理所有的连接。它将要发送的数据(字母D)填充buf缓存,然后调用select等待任何一个TCP连接输入或者输出就绪。当select调用返回时,TCOtecho遍历所有描述符以查看哪个就绪了。
当一个连接输出就绪,TCPecho就调用过程writer,write发送缓存中的数据,只要TCP在单个write调用中能接受,它就尽量将缓存中的数据发送出去。如果writer发现整个缓存都已经发送完毕,它就调用shutdown关闭这个用于输出的描述符,并从select所用的一组输出中删除该描述符
当一个描述符输入就绪,TCPtecho就调用过程reader,reader尽量从连接上接受数据,TCP能交付多少,它就读多少,并把这些数据放在缓存中。过程reader将读取的数据放入缓存,并减少剩余字符的计数。如果计数减到0(即使服务器已经收到的数据与被发出的数据一样多),过程reader就计算从开始传输数据以来逝去的时间,打印一个报文,并关闭连接。它还从select使用的一组输入中删除该描述符。因此,每当一个连接完成后,报告数据回显所需时间的消息就出现在输出上。
在一个连接上完成单个输入或者输出操作后,过程reader、witer都将返回,并接着在TCPtecho中再次调用select,继续进行循环。如果reader检测到文件结束的条件就返回0,并关闭连接,反之则返回1。TCPtecho使用reader的返回码来确定它是否应减少活动连接的数目。当连接数减少到0时,TEPecho中的循环将终止,TCPtecho将返回主程序。于是,客户端推出