Android设备在局域网内找寻服务器IP

1,745 阅读5分钟

准备

本文会以局域网内Android找到部署NodeJs服务器的IP为目标来实现案例。 对于长时间不太关注网络的这块的小伙伴们,很多基础概念可能容易记不大清了,这一节就简单给大家介绍一下博客中会用到相关网络概念,为本文做铺垫。

一次网络请求

拿我们最熟悉的HTTP协议来说,通过HTTP协议发送一个请求到服务器,请求中包含的有协议要求的源地址目的地址、**报文实体(数据)**和其他,一般通过TCP发送给指定的服务器IP,服务器通过监听某个端口,发现了我们发送的请求并给出响应,这样一次请求就完成了。 一次HTTP请求在四层的TCP/IP协议簇里面路线如下图所示。

一次http请求.jpg

DHCP

Dynamic Host Configuration Protocol,动态主机配置协议,这是一个应用层协议。这个协议会给所有接入局域网的设备一个有期限的不冲突的IP。也就是说,在局域网内设备的IP是动态的。IP是网络层IP协议规定的,当你没有目标设备的IP或者是说没有目的地址的时候,是无法发送请求的。

注:动态分配IP只是DHCP分配IP机制的一种,不过大部分路由器默认的是这种方式。本文解决的也是这种机制带来的问题。

单播、组播、广播

设备之间的通讯机制有三种:单播、组播和广播。这些通信方式的划分是网络层IP协议定义的。在IP协议中规定了源IP地址和目的IP地址,源地址只能有一个,目的地址可以有一个或多个。根据目的地址的个数可以划分为俩类:目的地址唯一的单播;可以有多个目的地址的组播和广播。单播很简单,接下来说说组播和广播。

组播

组播地址

IP协议为组播留的IP地址:224.0.0.0~239.255.255.255,这段地址里面也有不同的划分,不过在局域网里面无所谓了,有兴趣的小伙伴可以查一下。

组播特性

  • 只有加入特定组播组的成员才能收到组播数据;
  • 源地址仅需发送一份数据;
  • 从源地址到目的地址的任何一条路中都是只有一份数据在传送。

组播.jpg 图中还原了数据是如何通过组播从源地址到目的地址的。在图中能发现俩个组播传送数据的明显特征:**一是源地址只会发送一条数据,后面的数据全是路由器复制和转发实现的;二是从源地址到目的地址的任何一条路中都是只有一份数据在传送。**这俩点特征让组播在“一对多”的场景中,对网络资源有着很高的利用率。拷贝转发或是寻址加入都是组播的相关协议实现的。刚好每个平台都会有对应的库来实现。 当有设备退出组播组,源地址发出的数据也就不会向它那边发送了。

广播

说到广播首先得提一个概念:广播域。 广播域即为广播可以传播的区域。一个路由器链接的设备就是一个广播域,但是路由器不会转发广播,也就是说广播发不出局域网范围,也发不到链接局域网的另一台路由器的局域网中。 广播的传送数据的方式是广播域内任意设备往广播地址发送一个数据包,整个广播域内其他设备都会接收到这个数据包。很方便是不是?没错,就是很简单粗暴。 不过广播有些缺点:

  • 每个设备接收到数据后都要耗费资源区处理数据,发现目的地址不是自己就丢弃这个包,这样会无端浪费非目的地址的资源;
  • 在IPv6中,广播这种传输方式会被取消!!!

方案v1.0——广播实现

广播相较于组播,好处是很简单,很容易理清其中的编程思路。 用广播实现的思路是服务器发送一条“我是服务器”的广播报文,Android监听自身与服务器约定好的端口,当发现数据中有“我是服务器”的报文时,解析出该报文的源IP即为服务器IP。具体实现如下: SendBroad.js(NodeJs)

const dgram = require("dgram");
const server = dgram.createSocket("udp4");

const message = Buffer.from("this is nodejs server", "ascii");
server.bind(function () {
  server.setBroadcast(true);
});
const interval = setInterval(() => {
  if (i> 5) {
    console.log("结束广播");
    server.close()
    clearInterval(interval);
  }else{
    server.send(message, 4000, "255.255.255.255");
    console.log(`广播数据No.${i++}`);
  }
}, 1000,i=0);

dgram 模块提供了 UDP 数据包 socket 的实现。一般各个平台对于dgram这类库都称之为socket编程库(网络编程库),顾名思义是提供了对网络编程的库,一般分为TCP网络编程和UDP网络编程。还记得最开始的TCP/IP四层架构图吗,网络编程库的作用就是为最上层应用层提供第二层传输层的API。 开始说说代码

const dgram = require("dgram");
const server = dgram.createSocket("udp4");

首先创建了一个udp的socket实例;

const message = Buffer.from("this is nodejs server", "ascii");

然后我们准备了一段与客户端约定好的「暗号」,将其转成了二进制,这样才能在网络中传输;

server.bind(function () {
  server.setBroadcast(true);
});

设置成广播;

const interval = setInterval(() => {
  if (i> 5) {
    ...
    clearInterval(interval);
  }
}, 1000,i=0);

每隔1s做点什么,5次后关闭这个计时器。

