4.2 阻塞和超时
Socket的I/O调用可能会因为多种原因而阻塞。数据输入方法read()和receive()在没有数据可读时会阻塞。TCP套接字的write()方法在没有足够的空间缓存传输的数据时可能阻塞。
4.2.1 accept(),read()和receive()
对于这些方法,我们可以使用Socket类、ServerSocket类和DatagramSocket类的setSoTimeout()方法,设置其阻塞的最长时间(以毫秒为单位)。如果在指定时间内这些方法没有返回,则将抛出一个InterruptedIOException异常。对于Socket实例,在调用read()方法前,我们还可以使用该套接字的InputStream的available()方法来检测是否有可读的数据。
4.2.2 连接和写数据
Socket类的构造函数会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时。不幸的是,系统定义的超时时间很长,而Java又没有提供任何缩短它的方法。要改变这种情况,可以使用Socket类的无参数构造函数,它返回的是一个没有建立连接的Socket实例。需要建立连接时,调用该实例的connect()方法,并指定一个远程终端和超时时间(毫秒)。
write()方法调用也会阻塞等待,直到最后一个字节成功写入到了TCP实现的本地缓存中。如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端。因此,write()方法的阻塞总时间最终还是取决于接收端的应用程序。不幸的是Java现在还没有提供任何使write()超时或由其他线程将其打断的方法。所以如果一个可以在Socket实例上发送大量数据的协议可能会无限期地阻塞下去。
4.2.3 限制每个客户端的时间
使用如下协议可以在代码级别对服务时间进行限制
package com.suifeng.tcpip.chapter4; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.logging.Logger; /** * 独立于客户服务器的协议 * @author Suifeng * */ public class TimeLimitEchoProtocol implements Runnable { private static final int TIME_LIMIT = 5000; private static final int BUFFER_SIZE = 32; private Socket socket; private Logger logger; public TimeLimitEchoProtocol(Socket socket, Logger logger) { super(); this.socket = socket; this.logger = logger; } public static void handleEchoClient(Socket socket, Logger logger) { try { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); int totalBytes = 0; int recvSize = -1; long endTime = System.currentTimeMillis()+TIME_LIMIT; int timeBounds = TIME_LIMIT; byte[] buffer = new byte[BUFFER_SIZE]; // 设置读取的超时时间 socket.setSoTimeout(TIME_LIMIT); // 限制服务时间,避免超时 while(timeBounds > 0 && (recvSize = in.read(buffer)) != -1) { out.write(buffer, 0, recvSize); totalBytes += recvSize; timeBounds =(int)( endTime - System.currentTimeMillis()); // 重新设置超时时间 socket.setSoTimeout(timeBounds); } logger.info(Thread.currentThread().getName()+" is handling Echo now"); logger.info("Client "+socket.getRemoteSocketAddress()+" echoed "+totalBytes+" bytes"); } catch (IOException e) { logger.warning("Exception in echo protocol:"+e.getMessage()); e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } @Override public void run() { handleEchoClient(socket, logger); } }
4.3 多接收者
我们的套接字都处理的是两个实体之间的通信,通常是一个服务器和一个客户端。这种一对一的通信方法有时称为单播(unicast)。而对于某些信息,多个接收者都可能对其感兴趣。对于这种情况,我们可以向每个接收者单播一个数据副本,但是这样做效率可能非常低。由于将同样的数据发送了多次,在一个网络连接上单播同一数据的多个副本非常浪费带宽。
幸运的是网络提供了一个更有效地使用带宽的方法。我们可以将复制数据包的工作交给网络来做,而不是由发送者负责。
有两种类型的一对多(one-to-many)服务:广播(broadcast)和多播(multicast)。对于广播,(本地)网络中的所有主机都会接收到一份数据副本。对于多播,消息只是发送给一个多播地址(multicast address),网络只是将数据分发给那些表示想要接收发送到该多播地址的数据的主机。总的来说,只有UDP套接字允许广播或多播。
4.3.1 广播
广播UDP数据报文与单播数据报文相似,唯一的区别是其使用的是一个广播地址而不是一个常规的(单播)IP地址。注意,IPv6并没有明确地提供广播地址;然而,有一个特殊的全节点(all - nodes)、本地连接范围(link-local-scope)的多播地址,FFO2::1,发送给该地址的消息将多播到一个连接上的所有节点。IPv4的本地广播地址(255.255.255.255)将消息发送到在同一广播网络上的每个主机。本地广播信息决不会被路由器转发。在以太网上的一个主机可以向在同一以太网内的其他主机发送消息,但是该消息不会被路由器转发。IPv4还指定了定向广播地址,允许向指定网络中的所有主机进行广播。
并不存在可以向网络范围内所有主机发送消息的广播地址。在这种地址发送单个数据报文就可能会由路由器产生非常大量的数据包副本,并可能会耗尽所有网络的带宽。
即使如此,本地广播功能还是非常有用的,它通常用于在网络游戏中处于同一本地(广播)网络的玩家之间交换状态信息。
4.3.2 多播
一个多播地址指示了一组接收者。IP协议的设计者为多播分配了一定范围的地址空间。IPV4的多播地址范围为224.0.0.0到239.255.255.255,IPV6的多播地址范围为任何由FF开头的地址。
下面是一个通过多播发送消息的例子。
package com.suifeng.tcpip.chapter4.multicast; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.net.UnknownHostException; import com.suifeng.tcpip.chapter3.vote.VoteMsg; import com.suifeng.tcpip.chapter3.vote.VoteMsgCoder; import com.suifeng.tcpip.chapter3.vote.VoteMsgTextCoder; public class VoteMulticastSender { public static final int CANDIDATE = 888; public static void main(String[] args) throws IOException { if (args.length < 2 || args.length > 3) { throw new IllegalArgumentException("Parameters:<Multicast Address> <Port> [<TTL>]"); } InetAddress destAddress = InetAddress.getByName(args[0]); // 检查是否是多播地址 if (!destAddress.isMulticastAddress()) { throw new IllegalArgumentException("Not a multicast address"); } int destPort = Integer.parseInt(args[1]); int TTL = (args.length == 3) ? Integer.parseInt(args[2]) : 1; MulticastSocket multicastSocket = new MulticastSocket(); // 设置报文的声明周期 multicastSocket.setTTL((byte) TTL); VoteMsgCoder coder = new VoteMsgTextCoder(); VoteMsg vote = new VoteMsg(true, true, CANDIDATE, 100001L); // 整理消息 byte[] msg = coder.toWire(vote); DatagramPacket message = new DatagramPacket(msg, msg.length, destAddress, destPort); System.out.println("Sending Text-Encode request (" + msg.length + " bytes)"); System.out.println(vote); // 发送消息 multicastSocket.send(message); multicastSocket.close(); } }
多播发送和单播发送的区别,1、对给定地址是否是多播地址进行验证;2、多播报文设置了初始的TTL(Time To Live,生命周期)。
网络多播只将消息发送给指定的一组接收者,这组接收者叫做多播组。
package com.suifeng.tcpip.chapter4.multicast; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.Arrays; import com.suifeng.tcpip.chapter3.vote.VoteMsg; import com.suifeng.tcpip.chapter3.vote.VoteMsgCoder; import com.suifeng.tcpip.chapter3.vote.VoteMsgTextCoder; public class VoteMulticastReceiver { public static void main(String[] args) throws IOException { if (args.length != 2) { throw new IllegalArgumentException("Parameters:<Muticast Addr> <Port>"); } InetAddress address = InetAddress.getByName(args[0]); // 检查是否是多播地址 if (!address.isMulticastAddress()) { throw new IllegalArgumentException("Not a multicast address "); } int port = Integer.parseInt(args[1]); MulticastSocket multicastSocket = new MulticastSocket(port); // 设置要获取消息的多播地址 multicastSocket.joinGroup(address); VoteMsgCoder coder = new VoteMsgTextCoder(); System.out.println("Receiver is OK!!!Waiting for data from sender"); DatagramPacket message = new DatagramPacket(new byte[VoteMsgTextCoder.MAX_WIRE_LENGTH], VoteMsgTextCoder.MAX_WIRE_LENGTH); // 接收消息 multicastSocket.receive(message); VoteMsg vote = coder.fromWire(Arrays.copyOfRange(message.getData(), 0, message.getLength())); System.out.println("Received Text-encoded Request (" + message.getLength() + " bytes)"); System.out.println(vote); multicastSocket.close(); } }
启动服务器端
启动客户端
查看服务器端