13.3 使用UDP实现无连接网络
前面已经阐述了在Java中如何以Socket类的形式使用TCP网络连接,下一步将学习如何使用Java的MulticastSocket类创建一个无连接网络客户端。
1.MulticastConnection类
我们已经看到,TCP连接对于客户端和服务器之间的一对一通信是很好的。虽然TCP服务器可以对到来的几乎任何数量的客户端启动服务,但是所有的通信都被限制在双向连接中。这对于像跳棋或者棋类游戏这样两个人连接玩的游戏是很好的。然而,很多游戏要求在一群人中间而不是两个人之间通信。大众化的多人在线游戏就是要求在一群人之间通信的一个例子。像这样成千上万的人可以在广泛的虚拟世界中同时聚集起来的游戏,推动着当今的技术不断突破限制。
网络上的组间通信可以有很多方式。我们将专注于所谓的多点传送(Multicasting)来满足多用户应用程序的需要。不要把“多点传送”和“广播”这两个词搞混淆,广播客户端给网络上的每一个人发送消息,其中包括那些对接收这些消息不感兴趣的人,而多点传送客户端把传送的范围缩小为那些属于同一个组的客户端。
多点传送环境理解起来很像教室,教室里坐满聚精会神渴望知识的学生,一个教室里的交流只是在听这堂课的人之间进行。每一个教室都可以看作是整个教室网络中的一个子节点,通过扩音器传到每一个教室的通知可以看作是一个广播消息,因为它传给了整个网络中的每一个子节点。由于我们只对特定网络组中的成员之间的通信感兴趣,所以采用了通过多点传送来传送数据包的方式。
MulticastSocket类被归入所谓的D类IP地址。一个D类IP地址由其前4位来标志。D类地址必须在第一,第二和第三位上为“1”,而第四位必须为“0”.剩下的24位可以是任意的(默然:这里说的是二进制位哦,看不太明白可以忽略这一段,接着往下看)。最低的D类地址的第一个8位是11100000,即十进制的224。最高的D类地址的第一个8位是11101111,即十进制中的239。这样,D类地址的整个有效范围是224.0.0.0和239.255.255.255。然而,记住224.0.0.0是保留的,而224.0.0.1被用来发送给现存的所有多点传送主机,所以不能使用它们。其他的所有IP地址都可以使用。
注意:记住,一个多点传送组的地址必须是在224.0.0.1到239.255.255.255。在创建多点传送套接字时,不在这个范围的地址将产生一个SocketException。
前面已经提到多点传送客户端如何连接到一个普通IP地址和端口号。通常端口号又int变量的形式给出,关于IP地址的信息封装在InetAddress类中。InetAddress类包含一个静态的getByName方法来把一个String转换为IP地址,因此,可又用下面的方式创建一个InetAddress对象:
InetAddress groupAddress=InetAddress.getByName("229.13.77.21");
前面已经提过,MulticastSocket类以IP包或者说以数据报的方式传送或者接收数据。DatagramPacket可以容纳IP传送消息所需要的一切信息。记住,IP工作的方式类似于邮政系统,包中包含数据和数据应该发送的目的地址,因此,数据包可又像下面这样发送:
String msg="Icn bin maroon";
DatagramPacket packet=new DatagramPacket(msg.getBytes(),msg.length(),groupAddress,port);
MulticastSocket.send(packet);
如果大家理解加密消息,那么将发现这其实很简单。然而,大家可以看到,上面的包包含组成消息的原始字节和消息长度,又及目标IP地址及端口号。
数据报的接收也是一样简单。首先,必须建立一个有足够大的字节数组的DatagramPacket来容纳消息。由于通常的目的而言,1KB(1024byte)的缓冲区应该足够了。一旦接收到数据包,就可以以数据报中的字节数组为参数创建一个新的String对象,并打印出消息,就像下面这样:
byte[] data=new byte[1024];
dataPacket=new DatagramPacket(data,data.length);
multicastSocket.receive(dataPacket);
System.out.println(new String(dataPacket.getDate()).trim());
了解了这些后,看看下面的一个类的代码,这个类封装了加入和离开一个多点传输组的方法,以及发送和接收消息的方法。我们将使用MulticastConnection类来和一定数量的客户端建立多点传输组连接。
import java.io.*;
import java.net.*;

