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 blocke.printStackTrace();}}}@Overridepublic 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();}}
啟動伺服器端
啟動用戶端
查看伺服器端