在路上

 找回密码
 立即注册
在路上 站点首页 学习 查看内容

Java网络编程(3):使用 UDP 探测局域网内特定类型的机器

2016-12-20 13:12| 发布者: zhangjf| 查看: 448| 评论: 0

摘要: 记得以前我们使用类似“快牙”这些文件分享工具的时候,一开始就是先在 手机A 上创建一个“房间”,然后连接上 手机A WiFi 热点的其他手机(即这些手机处于一个局域网内)就可以发现到这个房间并加入到这个房间里面 ...

记得以前我们使用类似“快牙”这些文件分享工具的时候,一开始就是先在 手机A 上创建一个“房间”,然后连接上 手机A WiFi 热点的其他手机(即这些手机处于一个局域网内)就可以发现到这个房间并加入到这个房间里面,然后就可以互相分享文件了。那没有建立连接的情况下,“发现房间”这个功能是怎么实现的呢?
首先,既然 手机A 处于局域网中,那么根据 手机A 当前在局域网的 IP 地址和子网掩码,就可以获得这个局域网内所有机器的 IP 地址 的范围。如果在没有建立连接的情况下,手机A 就可以给这个范围内的每个 IP 地址都发送一个消息 —— 那么如果某个 IP 地址的机器(设为 手机B)会对这个消息做出回应,便说明 手机B手机A 的“自己人”,那么 手机A 便可以告诉 手机B 它在当前的局域网建了一个“房间”,房间号是个啥,然后 手机B 可以选择是否加入到这个“房间”。

在Java网络编程(1)中,我们已经知道可以使用 NetworkInterface 来获得机器在局域网内 IP 地址;

在Java网络编程(2)中,我们知道使用 UDP,便可以在不建立连接的情况下,直接向某个 IP 地址发送消息;

如果每次都是遍历这个局域网内所有的 IP 地址,并使用 UDP 向每个 IP 发送消息,那样就有点麻烦了。事实上,我们可以使用广播。每个局域网都有一个对应的广播地址,向广播地址发送的数据包通过网关设备(比如路由器)时,网关设备会向局域网的每台设备发送一份该数据包的副本。通过 IP 和子网掩码计算广播地址的方法简单的形容就是 (IP地址)|(~子网掩码)—— 将子网掩码按位取反再和IP地址进行或运算,比如当前机器在局域网内的地址为 192.168.1.3,子网掩码为 255.255.255.0(取反后为 0.0.0.255),那么广播地址为 192.168.1.255。广播也是在不建立连接的情况下就发送数据,所以广播不能通过 TCP 实现,只能是 UDP。在 Java 中,通过 UDP 进行广播和单播(即只向一个 IP 地址发送数据包)的程序几乎没有区别,只是地址由一个特定的单播地址(如 192.168.1.3)变为了其对应的广播地址(192.168.1.255)。

现在让我们来实现下面的功能:
1、Broadcaster 创建一个房间,并每隔 1 秒向局域网广播一个特定的消息;
2、同一个局域网的 Device 如果收到了 3 次这个特定的消息,之后便向 Broadcaster 发送加入房间的消息;
3、Broadcaster 收到 Device 请求加入房间的消息后,将 Device 加入房间。

首先定义发送者类和接收者类,他们都实现了 Runnable,分别可以用来发送和接收:

Sender.java

  1. import java.io.IOException;
  2. import java.net.*;
  3. public class Sender implements Runnable {
  4. private static final byte[] EMPTY_DATA = new byte[0];
  5. private final DatagramSocket socket;
  6. private final SocketAddress broadcastAddress;
  7. private final long sendingInterval; // unit is ms
  8. public Sender(DatagramSocket socket,
  9. SocketAddress broadcastAddress, int sendingInterval) {
  10. this.socket = socket;
  11. this.broadcastAddress = broadcastAddress;
  12. this.sendingInterval = sendingInterval;
  13. }
  14. @Override
  15. public void run() {
  16. while (true) {
  17. byte[] data = getNextData();
  18. if (data == null || data.length == 0) {
  19. break;
  20. }
  21. DatagramPacket outPacket = new DatagramPacket(
  22. data, data.length, broadcastAddress);
  23. try {
  24. socket.send(outPacket);
  25. System.out.println("Sender: Data has been sent");
  26. Thread.sleep(sendingInterval);
  27. } catch (IOException | InterruptedException ex) {
  28. System.err.println("Sender: Error occurred while sending packet");
  29. break;
  30. }
  31. }
  32. System.out.println("Sender: Thread is end");
  33. }
  34. /**
  35. * 获得下一次发送的数据<br>
  36. * 子类需要重写这个方法,返回下一次要发送的数据
  37. *
  38. * @return 下一次发送的数据
  39. */
  40. public byte[] getNextData() {
  41. return EMPTY_DATA;
  42. }
  43. }
