4.1多任务处理
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应,。这种类型的服务器称为"迭代服务器(iterative server)"。(顺序处理请求)
本节我们将介绍两种实现并行服务器(concurrent servers)的编程方法,分别为:一客户一线程(thread-per-client),即为每一个客户端连接创建一个新的线程;
线程池(thread pool),即将客户端连接分配给一组事先创建好的线程
4.1.1Java多线程
当Thread对象的start()方法被调用时,Java虚拟机将在一个新的线程中执行该对象的run方法,从而实现并行.
一客户一线程
在一客户一线程(thread-per-client)的服务器中,为每个连接都创建了一个新的线程来处理.
区别于迭代服务器,该服务器为每个连接创建了一个新的线程来处理,而不是直接处理.
当多个客户端几乎同时连接服务器时,后请求的客户端不需要等服务器对前面的客户端处理结束后才获得服务,相反,它们看起来是同时接受的服务
线程池
当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
线程池服务器首先创建一个ServerSocket实例。然后创建N个线程,每个线程都反复循环,从(共享的)ServerSocket实例接收客户端连接.
与一客户一线程服务器不同,线程池中的线程在完成对一个客户端的服务后并不终止,相反,它又重新开始在accept()方法上阻塞等待。
4.1.5系统管理调度:Executor接口
将客户服务器协议的细节封装起来(如EchoProtocol.java),就可以通过同一个协议实现来使用不同的"调度"方法
Executor接口(java.util.concurrent包的一部分)就代表了一个根据某种策略来执行Runnable实例的对象,其中可能包括了排队和调度的细节,或如何选择要执行的任务
4.2阻塞和超时
ServerSocket的accept()方法和Socket的构造函数都会阻塞等待,直到连接建立
4.2.1accept(),read()和receive()
我们可以使用Socket类、ServerSocket类和DatagramSocket类的setSoTimeout()方法,设置其阻塞的最长时间(以毫秒为单位).
对于Socket实例,在调用read()方法前,我们还可以使用该套接字的InputStream的available()方法来检测是否有可读的数据
4.2.2连接和写数据
Socket类的构造函数会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时(无法缩短),可以使用Socket类的无参构造,它返回的是一个没有建立连接的Sokcet实例,建立连接时,调用connect()方法,并指定一个远程终端和超时时间(毫秒)
write()方法调用也会阻塞等待,直到最后一个字节成功写入到了TCP实现的本地缓存中
如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端(详情见第6.1节)。因此,write()方法的阻塞总时间最终还是取决于接收端的应用程序。不幸的是Java现在还没有提供任何使write()超时或由其他线程将其打断的方法。所以如果一个可以在Socket实例上发送大量数据的协议可能会无限期地阻塞下去
4.3多接受者
一个客户端和一个服务器的通信方法被称为单播.
在需要一对多服务的时候,可以将复制数据包的工作交给网络来做,而不是由发送者负责.
4.3.1广播
与单播的区别是地址不同
IPv6有一个特殊的全节点(all-nodes),本地连接范围(link-local-scope)的多播地址,FFO2::1,
Ipv4的本地广播地址(255:255:255:255)将消息发送到在同一广播网络上的每个主机
发送给该地址的消息将多播到一个连接上的所有节点
4.3.2多播
与单播之间的主要区别也是地址的形式.
一个多播地址指示了一组接收者. IP协议的设计者为多播分配了一定范围的地址空间,IPv4中的多播地址范围是224.0.0.0到239.255.255.255,IPv6中的多播地址是任何由FF开头的地址.
除了少数系统保留的多播地址外,发送者可以向以上范围内的任何地址发送数据.
。Java中多播应用程序主要通过MulticastSocket实例(其实就是一个UDP套接字,只是包含了一些额外的可以控制的多播特定属性)进行通信,它是DatagramSocket的一个子类.
4.4控制默认行为
TCP/IP协议的开发者用了大量的时间来考虑协议的默认行为,以满足大部分应用程序的需要
4.4.1Keep-Alive
该机制在经过一段不活动时间后,将向另一个终端发送一个探测消息。如果另一个终端还出于活跃状态,它将回复一个确认消息。如果经过几次尝试后依然没有收到另一终端的确认消息,则终止发送探测信息,关闭套接字,并在下一次尝试I/O操作时抛出一个异常
4.4.2发送和接收缓存区的大小
一旦创建了一个Socket或DatagramSocket实例,操作系统就必须为其分配缓存区以存放接收的和要发送的数据
只能设置接收缓冲区大小(没时间),而不能设置发送缓冲区(有时间)
4.4.3 超时
很多I/O操作如果不能立即完成就会阻塞等待:读操作将阻塞等待直到至少有一个字节可读;接收操作将阻塞等待直到成功建立连接。不幸的是阻塞的时间没有限制。可以为各种操作指定一个最大阻塞时间。
4.4.4地址重用
对于TCP,当一个连接关闭后,通信的一端(或两端)必须在"Time-Wait"状态上等待一段时间,以对传输途中丢失的数据包进行清理.所以需要能够与正在使用的地址进行绑定的能力,要求实现地址重用(Socket的setReuseAddress)
4.4.5消除缓冲延迟
getTcpNoDelay()和setTcpNoDelay()方法用于获取和设置是否消除缓冲延迟。将值设置为true表示禁用缓冲延迟功能。
4.4.6紧急数据(队列中插队)(urgent)
SendUrgentData(int data)
SetOOBInline(true)启动接收者对频道外数据的接收
4.4.7关闭后停留
默认当调用套接字的close()方法后,即使套接字的缓冲区中还有没有发送的数据,它也将立即返回,可以设置方法停留让其延后close
SetSoLinger(Boolean on, int linger)
4.4.10基于性能的协议选择
Socket, ServerSocket: 指定协议参数选择
void setPerformancePreferences(int connectionTime, int latency, int bandwidth)
套接字的性能参数由三个整数表示,分别代表连接时间,延迟和带宽。具体的数值并不重要,Java将比较各种标准的相关参数值,并返回与之最匹配的可用协议。例如,如果connectionTime和latency都等于0,bandwidth等于1,那么则将选择能够使带宽最大的协议。注意,要使这个方法生效,必须在套接字建立连接之前调用。
4.5关闭连接
对于HTTP协议,是由服务器端发起的关闭连接。客户端先向服务器发送一个请求("get"),然后服务器发送回一个响应头信息(通常由"200 OK"开始),后面跟的是所请求的文件。由于客户端不知道文件的大小,因此服务器必须通过关闭套接字来指示文件的结束。
幸运的是套接字提供了一种实现这个功能的方法。Socket类的shutdownInput()和shutdownOutput()方法能够将输入输出流相互独立地关闭。调用shutdownInput()后,套接字的输入流将无法使用。任何没有发送的数据都将毫无提示地被丢弃,任何想从套接字的输入流读取数据的操作都将返回-1