1. 组播——多点广播:
1) 可以看到如果使用DatagramSocket很那实现广播的功能,至少需要用一个Set集合来保存用户发来的数据报(或是取出其中的字段加以保存),但是有一个严重的问题是UDP并不记录客户端的状态,上一秒发送了数据报下一秒也许客户端就关闭socket了,这些事情服务器端无法知晓(因为UDP是无连接),如果是TCP一旦客户端断开了连接那么服务器端的socket就会抛出异常(基于连接的);
2) IP协议对组播的支持:
i. 组播就是多点广播的俗称,即一群机器组成一个组,里面任意一个人发送的消息可以同时广播到其它所有机器上(当然也可以包括自己,如果自己也收到则叫做回环);
ii. 之前我们用TCP编写过一个群聊的应用,其实也是一种另类的组播,那时我们将所有客户端的信息都保存在服务器上,广播和私聊都是通过服务器实现的;
iii. 但是这种在程序中我们自己保存客户端信息的方式非常繁琐,有没有方法允许不记录组内机器的信息而自动实现广播呢?答案是肯定的,IP协议本身就支持这种功能;
iv. IP地址支持一种特殊的IP地址,叫做多点广播地址,简称广播IP,想要参与组播的所有机器可以“加入”这个地址,任意一台机器只要向该地址发送信息,那么该信息就会自动广播给所有“加入”到该地址的机器上,也就是说这个神秘的广播IP地址仿佛是一个虚拟的服务器,它履行着我们之前TCP群聊的服务器的功能;
v. 只不过IP协议底层确实是用服务器的方法实现广播IP的功能的;
2. 那么在Java中如何调用IP协议的多点广播功能呢?
1) 首先IP协议规定多点广播地址的范围是224.0.0.0 ~ 239.255.255.255,协议软件底层设定好的,如果超出这个返回就会抛出异常;
2) 其次是参与组播的所有主机的socket必须是MulticastSocket类的对象,它是DatagramSocket的子类,因此组播socket也是UDP协议的一种!!
3) 也就是说单点(一对一)UDP通信就用DatagramSocket,组播群体通信就得使用MulticastSocket,如果程序中同时需要这两种通信方式,那么就得同时用这两种socket,但这两种socket不能共享一个端口,必须分别指定,否则会抛出异常;
!!这是IP协议的规定,组播端口和单点端口必须不同!!
!!这是因为单点和组播无法在同一个端口上监听(receive)!!
4) MulticastSocket的构造器和DatagramSocket以及其它所有Socket大同小异,无非都是绑定本机的IP和端口罢了;
5) 要想加入组播就必须先让自己的socket“加入”到目标广播IP中,直接调用MulticastSocket对象的joinGroup方法加入到该IP地址中即可(该方法的参数就是广播IP地址);
6) 接下来的发送和接受信息就和DatagramSocket一模一样,都是定义一下DatagramPacket,其余完全一样,只不过发送的消息会被广播到组内所有机器,接受的消息时其它机器的广播消息罢了;
!!发送数据报时的目的地端口和MulticastSocket绑定的端口一样!!其实在IP协议底层,MulticastSocket(成员socket)既是客户端也是服务器端(广播IP);
3. 具体方法:
1) MulticastSocket构造器:
i. MulticastSocket(); // 使用默认IP和端口绑定
ii. MulticastSocket(int port); // 使用默认IP和指定端口
iii. MulticastSocket(SocketAddress addr); // 同时指定
2) 加入和退出一个组播IP:
i. void joinGroup(InetAddress mcastaddr);
ii. void leaveGroup(InetAddress mcastaddr); // 可以看到很繁琐,退出时还要指定组播IP而不能自动(无参),可以看到所有UDP协议的方法还是显得非常底层
3) 关闭回环开启广播功能:接下来一定要这样调用才能开启广播功能,只要记住这点就行了!
!!sock.setLoopbackMode(false);!!
4) 剩下的发送接收数据报就和DatagramSocket一模一样了,此时只要将MulticastSocket完全当成DatagramSocket使用就行了;
4. 示例:
public class Test { private final static String BC_IP = "230.0.0.1"; // 组播地址 private final static int BC_PORT = 30000; // 组播端口 private final static int PACK_SIZE = 4096; public void init() throws IOException { MulticastSocket sock = new MulticastSocket(BC_PORT); InetAddress bcAddr = InetAddress.getByName(BC_IP); try (Scanner scan = new Scanner(System.in)) { // 创建socket并加入组播地址 sock.joinGroup(bcAddr); sock.setLoopbackMode(false); // 必须是false才能开启广播功能!! new Thread(() -> { // 接受广播消息的线程 try { DatagramPacket inpack = new DatagramPacket(new byte[PACK_SIZE], PACK_SIZE); while (true) { sock.receive(inpack); System.out.println("广播消息:" + new String(inpack.getData(), 0, inpack.getLength())); } } catch (IOException e) { e.printStackTrace(); if (sock != null) { try { sock.leaveGroup(bcAddr); } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } sock.close(); } System.exit(1); } }).start(); // 主线程接受控制台输入并广播出去 DatagramPacket outpack = new DatagramPacket(new byte[0], 0, bcAddr, BC_PORT); // 目的端口和MulticastSocket端口一样!! while (scan.hasNextLine()) { byte[] buf = scan.nextLine().getBytes(); outpack.setData(buf); sock.send(outpack); } } finally { // 最终关闭程序之前一定要关闭socket sock.close(); } } public static void main(String[] args) throws NumberFormatException, IOException { // TODO Auto-generated method stub new Test().init(); } }!UDP没有C/S一说,因此该程序即使客户端也是服务器端,多开几个就能相互通信了;