复制代码

Receiver.java

  1. import java.io.IOException;
  2. import java.net.*;
  3. public class Receiver implements Runnable {
  4. private final int BUF_SIZE = 512;
  5. private final DatagramSocket socket;
  6. public Receiver(DatagramSocket socket) {
  7. this.socket = socket;
  8. }
  9. @Override
  10. public void run() {
  11. byte[] inData = new byte[BUF_SIZE];
  12. DatagramPacket inPacket = new DatagramPacket(inData, inData.length);
  13. while (true) {
  14. try {
  15. socket.receive(inPacket);
  16. if (!handlePacket(inPacket)) {
  17. break;
  18. }
  19. } catch (IOException ex) {
  20. System.out.println("Receiver: Socket was closed.");
  21. break;
  22. }
  23. }
  24. System.out.println("Receiver: Thread is end");
  25. }
  26. /**
  27. * 处理接收到的数据报<br>
  28. * 子类需要重写这个方法,处理接收到的数据包,并返回是否继续接收
  29. *
  30. * @param packet 接收到的数据报
  31. * @return 是否需要继续接收
  32. */
  33. public boolean handlePacket(DatagramPacket packet) {
  34. return false;
  35. }
  36. }
复制代码

然后我们定义 Device 和 Broadcaster:

Device.java

  1. import java.io.IOException;
  2. import java.net.*;
  3. public class Device {
  4. private static final int DEFAULT_LISTENING_PORT = 10000;
  5. private final InetAddress address;
  6. private final int port;
  7. private DatagramSocket socket;
  8. public Device(int port) throws IOException {
  9. this.port = port;
  10. this.address = InetAddress.getLocalHost();
  11. }
  12. public Device(InetAddress address, int port) {
  13. this.address = address;
  14. this.port = port;
  15. }
  16. public void start() throws SocketException, InterruptedException {
  17. System.out.println("Device has been started...");
  18. InetAddress lanAddr = LANAddressTool.getLANAddressOnWindows();
  19. if (lanAddr != null) {
  20. System.out.println("Device: LAN Address: " + lanAddr.getHostAddress());
  21. }
  22. socket = new DatagramSocket(port);
  23. Receiver receiver = new Receiver(socket) {
  24. int recvCount = 0;
  25. @Override
  26. public boolean handlePacket(DatagramPacket packet) {
  27. String recvMsg = new String(packet.getData(), 0, packet.getLength());
  28. if ("ROOM".equals(recvMsg)) {
  29. System.out.printf("Device: Received msg '%s'n", recvMsg);
  30. recvCount++;
  31. if (recvCount == 3) {
  32. byte[] data = "JOIN".getBytes();
  33. DatagramPacket respMsg = new DatagramPacket(
  34. data, data.length, packet.getSocketAddress()); // 此时 packet 包含了发送者地址和监听端口
  35. try {
  36. socket.send(respMsg);
  37. System.out.println("Device: Sent response 'JOIN'");
  38. } catch (IOException ex) {
  39. ex.printStackTrace(System.err);
  40. }
  41. return false; // 停止接收
  42. }
  43. }
  44. return true;
  45. }
  46. };
  47. Thread deviceThread = new Thread(receiver);
  48. deviceThread.start(); // 启动接收数据包的线程
  49. deviceThread.join();
  50. close();
  51. System.out.println("Device has been closed.");
  52. }
  53. public void close() {
  54. if (socket != null) {
  55. socket.close();
  56. }
  57. }
  58. @Override
  59. public String toString() {
  60. return "Device {" + "address=" + address + ", port=" + port + '}';
  61. }
  62. public static void main(String[] args) throws Exception {
  63. Device device = new Device(DEFAULT_LISTENING_PORT);
  64. device.start();
  65. }
  66. }
复制代码

