基于UDP协议的网络编程
UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象。Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。
UDP协议基础
UDP协议是英文User Datagram Protocol的缩写,即用户数据报协议,主要用来支持那些需要在计算机之间传输数据的网络连接。UDP协议从问世至今已经被使用了很多年,虽然UDP协议目前应用不如TCP协议广泛,但UDP协议依然是一个非常实用和可行的网络传输层协议。尤其是在一些实时性很强的应用场景中,比如网络游戏、视频会议等,UDP协议的快速更具有独特的魅力。
UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。UDP协议适用于一次只传送少量数据、对可靠性要求不高的应用环境。
与前面介绍的TCP协议一样,UDP协议直接位于IP协议之上。实际上,IP协议属于OSI参考模型的网络层协议,而UDP协议和TCP协议都属于传输层协议。
因为UDP协议是面向非连接的协议,没有建立连接的过程,因此它的通信效率很高;但也正因为如此,它的可靠性不如TCP协议。
UDP协议的主要作用是完成网络数据流和数据报之间的转换——在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去;在信息的接收端,UDP协议将数据报转换成实际数据内容。
可以认为UDP协议的Socket类似于码头,数据报则类似于集装箱;码头的作用就是负责发送、接收集装箱,而DatagramSocket的作用则是发送、接收数据报。因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器端的概念。
UDP协议和TCP协议简单对比如下:
- TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
- UDP协议:不可靠,差错控制开销较小,传输大小限制在64KB以下,不需要建立连接。
使用DatagramSocket发送、接收数据
Java使用DatagramSocket代表UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报,Java使用DatagramPacket来代表数据报, DatagramSocket接收和发送的数据都是通过DatagramPacket对象完成的。
先看一下DatagramSocket的构造器:
- DatagramSocket():创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、本机所有可用端口中随机选择的某个端口。
- DatagramSocket(int prot):创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、指定端口。
- DatagramSocket(int port, InetAddress laddr):创建一个DatagramSocket实例,并将该对象绑定到指定IP地址、指定端口。
通过上面三个构造器中的任意一个构造器即可创建一个DatagramSocket实例,通常在创建服务器时,创建指定端口的DatagramSocket实例——这样保证其他客户端可以将数据发送到该服务器。一旦得到了DatagramSocket实例之后,就可以通过如下两个方法来接收和发送数据:
- receive(DatagramPacket p):从该DatagramSocket中接收数据报。
- send(DatagramPacket p):以该DatagramSocket对象向外发送数据报。
从上面两个方法可以看出,使用DatagramSocket发送数据报时,DatagramSocket并不知道将该数据报发送到哪里,而是由DatagramPacket自身决定数据报的目的地。就像码头并不知道每个集装箱的目的地,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。
下面看一下DatagramPacket的构造器:
- DatagramPacket(byte[] buf,int length):以一个空数组来创建DatagramPacket对象,该对象的作用是接收DatagramSocket中的数据。
- DatagramPacket(byte[] buf, int length, InetAddress addr, int port):以一个包含数据的数组来创建DatagramPacket对象,创建该DatagramPacket对象时还指定了IP地址和端口——这就决定了该数据报的目的地。
- DatagramPacket(byte[] buf, int offset, int length):以一个空数组来创建DatagramPacket对象,并指定接收到的数据放入buf数组中时从offset开始,最多放length个字节。
- DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):创建一个用于发送的DatagramPacket对象,指定发送buf数组中从offset开始,总共length个字节。
在接收数据之前,应该采用上面的第一个或第三个构造器生成一个DatagramPacket对象,给出接收数据的字节数组及其长度。然后调用DatagramSocket的receive()方法等待数据报的到来,receive()将一直等待(该方法会阻塞调用该方法的线程),直到收到一个数据报为止。如下代码所示:
// 创建一个接收数据的DatagramPacket对象
DatagramPacket packet=new DatagramPacket(buf, 256);
// 接收数据报
socket.receive(packet);
在发送数据之前,调用第二个或第四个构造器创建DatagramPacket对象,此时的字节数组里存放了想发送的数据。除此之外,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的send()方法实现的,send()方法根据数据报的目的地址来寻径以传送数据报。如下代码所示:
// 创建一个发送数据的DatagramPacket对象
DatagramPacket packet=new DatagramPacket(buf, length, address, port);
// 发送数据报
socket.send(packet);
当服务器端(也可以是客户端)接收到一个DatagramPacket对象后,如果想向该数据报的发送者“反馈”一些信息,但由于UDP协议是面向非连接的,所以接收者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket的如下3个方法来获取发送者的IP地址和端口:
- InetAddress getAddress():当程序准备发送此数据报时,该方法返回此数据报的目标机器的IP地址;当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的IP地址。
- int getPort():当程序准备发送此数据报时,该方法返回此数据报的目标机器的端口;当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的端口。
- SocketAddress getSocketAddress():当程序准备发送此数据报时,该方法返回此数据报的目标SocketAddress;当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的SocketAddress。
下面程序使用DatagramSocket实现了Server/Client结构的网络通信。本程序的服务器端使用循环1000次来读取DatagramSocket中的数据报,每当读取到内容之后便向该数据报的发送者送回一条信息。服务器端程序代码如下:
package Udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpServer {
public static final int PORT=30000;
// 定义每个数据报的最大大小为4KB
private static final int DATA_LEN=4096;
// 定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
// 以指定字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket;
// 定义一个字符串数组,服务器端发送该数组的元素
String[] books=new String[]{
"ffzs",
"sleepycat",
"泛泛之素",
"栏目"
};
public void init()throws IOException {
try(
// 创建DatagramSocket对象
DatagramSocket socket=new DatagramSocket(PORT)) {
// 采用循环接收数据
for (int i=0; i < 1000 ; i++ ) {
// 读取Socket中的数据,读到的数据放入inPacket封装的数组里
socket.receive(inPacket);
// 判断inPacket.getData()和inBuff是否是同一个数组
System.out.println(inBuff==inPacket.getData());
// 将接收到的内容转换成字符串后输出
System.out.println(new String(inBuff
, 0 , inPacket.getLength()));
// 从字符串数组中取出一个元素作为发送数据
byte[] sendData=books[i % 4].getBytes();
// 以指定的字节数组作为发送数据,以刚接收到的DatagramPacket的
// 源SocketAddress作为目标SocketAddress创建DatagramPacket
outPacket=new DatagramPacket(sendData, sendData.length , inPacket.getSocketAddress());
// 发送数据
socket.send(outPacket);
}
}
}
public static void main(String[] args)
throws IOException {
new UdpServer().init();
}
}
客户端程序代码也与此类似,客户端采用循环不断地读取用户键盘输入,每当读取到用户输入的内容后就将该内容封装成DatagramPacket数据报,再将该数据报发送出去;接着把DatagramSocket中的数据读入接收用的DatagramPacket中(实际上是读入该DatagramPacket所封装的字节数组中)。客户端程序代码如下:
package Udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Scanner;
public class UdpClient {
// 定义发送数据报的目的地
public static final int DEST_PORT=30000;
public static final String DEST_IP="127.0.0.1";
// 定义每个数据报的最大大小为4KB
private static final int DATA_LEN=4096;
// 定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
// 以指定的字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket=null;
public void init()throws IOException {
try(
// 创建一个客户端DatagramSocket,使用随机端口
DatagramSocket socket=new DatagramSocket()) {
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
// outPacket=new DatagramPacket(new byte[0] , 0, InetAddress.getByName(DEST_IP) , DEST_PORT);
// 创建键盘输入流
Scanner scan=new Scanner(System.in);
// 不断地读取键盘输入
while(scan.hasNextLine()) {
// 将键盘输入的一行字符串转换成字节数组
byte[] buff=scan.nextLine().getBytes();
// 设置发送用的DatagramPacket中的字节数据
outPacket.setData(buff);
//发送数据报
socket.send(outPacket);
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
socket.receive(inPacket);
System.out.println(new String(inBuff , 0
, inPacket.getLength()));
}
}
}
public static void main(String[] args) throws IOException {
new UdpClient().init();
}
}
使用DatagramSocket发送、接收DatagramPacket的关键代码,这些代码与服务器端代码基本相似。而客户端与服务器端的唯一区别在于:服务器端的IP地址、端口是固定的,所以客户端可以直接将该数据报发送给服务器端,而服务器端则需要根据接收到的数据报来决定“反馈”数据报的目的地。
读者可能会发现,使用DatagramSocket进行网络通信时,服务器端无须也无法保存每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立即退出。但不管客户端是否退出,服务器端都无法知道客户端的状态。
当使用UDP协议时,如果想让一个客户端发送的聊天信息被转发到其他所有的客户端则比较困难,可以考虑在服务器端使用Set集合来保存所有的客户端信息,每当接收到一个客户端的数据报之后,程序检查该数据报的源SocketAddress是否在Set集合中,如果不在就将该SocketAddress添加到该Set集合中。这样又涉及一个问题:可能有些客户端发送一个数据报之后永久性地退出了程序,但服务器端还将该客户端的SocketAddress保存在Set集合中……总之,这种方式需要处理的问题比较多,编程比较烦琐。幸好Java为UDP协议提供了MulticastSocket类,通过该类可以轻松地实现多点广播。
使用MulticastSocket实现多点广播
DatagramSocket只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播方式发送到多个客户端。
若要使用多点广播,则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。IP多点广播(或多点发送)实现了将单一信息发送到多个接收者的广播,其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。
IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255。
MulticastSocket类是实现多点广播的关键,当MulticastSocket把一个DatagramPacket发送到多点广播IP地址时,该数据报将被自动广播到加入该地址的所有Multicast Socket。MulticastSocket既可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。
MulticastSocket有点像DatagramSocket,事实上MulticastSocket是DatagramSocket的一个子类,也就是说,MulticastSocket是特殊的DatagramSocket。当要发送一个数据报时,可以使用随机端口创建MulticastSocket,也可以在指定端口创建MulticastSocket。MulticastSocket提供了如下3个构造器。
- public MulticastSocket():使用本机默认地址、随机端口来创建MulticastSocket对象。
- public MulticastSocket(int portNumber):使用本机默认地址、指定端口来创建MulticastSocket对象。
- public MulticastSocket(SocketAddress bindaddr):使用本机指定IP地址、指定端口来创建MulticastSocket对象。
创建MulticastSocket对象后,还需要将该MulticastSocket加入到指定的多点广播地址, MulticastSocket使用joinGroup()方法加入指定组;使用leaveGroup()方法脱离一个组。
- joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址。
- leaveGroup(InetAddress multicastAddr):让该MulticastSocket离开指定的多点广播地址。
在某些系统中,可能有多个网络接口。这可能会给多点广播带来问题,这时候程序需要在一个指定的网络接口上监听,通过调用setInterface()方法可以强制MulticastSocket使用指定的网络接口;也可以使用getInterface()方法查询MulticastSocket监听的网络接口。
MulticastSocket用于发送、接收数据报的方法与DatagramSocket完全一样。但MulticastSocket比DatagramSocket多了一个setTimeToLive(int ttl)方法,该ttl参数用于设置数据报最多可以跨过多少个网络,当ttl的值为0时,指定数据报应停留在本地主机;当ttl的值为1时,指定数据报发送到本地局域网;当ttl的值为32时,意味着只能发送到本站点的网络上;当ttl的值为64时,意味着数据报应保留在本地区;当ttl的值为128时,意味着数据报应保留在本大洲;当ttl的值为255时,意味着数据报可发送到所有地方;在默认情况下,该ttl的值为1。
使用MulticastSocket进行多点广播时所有的通信实体都是平等的,它们都将自己的数据报发送到多点广播IP地址,并使用MulticastSocket接收其他人发送的广播数据报。下面程序使用MulticastSocket实现了一个基于广播的多人聊天室。程序只需要一个MulticastSocket,两个线程,其中MulticastSocket既用于发送,也用于接收;一个线程负责接收用户键盘输入,并向Multicast Socket发送数据,另一个线程则负责从MulticastSocket中读取数据。
package Mudp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Scanner;
// 让该类实现Runnable接口,该类的实例可作为线程的target
public class MulticastSocketTest implements Runnable {
// 使用常量作为本程序的多点广播IP地址
private static final String BROADCAST_IP
="230.0.0.1";
// 使用常量作为本程序的多点广播目的地端口
public static final int BROADCAST_PORT=30000;
// 定义每个数据报的最大大小为4KB
private static final int DATA_LEN=4096;
// 定义本程序的MulticastSocket实例
private MulticastSocket socket=null;
private InetAddress broadcastAddress=null;
private Scanner scan=null;
// 定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
// 以指定字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket=null;
public void init()throws IOException {
try(
// 创建键盘输入流
Scanner scan=new Scanner(System.in)) {
// 创建用于发送、接收数据的MulticastSocket对象
// 由于该MulticastSocket对象需要接收数据,所以有指定端口
socket=new MulticastSocket(BROADCAST_PORT);
broadcastAddress=InetAddress.getByName(BROADCAST_IP);
// 将该socket加入指定的多点广播地址
socket.joinGroup(broadcastAddress);
// 设置本MulticastSocket发送的数据报会被回送到自身
socket.setLoopbackMode(false);
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket=new DatagramPacket(new byte[0]
, 0 , broadcastAddress , BROADCAST_PORT);
// 启动以本实例的run()方法作为线程执行体的线程
new Thread(this).start();
// 不断地读取键盘输入
while(scan.hasNextLine()) {
// 将键盘输入的一行字符串转换成字节数组
byte[] buff=scan.nextLine().getBytes();
// 设置发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
// 发送数据报
socket.send(outPacket);
}
}
finally {
socket.close();
}
}
public void run() {
try {
while(true) {
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里
socket.receive(inPacket);
// 打印输出从socket中读取的内容
System.out.println("聊天信息:" + new String(inBuff
, 0 , inPacket.getLength()));
}
}
// 捕获异常
catch (IOException ex) {
ex.printStackTrace();
try {
if (socket !=null) {
// 让该Socket离开该多点IP广播地址
socket.leaveGroup(broadcastAddress);
// 关闭该Socket对象
socket.close();
}
System.exit(1);
}
catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new MulticastSocketTest().init();
}
}
上面程序中init()方法里代码先创建了一个MulticastSocket对象,由于需要使用该对象接收数据报,所以为该Socket对象设置使用固定端口;将该Socket对象添加到指定的多点广播IP地址;第三行粗体字代码设置该Socket发送的数据报会被回送到自身(即该Socket可以接收到自己发送的数据报)。至于程序中使用MulticastSocket发送、接收数据报的代码,与使用DatagramSocket并没有区别,故此处不再赘述。
下面将结合MulticastSocket和DatagramSocket开发一个简单的局域网即时通信工具,局域网内每个用户启动该工具后,就可以看到该局域网内所有的在线用户,该用户也会被其他用户看到。
该程序的实现思路是,每个用户都启动两个Socket,即一个MulticastSocket,一个DatagramSocket。其中MulticastSocket会周期性地向230.0.0.1发送在线信息,且所有用户的MulticastSocket都会加入到230.0.0.1这个多点广播IP地址中,这样每个用户都可以收到其他用户广播的在线信息,如果系统经过一段时间没有收到某个用户广播的在线信息,则从用户列表中删除该用户。除此之外,该MulticastSocket还用于向所有用户发送广播信息。
DatagramSocket主要用于发送私聊信息,当用户收到其他用户广播来的DatagramPacket时,即可获取该用户MulticastSocket对应的SocketAddress,这个SocketAddress将作为发送私聊信息的重要依据——本程序让MulticastSocket在30000端口监听,而DatagramSocket在30001端口监听,这样程序就可以根据其他用户广播来的DatagramPacket得到他的DatagramSocket所在的地址。
本系统提供了一个UserInfo类,该类封装了用户名、图标、对应的SocketAddress以及该用户对应的交谈窗口、失去联系的次数等信息。该类的代码片段如下:
import java.net.*;
public class UserInfo
{
// 该用户的图标
private String icon;
// 该用户的名字
private String name;
// 该用户的MulitcastSocket所在的IP地址和端口
private SocketAddress address;
// 该用户失去联系的次数
private int lost;
// 该用户对应的交谈窗口
private ChatFrame chatFrame;
public UserInfo(){}
// 有参数的构造器
public UserInfo(String icon , String name , SocketAddress address , int lost)
{
this.icon = icon;
this.name = name;
this.address = address;
this.lost = lost;
}
public void setIcon(String icon)
{
this.icon = icon;
}
public String getIcon()
{
return this.icon;
}
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void setAddress(SocketAddress address)
{
this.address = address;
}
public SocketAddress getAddress()
{
return this.address;
}
public void setLost(int lost)
{
this.lost = lost;
}
public int getLost()
{
return this.lost;
}
public void setChatFrame(ChatFrame chatFrame)
{
this.chatFrame = chatFrame;
}
public ChatFrame getChatFrame()
{
return this.chatFrame;
}
public int hashCode()
{
return address.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null &&
obj.getClass() == UserInfo.class)
{
return ((UserInfo)obj).getAddress().equals(address);
}
return false;
}
}
通过UserInfo类的封装,所有客户端只需要维护该UserInfo类的列表,程序就可以实现广播、发送私聊信息等功能。本程序底层通信的工具类则需要一个MulticastSocket和一个DatagramSocket,该工具类的代码如下:
import java.awt.Component;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketAddress;
import java.util.ArrayList;
import javax.swing.JOptionPane;
public class ComUtil {
private static final String BROADCAST_IP = "230.0.0.1";
public static final int BROADCAST_PORT = 30000;
private static final int DATA_LEN = 4096;
private MulticastSocket socket = null;
private DatagramSocket singleSocket = null;
private InetAddress broadcastAddress = null;
byte[] inBuff = new byte[4096];
private DatagramPacket inPacket;
private DatagramPacket outPacket;
private LanChat lanTalk;
public ComUtil(LanChat var1) throws IOException, InterruptedException {
this.inPacket = new DatagramPacket(this.inBuff, this.inBuff.length);
this.outPacket = null;
this.lanTalk = var1;
this.socket = new MulticastSocket(30000);
this.singleSocket = new DatagramSocket(30001);
this.broadcastAddress = InetAddress.getByName("230.0.0.1");
this.socket.joinGroup(this.broadcastAddress);
this.socket.setLoopbackMode(false);
this.outPacket = new DatagramPacket(new byte[0], 0, this.broadcastAddress, 30000);
(new ComUtil.ReadBroad()).start();
Thread.sleep(1L);
(new ComUtil.ReadSingle()).start();
}
public void broadCast(String var1) {
try {
byte[] var2 = var1.getBytes();
this.outPacket.setData(var2);
this.socket.send(this.outPacket);
} catch (IOException var3) {
var3.printStackTrace();
if (this.socket != null) {
this.socket.close();
}
JOptionPane.showMessageDialog((Component)null, "发送信息异常,请确认30000端口空闲,且网络连接正常!", "网络异常", 0);
System.exit(1);
}
}
public void sendSingle(String var1, SocketAddress var2) {
try {
byte[] var3 = var1.getBytes();
DatagramPacket var4 = new DatagramPacket(var3, var3.length, var2);
this.singleSocket.send(var4);
} catch (IOException var5) {
var5.printStackTrace();
if (this.singleSocket != null) {
this.singleSocket.close();
}
JOptionPane.showMessageDialog((Component)null, "发送信息异常,请确认30001端口空闲,且网络连接正常!", "网络异常", 0);
System.exit(1);
}
}
class ReadBroad extends Thread {
ReadBroad() {
}
public void run() {
while(true) {
try {
ComUtil.this.socket.receive(ComUtil.this.inPacket);
String var1 = new String(ComUtil.this.inBuff, 0, ComUtil.this.inPacket.getLength());
if (var1.startsWith("⊿⊿") && var1.endsWith("⊿⊿")) {
String var2 = var1.substring(2, var1.length() - 2);
String[] var3 = var2.split("▓");
UserInfo var4 = new UserInfo(var3[1], var3[0], ComUtil.this.inPacket.getSocketAddress(), 0);
boolean var5 = true;
ArrayList var6 = new ArrayList();
int var7;
for(var7 = 1; var7 < ComUtil.this.lanTalk.getUserNum(); ++var7) {
UserInfo var8 = ComUtil.this.lanTalk.getUser(var7);
var8.setLost(var8.getLost() + 1);
if (var8.equals(var4)) {
var8.setLost(0);
var5 = false;
}
if (var8.getLost() > 2) {
var6.add(var7);
}
}
for(var7 = 0; var7 < var6.size(); ++var7) {
ComUtil.this.lanTalk.removeUser((Integer)var6.get(var7));
}
if (var5) {
ComUtil.this.lanTalk.addUser(var4);
}
} else {
ComUtil.this.lanTalk.processMsg(ComUtil.this.inPacket, false);
}
} catch (IOException var9) {
var9.printStackTrace();
if (ComUtil.this.socket != null) {
ComUtil.this.socket.close();
}
JOptionPane.showMessageDialog((Component)null, "接收信息异常,请确认30000端口空闲,且网络连接正常!", "网络异常", 0);
System.exit(1);
}
}
}
}
class ReadSingle extends Thread {
byte[] singleBuff = new byte[4096];
private DatagramPacket singlePacket;
ReadSingle() {
this.singlePacket = new DatagramPacket(this.singleBuff, this.singleBuff.length);
}
public void run() {
while(true) {
try {
ComUtil.this.singleSocket.receive(this.singlePacket);
ComUtil.this.lanTalk.processMsg(this.singlePacket, true);
} catch (IOException var2) {
var2.printStackTrace();
if (ComUtil.this.singleSocket != null) {
ComUtil.this.singleSocket.close();
}
JOptionPane.showMessageDialog((Component)null, "接收信息异常,请确认30001端口空闲,且网络连接正常!", "网络异常", 0);
System.exit(1);
}
}
}
}
}
该类主要实现底层的网络通信功能,在该类中提供了一个broadCast()方法,该方法使用Multicast Socket将指定字符串广播到所有客户端;还提供了sendSingle()方法,该方法使用DatagramSocket将指定字符串发送到指定SocketAddress,如程序中前两行粗体字代码所示。除此之外,该类还提供了两个内部线程类:ReadSingle和ReadBroad,这两个线程类采用循环不断地读取DatagramSocket和Multicast Socket中的数据,如果读到的信息是广播来的在线信息,则保持该用户在线;如果读到的是用户的聊天信息,则直接将该信息显示出来。
在该类中用到了本程序的一个主类:LanTalk,该类使用DefaultListModel来维护用户列表,该类里的每个列表项就是一个UserInfo。该类还提供了一个ImageCellRenderer,该类用于将列表项绘制出用户图标和用户名字:
import java.util.*;
import java.awt.*;
import java.net.*;
import java.text.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class LanTalk extends JFrame {
private DefaultListModel<UserInfo> listModel
=new DefaultListModel<>();
// 定义一个JList对象
private JList<UserInfo> friendsList=new JList<>(listModel);
// 定义一个用于格式化日期的格式器
private DateFormat formatter=DateFormat.getDateTimeInstance();
public LanTalk() {
super("局域网聊天");
// 设置该JList使用ImageCellRenderer作为单元格绘制器
friendsList.setCellRenderer(new ImageCellRenderer());
listModel.addElement(new UserInfo("all" , "所有人"
, null , -2000));
friendsList.addMouseListener(new ChangeMusicListener());
add(new JScrollPane(friendsList));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(2, 2, 160 , 600);
}
// 根据地址来查询用户
public UserInfo getUserBySocketAddress(SocketAddress address) {
for (int i=1 ; i < getUserNum() ; i++) {
UserInfo user=getUser(i);
if (user.getAddress() !=null
&& user.getAddress().equals(address)) {
return user;
}
}
return null;
}
// ------下面四个方法是对ListModel的包装------
// 向用户列表中添加用户
public void addUser(UserInfo user){
listModel.addElement(user);
}
// 从用户列表中删除用户
public void removeUser(int pos) {
listModel.removeElementAt(pos);
}
// 获取该聊天窗口的用户数量
public int getUserNum(){
return listModel.size();
}
// 获取指定位置的用户
public UserInfo getUser(int pos){
return listModel.elementAt(pos);
}
// 实现JList上的鼠标双击事件监听器
class ChangeMusicListener extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
// 如果鼠标的击键次数大于2
if (e.getClickCount() >=2) {
// 取出鼠标双击时选中的列表项
UserInfo user=(UserInfo)friendsList.getSelectedValue();
// 如果该列表项对应用户的交谈窗口为null
if (user.getChatFrame()==null) {
// 为该用户创建一个交谈窗口,并让该用户引用该窗口
user.setChatFrame(new ChatFrame(null , user));
}
// 如果该用户的窗口没有显示,则让该用户的窗口显示出来
if (!user.getChatFrame().isShowing()) {
user.getChatFrame().setVisible(true);
}
}
}
}
/**
* 处理网络数据报,该方法将根据聊天信息得到聊天者
* 并将信息显示在聊天对话框中
* @param packet 需要处理的数据报
* @param single 该信息是否为私聊信息
*/
public void processMsg(DatagramPacket packet , boolean single) {
// 获取发送该数据报的SocketAddress
InetSocketAddress srcAddress=(InetSocketAddress)
packet.getSocketAddress();
// 如果是私聊信息,则该Packet获取的是DatagramSocket的地址
// 将端口号减1才是对应的MulticastSocket的地址
if (single) {
srcAddress=new InetSocketAddress(srcAddress.getHostName()
, srcAddress.getPort() - 1);
}
UserInfo srcUser=getUserBySocketAddress(srcAddress);
if (srcUser !=null) {
// 确定消息将要显示到哪个用户对应的窗口中
UserInfo alertUser=single ? srcUser : getUser(0);
// 如果该用户对应的窗口为空,则显示该窗口
if (alertUser.getChatFrame()==null) {
alertUser.setChatFrame(new ChatFrame(null , alertUser));
}
// 定义添加的提示信息
String tipMsg=single ? "对您说:" : "对大家说:";
// 显示提示信息
alertUser.getChatFrame().addString(srcUser.getName()
+ tipMsg + "......................("
+ formatter.format(new Date()) + ")\n"
+ new String(packet.getData() , 0 , packet.getLength())
+ "\n");
if (!alertUser.getChatFrame().isShowing()) {
alertUser.getChatFrame().setVisible(true);
}
}
}
// 主方法,程序的入口
public static void main(String[] args) {
LanTalk lanTalk=new LanTalk();
new LoginFrame(lanTalk , "请输入用户名、头像后登录");
}
}
// 定义用于改变JList列表项外观的类
class ImageCellRenderer extends JPanel
implements ListCellRenderer<UserInfo> {
private ImageIcon icon;
private String name;
// 定义绘制单元格时的背景色
private Color background;
// 定义绘制单元格时的前景色
private Color foreground;
@Override
public Component getListCellRendererComponent(JList list
, UserInfo userInfo , int index
, boolean isSelected , boolean cellHasFocus) {
// 设置图标
icon=new ImageIcon("ico/" + userInfo.getIcon() + ".gif");
name=userInfo.getName();
// 设置背景色、前景色
background=isSelected ? list.getSelectionBackground()
: list.getBackground();
foreground=isSelected ? list.getSelectionForeground()
: list.getForeground();
// 返回该JPanel对象作为单元格绘制器
return this;
}
// 重写paintComponent方法,改变JPanel的外观
public void paintComponent(Graphics g) {
int imageWidth=icon.getImage().getWidth(null);
int imageHeight=icon.getImage().getHeight(null);
g.setColor(background);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(foreground);
// 绘制好友图标
g.drawImage(icon.getImage() , getWidth() / 2 - imageWidth / 2
, 10 , null);
g.setFont(new Font("SansSerif" , Font.BOLD , 18));
// 绘制好友用户名
g.drawString(name, getWidth() / 2 - name.length() * 10
, imageHeight + 30 );
}
// 通过该方法来设置该ImageCellRenderer的最佳大小
public Dimension getPreferredSize() {
return new Dimension(60, 80);
}
}
上面类中提供的addUser()和removeUser()方法暴露给通信类ComUtil使用,用于向用户列表中添加、删除用户。除此之外,该类还提供了一个processMsg()方法,该方法用于处理网络中读取的数据报,将数据报中的内容取出,并显示在特定的窗口中。
YeekuProtocol类:
public interface YeekuProtocol {
String PRESENCE = "⊿⊿";
String SPLITTER = "▓";
}
ChatFrame类:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.InetSocketAddress;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class ChatFrame extends JDialog {
JTextArea msgArea = new JTextArea(12, 45);
JTextField chatField = new JTextField(30);
JButton sendBn = new JButton("发送");
UserInfo user;
public ChatFrame(LanChat var1, UserInfo var2) {
super(var1, "和" + var2.getName() + "聊天中", false);
this.user = var2;
this.msgArea.setEditable(false);
this.add(new JScrollPane(this.msgArea));
JPanel var3 = new JPanel();
var3.add(new JLabel("输入信息:"));
var3.add(this.chatField);
var3.add(this.sendBn);
this.add(var3, "South");
ChatFrame.MyActionListener var4 = new ChatFrame.MyActionListener();
this.chatField.addActionListener(var4);
this.sendBn.addActionListener(var4);
this.pack();
}
public void addString(String var1) {
this.msgArea.setText(var1 + "\n" + this.msgArea.getText());
}
class MyActionListener implements ActionListener {
MyActionListener() {
}
public void actionPerformed(ActionEvent var1) {
InetSocketAddress var2 = (InetSocketAddress)ChatFrame.this.user.getAddress();
if (var2 == null) {
LoginFrame.comUtil.broadCast(ChatFrame.this.chatField.getText());
ChatFrame.this.msgArea.setText("您对大家说:" + ChatFrame.this.chatField.getText() + "\n" + ChatFrame.this.msgArea.getText());
} else {
var2 = new InetSocketAddress(var2.getHostName(), var2.getPort() + 1);
LoginFrame.comUtil.sendSingle(ChatFrame.this.chatField.getText(), var2);
ChatFrame.this.msgArea.setText("您对" + ChatFrame.this.user.getName() + "说:" + ChatFrame.this.chatField.getText() + "\n" + ChatFrame.this.msgArea.getText());
}
ChatFrame.this.chatField.setText("");
}
}
}
LoginFrame类:
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;
public class LoginFrame extends JDialog {
public JLabel tip;
public JTextField userField = new JTextField("李刚", 20);
public JComboBox iconList = new JComboBox(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
private JButton loginBn = new JButton("登陆");
private LanChat chatFrame;
public static ComUtil comUtil;
public LoginFrame(LanChat var1, String var2) {
super(var1, "输入名字后登陆", true);
this.chatFrame = var1;
this.setLayout(new GridLayout(5, 1));
JPanel var3 = new JPanel();
this.tip = new JLabel(var2);
this.tip.setFont(new Font("Serif", 1, 16));
var3.add(this.tip);
this.add(var3);
this.add(this.getPanel("用户名", this.userField));
this.iconList.setPreferredSize(new Dimension(224, 20));
this.add(this.getPanel("图 标", this.iconList));
JPanel var4 = new JPanel();
this.loginBn.addActionListener(new LoginFrame.MyActionListener(this));
var4.add(this.loginBn);
this.add(var4);
this.pack();
this.setVisible(true);
}
private JPanel getPanel(String var1, JComponent var2) {
JPanel var3 = new JPanel();
var3.add(new JLabel(var1 + ":"));
var3.add(var2);
return var3;
}
public void setTipMsg(String var1) {
this.tip.setText(var1);
}
class MyActionListener implements ActionListener {
private LoginFrame loginFrame;
public MyActionListener(LoginFrame var2) {
this.loginFrame = var2;
}
public void actionPerformed(ActionEvent var1) {
try {
LoginFrame.comUtil = new ComUtil(LoginFrame.this.chatFrame);
final String var2 = "⊿⊿" + LoginFrame.this.userField.getText() + "▓" + LoginFrame.this.iconList.getSelectedObjects()[0] + "⊿⊿";
LoginFrame.comUtil.broadCast(var2);
Timer var3 = new Timer(10000, new ActionListener() {
public void actionPerformed(ActionEvent var1) {
LoginFrame.comUtil.broadCast(var2);
}
});
var3.start();
this.loginFrame.setVisible(false);
LoginFrame.this.chatFrame.setVisible(true);
} catch (Exception var4) {
this.loginFrame.setTipMsg("确认30001端口空闲,且网络正常!");
}
}
}
}