server.send(message, 4000, "255.255.255.255");

向广播地址"255.255.255.255"的4000端口,发送一条准备好的数据。该广播域每个设备监听4000都会收到这条数据。

这时在一个路由下的设备都能接收到这个报文了,下面是Android设备监听4000端口,并解析报文源地址的实现。 MainActivity.kt(Android)

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        receiverBroadcast()
    }

    fun receiverBroadcast() {
        Thread(Runnable {
            val buffer = ByteArray(25)
            val packet = DatagramPacket(buffer, buffer.size)
            val socket = DatagramSocket(4000)
            socket.soTimeout = 5000
            while (true) {
                try {
                    socket.receive(packet) // 阻塞接收
                    val msg = String(packet.data)
                    if (packet.data != null && msg.contains("this is nodejs server")) {
                        val ip = packet.address.hostAddress
                        Log.i(tag, "receiverBroadcast>>>>>>>>获取到服务器ip=${ip},msg=${msg}")
                        break
                    }
                } catch (e: SocketTimeoutException) {  // 接收超时异常
                    Log.i(tag, "startReceiver>>>>>>>> timeout")
                    break
                }
            }
        }).start()
    }

因为监听网络端口是要阻塞住线程的,所以开了个线程放具体实现。

val buffer = ByteArray(25)
val packet = DatagramPacket(buffer, buffer.size)

初始化一个报文包,并给一个25字节的字节数组,用来接收报文。

val socket = DatagramSocket(4000)
socket.soTimeout = 5000

监听自身4000端口,设置超时时间5s,启动阻塞监听后5s,将抛出异常。

socket.receive(packet)

启动阻塞监听,监听到数据将放入数据包packet中。

val msg = String(packet.data)
if (packet.data != null && msg.contains("this is nodejs server")) {
	val ip = packet.address.hostAddress
}

若能走到下一步,说明该端口收到了数据,再来判断一下,数据中有没有和服务器约定好的「暗号」,有的话就能直接取到该条数据的源地址了,即我们想要的服务器地址。若没有,继续监听。

整个流程是不是很简单,我们再来梳理一遍:

  • 服务器发送广播报文;
  • 客户机监听对应端口;
  • 发现目标字段,即可解析报文,拿到IP。

方案V2.0——组播实现

方案1对于我的问题来说是一个解决方案,不过只能算是够用。相信大家在看了第一节的组播和广播的介绍后,一定会认为组播在本文提出的问题中更合适。我也是这样认为的,那么我们来看一下怎么实现的吧。 组播的重点是组播中的设备在发送和接收数据之前需要加入组播组,相关API在各平台的网络编程库中都有提供。除了在发送和接收报文之前需要先进入组播组以外,组播的使用和广播几乎一样。 SendBroad.js(NodeJs)

const dgram = require("dgram");
const server = dgram.createSocket("udp4");

const message = Buffer.from("this is nodejs server", "ascii");

server.bind(1234, function () {
  server.addMembership("224.0.0.114");
});

const interval = setInterval(
  () => {
    if (i > 5) {
      console.log("结束组播");
      server.close();
      clearInterval(interval);
    } else {
      server.send(message, 1234, "224.0.0.114");
      console.log(`组播数据No.${i++}`);
    }
  },1000,(i = 0));

与广播代码的区别在这,首先得加入组播组。

server.bind(1234, function () {
  server.addMembership("224.0.0.114");
});

再向组播地址发送报文就可以。 Android客户端这边也差不多,也得先加入组播组,再能接收信息。

fun receiverMulticast(){
        Thread(Runnable {
            val buffer = ByteArray(25)
            val packet = DatagramPacket(buffer, buffer.size)
            val address= Inet4Address.getByName("224.0.0.114")
            val socket = MulticastSocket(1234)
            socket.joinGroup(address)
            socket.soTimeout = 50000
            while (true) {
                try {
                    socket.receive(packet) // 阻塞接收
                    val msg = String(packet.data)
                    if (packet.data != null && msg.contains("this is nodejs server")) {
                        val ip = packet.address.hostAddress
                        Log.i(tag, "receiverMulticast>>>>>>>>获取到服务器ip=${ip},msg=${msg}")
                        break
                    }
                } catch (e: SocketTimeoutException) {  // 接收超时异常
                    Log.i(tag, "receiverMulticast>>>>>>>> timeout")
                    break
                }
            }
        }).start()
    }

差异是这三行代码:

val address= Inet4Address.getByName("224.0.0.114")  // 组播组地址
val socket = MulticastSocket(1234) // 配置监听端口
socket.joinGroup(address) // 加入组播组

拓展——通用网络发现协议

在应用层以下,服务器也就是一个设备,现阶段有很多的设备发现协议,例如mDNS、Bonjour、dns-sd、DDPS等,不过这些协议都有个特定都是基于组播实现的。

mDNS

Android系统中自带的服务,有注册、发现和解析的功能。

Bonjour

iOS系统的,Android的mDNS来源于此。

SSDP协议

上面的协议在跨平台的时候很头疼,可能找不到完备的库,需要自己实现。SSDP协议协议源自于UPnP(即插即用)技术协议族(例如电视投屏用到的),资料还是挺多的。