Broadcaster.java

  1. import java.net.*;
  2. public class Broadcaster {
  3. private static final int DEFAULT_BROADCAST_PORT = 10000;
  4. private final InetAddress bcAddr;
  5. private final int bcPort;
  6. private DatagramSocket socket;
  7. public Broadcaster(InetAddress broadcastAddress, int broadcastPort) {
  8. this.bcAddr = broadcastAddress;
  9. this.bcPort = broadcastPort;
  10. }
  11. public void start() throws SocketException, InterruptedException {
  12. System.out.println("Broadcaster has been started...");
  13. final Room room = new Room("Test");
  14. System.out.printf("Broadcaster: Created room '%s'nn", room.getName());
  15. socket = new DatagramSocket();
  16. SocketAddress bcSocketAddr = new InetSocketAddress(bcAddr, bcPort);
  17. Sender sender = new Sender(socket, bcSocketAddr, 1000) {// 每隔 1000ms 广播一次
  18. final byte[] DATA = "ROOM".getBytes();
  19. @Override
  20. public byte[] getNextData() {
  21. return DATA;
  22. }
  23. };
  24. Receiver recver = new Receiver(socket) {
  25. @Override
  26. public boolean handlePacket(DatagramPacket packet) {
  27. String recvMsg = new String(packet.getData(), 0, packet.getLength());
  28. if ("JOIN".equals(recvMsg)) {
  29. Device device = new Device(packet.getAddress(), packet.getPort());
  30. room.addDevice(device);
  31. room.listDevices();
  32. }
  33. return true; // 一直接收
  34. }
  35. };
  36. Thread senderThread = new Thread(sender);
  37. Thread recverThread = new Thread(recver);
  38. senderThread.start(); // 启动发送(广播)数据包的线程
  39. recverThread.start(); // 启动接收数据包的线程
  40. senderThread.join();
  41. recverThread.join();
  42. close();
  43. }
  44. public void close() {
  45. if (socket != null) {
  46. socket.close();
  47. }
  48. }
  49. public static void main(String[] args) throws Exception {
  50. InetAddress bcAddr = LANAddressTool.getLANBroadcastAddressOnWindows();
  51. if (bcAddr != null) {
  52. System.out.println("Broadcast Address: " + bcAddr.getHostAddress());
  53. Broadcaster broadcaster = new Broadcaster(bcAddr, DEFAULT_BROADCAST_PORT);
  54. broadcaster.start();
  55. } else {
  56. System.out.println("Please check your LAN~");
  57. }
  58. }
  59. }
复制代码

Room.java

  1. import java.util.*;
  2. public class Room {
  3. private final String name;
  4. private final List<Device> devices;
  5. public Room(String name) {
  6. this.name = name;
  7. this.devices = new ArrayList<>();
  8. }
  9. public boolean addDevice(Device device) {
  10. return devices.add(device);
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void listDevices() {
  16. System.out.printf("Room (%s), current devices:n", name);
  17. for (Device device : devices) {
  18. System.out.println(device);
  19. }
  20. }
  21. }
复制代码

(完整的 Demo 可以访问:https://github.com/mizhoux/LA...)

我们将这个 Demo 打包成 jar,然后开始运行:
1、首先我们在本机上启动 Broadcaster:

2、我们将本机作为一个 Device 启动:

可以看到此时 Broadcaster 创建的房间已经有了一个 Device:

3、我们启动局域网内的另外一台设备:

此时 Broadcaster 创建的房间便有两个 Device:

4、再启动局域网内的一台设备:

此时房间里则有三个 Device:

因为 UDP 在不需要建立连接的基础上就可以发送消息,所以它可以方便的用来探测局域网内特定类型的机器 —— 这是个很有用的功能 —— 又比如一个集群当中可能会突然有机器宕机,为了检测这一事件的发生,就需要集群 master机器 每隔一定的时间向每台机器发送若干心跳检测包,如果有回复说明机器正常,否则说明该机器出现了故障,此时不需要连接而且高效的 UDP 就十分适合这种场合。当然,我们始终还是要考虑到 UDP 是不可靠的协议,它并不能代替 TCP —— 永远需要根据环境,来选择最合适的技术。

最新评论

小黑屋|在路上 ( 蜀ICP备15035742号-1 

;

GMT+8, 2025-7-7 21:08

Copyright 2015-2025 djqfx

返回顶部