旧机宝需要在一个局域网内(同一wifi下)实现设备的发现和通信功能。对于设备的发现,包括主动去搜寻别的设备以及主动声明自身的存在。对于通信则要求通信的句柄和设备的基本信息存储起来以供下次使用。实现的原理非常简单,创建socketserver,并通过UDP告诉其他设备该socketserver的存在,其他设备则通过连接该socketserver来维持通信链路。
1、设备发现
在所有设备都在同一网络并打开旧机宝的情况下,通过MulticastSocket向指定ip地址发送信息来声明自身存在,使用同一个MulticastSocket接收报文来发现其他设备。
private var multicastSocket: MulticastSocket = MulticastSocket(Constants.Port.udp_broadcast)
multicastSocket.joinGroup(inetAddress)
multicastSocket.loopbackMode = false
multicastSocket需要在应用中指定固定的端口号,并加入到指定的ip地址,以使不同的设备“加入到一个广播群组”中。
fun sayHiToAll(){
Thread(Runnable {
if(TCPManager.port!=0){
val builder= MyMessage.DeviceInfo.newBuilder()
if(TextUtils.isEmpty(DataHelper.androidId))return@Runnable
builder.androidId=DataHelper.androidId
builder.ip=Utils.getIPAddress(MyApplication.context) ?: return@Runnable
builder.connectedSize=TCPManager.connecteds.size
builder.port=TCPManager.port
builder.phoneBrand= Build.BRAND
builder.phoneModel=Build.MODEL
outDatagramPacket.data = MyMessage.Message.newBuilder().setDeviceInfo( builder.build()).setType(MessageType.UDP_HELLO).build().toByteArray()
try {
multicastSocket.send(outDatagramPacket)
Log.i(TAG_DEVICE,"发送UDP信息:\n${builder.build()}")
}catch (e:Exception){
}
}
}).start()
}
通过该multicastSocket将本机的一些基本信息(TCP的ip地址、端口等)发送到广播组里,从而通知到每一个加入进来的设备,实现对自身的声明。
fun startServer() {
Log.i(TAG_DEVICE,"开启UDP服务")
Thread(Runnable {
while (true) {
val inData = ByteArray(256)
val datagramPacket = DatagramPacket(inData, inData.size)
multicastSocket.receive(datagramPacket)
val content=ByteArray(datagramPacket.length)
System.arraycopy(inData,0,content,0,datagramPacket.length)
val myMessage=MyMessage.Message.parseFrom(content)
if(myMessage!=null){
if(myMessage.deviceInfo.androidId==DataHelper.androidId)continue
Log.i(TAG_DEVICE,"接收到UDP信息:\n$myMessage")
if(myMessage.type==MessageType.UDP_HELLO){
if(DataHelper.androidId > myMessage.deviceInfo.androidId) {
TCPManager.connect(myMessage)
} else {
sayHiToAll()
continue
}
if(TCPManager.connecteds.size<myMessage.deviceInfo.connectedSize) sayHiToAll()
}else{
EventBus.getDefault().post(myMessage)
}
}
}
}).start()
}
当实现对同一个multicastSocket的UDP消息监听的时候,应用就具备了接收其他设备声明自己的消息的能力,消息里有对方的ip地址和端口等有用信息。值得注意的是,我们也可以通过增加一些逻辑来实现:当接收到指定的UDP信息,就立刻将本机信息通过UDP发送到广播组。以此来主动要求在网设备暴露自己。
2、设备通信
通过设备发现(定时、用户触发等),我们已经可以获知当前网络有多少旧机宝设备(加入到特定ip地址的multicastSocket),现在可以创建稳定的通信连接了。其实在上面UDP信息发送之前,设备就已经创建了socketserver了,该服务的ip地址和端口通过UDP消息发出,从而让接收到UDP消息的设备,通过ip信息来和socketsever建立网络连接。这里有一个小插曲:同一网络下的多个设备,如果任意的两台设备之间,同时向对方发送UDP信息来要求对方连接自己,则有可能在两个设备之间创建两条通信线路,这明显多余并且不利于之后的使用。解决的方式有很多种,旧机宝采用比较androidid大小的方式,由androidid小的设备来连接androidid大的设备,这样就算两台设备之间同时发给对方UDP消息,他们之间被连接的“服务端”身份和发起连接的“客户端”身份也是指定的,不会同时连接对方。
if(DataHelper.androidId > myMessage.deviceInfo.androidId) {
TCPManager.connect(myMessage)
}
fun startServer() {
Log.i(Constants.LogTag.TAG_DEVICE,"开启TCP服务")
Thread(Runnable {
val serverSocket=ServerSocket(0)
port=serverSocket.localPort
while (true){
val socket=serverSocket.accept()
socket.keepAlive=true
val inputStream=socket.getInputStream()
val lengthBytes = ByteArray(4)
if (inputStream.read(lengthBytes) != 4) return@Runnable
val length = bytes2Int(lengthBytes)
val content = ByteArray(length)
if(inputStream.read(content) <= 0)return@Runnable
val socketMessage=MyMessage.Message.parseFrom(content)
if(socketMessage!=null && socketMessage.type==MessageType.TCP_HELLO){
addConnected(socketMessage.deviceInfo.androidId)
val phoneDevice=PhoneDevice()
phoneDevice.deviceInfo=socketMessage.deviceInfo
phoneDevice.functionId=socketMessage.dataLong.toInt()
phoneDevice.socket=socket
DeviceManager.addDevice(phoneDevice)
sayTCPHi(socket)
}
}
}).start()
}
首先启动的socketserver服务会一直等待别人的连接,期间UDP消息正在大肆宣扬该socketserver的存在。
fun connect(message: MyMessage.Message) {
val deviceInfo=message.deviceInfo
if(TextUtils.isEmpty(deviceInfo.androidId))return
if(isConnecting(deviceInfo.androidId) || isConnected(deviceInfo.androidId))return
addConnecting(deviceInfo.androidId)
Log.i(Constants.LogTag.TAG_DEVICE,"开始连接:\n$deviceInfo")
val socket: Socket?
try {
socket=Socket(deviceInfo.ip,deviceInfo.port)
}finally {
removeConnecting(deviceInfo.androidId)
}
if (socket==null)return
socket.keepAlive=true
sayTCPHi(socket)
val inputStream=socket.getInputStream()
val lengthBytes = ByteArray(4)
if (inputStream.read(lengthBytes) != 4) return
val length = bytes2Int(lengthBytes)
val content = ByteArray(length)
if(inputStream.read(content) <= 0)return
val socketMessage=MyMessage.Message.parseFrom(content)
if(socketMessage!=null && socketMessage.type==MessageType.TCP_HELLO){
addConnected(socketMessage.deviceInfo.androidId)
val phoneDevice=PhoneDevice()
phoneDevice.deviceInfo=socketMessage.deviceInfo
phoneDevice.functionId=socketMessage.dataLong.toInt()
phoneDevice.socket=socket
DeviceManager.addDevice(phoneDevice)
}
// val phoneDevice=PhoneDevice()
// phoneDevice.deviceInfo=deviceInfo
// phoneDevice.socket=socket
// DeviceManager.addDevice(phoneDevice)
//
// sayTCPHi(socket)
}
接收到UDP信息的设备连接该socketserver,使得两台设备都拿到对应的socket,对该socket保存留作后用,至此设备的通信功能实现了。