public class MulticastConnection...{
public static final int DEFAULT_PORT=1234;
//连接,发送和接收消息用的多点传送
protected MulticastSocket mcSocket;
//属于多点传送群中的一个IP地址
protected InetAddress groupAddress;
//连接用的端口号
protected int port;
//发送和接收消息的IP报
protected DatagramPacket dataPacket;
//接收和发送数据报用的byte数组
protected byte[] data;
//一个数据报可以容纳的字节的最大数目
protected final int PACKET_SIZE=1024;
//用给定的地址和端口创建一个新的MulticastConnection对象

public MulticastConnection(String address,int portNo)throws Exception...{
//将给定的地址解析为有效的IP地址
groupAddress=InetAddress.getByName(address);
port=portNo;
//确保端口号有效
mcSocket=(port>0)?new MulticastSocket(port):new MulticastSocket(DEFAULT_PORT);
//将socket连接到IP地址群
mcSocket.joinGroup(groupAddress);
data=null;
}
//试图从群中断开

public void disconnect()...{
if(mcSocket==null)return;

try...{

if(mcSocket==null)...{
mcSocket.leaveGroup(groupAddress);
}

}catch(IOException e)...{
}
}
//尝试从群中接收一个数据报

public String recv()...{
data=new byte[PACKET_SIZE];
dataPacket=new DatagramPacket(data,data.length);

try...{
mcSocket.receive(dataPacket);

}catch(IOException e)...{
return "";
}
//将数据报中的原始字节转化为一个有效的String对象
return new String(dataPacket.getData()).trim();
}

public boolean send(String msg)...{
dataPacket=new DatagramPacket(msg.getBytes(),msg.length(),groupAddress,port);

try...{
mcSocket.send(dataPacket);

}catch(IOException e)...{
return false;
}
return true;
}
}//MulticastConnection
3.创建一个可视化的排五点游戏
在因特网服务器上,同一个时间可能会有成百上千的人在玩各种版本的排五点游戏。因特网上排五点游戏赢得的奖品各不相同,可又是金钱,可又是荣誉标志,可又完全什么都没有。大多数人玩排五点并不是为了赢——他们只是因为它有趣才玩它。重要的是,互联网让开发者使生活倒退到过去的时代。
实现自己的排五点游戏框架的微妙之处在于:在服务器端,可以让程序加入某个多点传送组并开始广播叫牌。和其他的排五点游戏一样,在每一盘中,每一个数字只能叫一次。所有的数字叫完后,服务器对所有监听客户端发送一个消息来启动它们,这个过程不断执行。和聊天程序一样,我们的BingoServer applet也在一个Frame中运行。代码如下:
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
import com.speakmore.game.net.*;

public class BingoServer extends Applet...{
//用户广播数字的多点传送连接
protected MulticastConnection service;
//放置内部服务器消息的区域
protected TextArea textArea;
//容纳可用的bingo叫号
protected int[] numbers;
//这局游戏中bingo叫号数目
protected int numbersCalled;
//产生bingo叫号
protected Random random;
//在叫号之间等待的时间
protected final int CALL_PAUSE=3000;

public void init()...{
textArea=new TextArea("",15,60,TextArea.SCROLLBARS_VERTICAL_ONLY);
textArea.setEditable(false);
add(textArea);
random=new Random();
reset();
//连接到bingo组
String address="224.0.0.21";
int port=1234;

try ...{
service=new MulticastConnection(address,port);
textArea.append("系统消息:Java BINGO 在线服务 ");
}

catch (Exception ex) ...{
ex.printStackTrace();
}
}
//填充有效bingo叫号的数组(1~75)

public void reset()...{
numbers=new int[75];

for(int i=0;i<75;i++)...{
numbers[i]=i+1;
}
numbersCalled=0;
//清除文本域
textArea.setText("");
}

public void callNumber()...{
//检查是否所有的数字都被叫过

if(numbersCalled==75)...{
reset();
textArea.append("所有数字都被叫过!重新开始游戏...... ");
//向整个组广播reset动作
service.send("Reset");
//在开始一局新游戏前等待10秒

try ...{
Thread.sleep(10000);
}

catch (Exception ex) ...{
ex.printStackTrace();
}
}
//产生下一个叫号数字
int i=random.nextInt(75);

while(numbers[i]==-1)...{
i=random.nextInt(75);
}
//保存下一个数字并从数组中清除它
int n=numbers[i];
numbers[i]=-1;
//叫一个数字
textArea.append("Calling"+n+" ");
service.send(""+n);
++numbersCalled;
}
//启动服务器,不断叫号

public void start()...{

while(true)...{
callNumber();
//在下一次叫号前暂停

try ...{
Thread.sleep(CALL_PAUSE);
}

catch (Exception ex) ...{
ex.printStackTrace();
}
}
}
//创建一个BingoServer applet并把它加载到一个Frame中

public static void main(String[] args)...{
Applet a=new BingoServer();
a.init();
Frame f=new Frame("Java BINGO Server");
f.setSize(500,320);

f.addWindowListener(new WindowAdapter()...{

public void windowClosing(WindowEvent e)...{
System.exit(0);
}
});
f.add(a);
f.show();
a.start();
}
}//BingoServer
客户端要复杂很多,不过只是复杂在它要比服务器程序做更多的绘制工作。毕竟,我们的用户看到的是客户端程序。这里没有完全列出编写客户端的过程,读者已经可以自己写出来了。在实现客户端时,应注意以下几点:
q 服务器可以广播两种消息:一种是重启的消息,它告诉客户端清除所有的叫牌,重新开始游戏:另一种是一个1~75之间的数字。
q 在每一盘中,应该打印服务器所叫的每一个数字。记住数字1~15归入“B”,16~30归入“I”,依此类推。给定一个数字,我们可以用算法给出它归属的类。
q 只要一个用户调用“Bingo!”,客户端程序就应该在内部对所叫的数字进行验证。如果用户确实赢了,那么客户端程序将告诉服务器应该重新开始。
下面给出一个Bingo客户端,把它留给读者只是作为参考来完成自己的客户端。它决不是一个完整的Bingo客户端!