Android下实现局域网设备发现与通信
在使用Android开发智能设备时,一般会分为用于遥控与管理的Host端,和用于执行个性功能的Slave端,二者可以借助网络或蓝牙等途径实现通信。
局域网设备发现
如果是借助网络通信,那就必须知道对方的ip地址,而常见的网络环境中ip地址一般是通过DHCP服务动态分配的,所以事先无法确定对方的ip地址。为了确定对方的地址,可以通过向局域网内发送查找设备的广播,收到广播的Slave端就知道了Host端的ip地址,在向Host端发送应答包之后,双方就都知道了对方的ip地址。
局域网设备通信
在Host端与Slave端互相知道ip地址后,就可以实现局域网通信了。局域网通信一般通过TCP或UDP实现,TCP的优势在于它的可靠性,通过TCP传送的数据不丢失、无差错、不重复且按序到达,但只支持一对一,效率比UDP低;UDP的优势在于效率比TCP高,支持一对一、一对多、多对一、多对多,但不保证可靠性,网络差的环境下可能丢包和顺序错乱。因此如果对可靠性要求较高,建议使用TCP,如果对实时性要求较高则可以使用UDP。
代码实现设备发现
编写用于搜索局域网中设备DeviceSearcher,向局域网发送特定格式的广播包,并接收应答包,从而发现局域网中的己方设备。
/**
* 用于搜索局域网中的设备
*/
public class DeviceSearcher {
private static ExecutorService executorService = Executors.newSingleThreadExecutor();
private static Handler uiHandler = new Handler(Looper.getMainLooper());
/**
* 开始搜索
* @param onSearchListener
*/
public static void search(OnSearchListener onSearchListener){
executorService.execute(new SearchRunnable(onSearchListener));
}
public static interface OnSearchListener{
void onSearchStart();
void onSearchedNewOne(Device device);
void onSearchFinish();
}
private static class SearchRunnable implements Runnable {
OnSearchListener searchListener;
public SearchRunnable(OnSearchListener listener){
this.searchListener = listener;
}
@Override
public void run() {
try {
if(searchListener!=null){
uiHandler.post(new Runnable() {
@Override
public void run() {
searchListener.onSearchStart();
}
});
}
DatagramSocket socket = new DatagramSocket();
//设置接收等待时长
socket.setSoTimeout(RemoteConst.RECEIVE_TIME_OUT);
byte[] sendData = new byte[1024];
byte[] receData = new byte[1024];
DatagramPacket recePack = new DatagramPacket(receData, receData.length);
//使用广播形式(目标地址设为255.255.255.255)的udp数据包
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName("255.255.255.255"), RemoteConst.DEVICE_SEARCH_PORT);
//用于存放已经应答的设备
HashMap<String, Device> devices = new HashMap<>();
//搜索指定次数
for(int i=0;i<RemoteConst.SEARCH_DEVICE_TIMES;i++){
sendPacket.setData(packSearchData(i+1));
//发送udp数据包
socket.send(sendPacket);
try {
//限定搜索设备的最大数量
int rspCount = RemoteConst.SEARCH_DEVICE_MAX;
while (rspCount > 0) {
socket.receive(recePack);
final Device device = parseRespData(recePack);
if(devices.get(device.getIp())==null){
//保存新应答的设备
devices.put(device.getIp(), device);
if(searchListener!=null){
uiHandler.post(new Runnable() {
@Override
public void run() {
searchListener.onSearchedNewOne(device);
}
});
}
}
rspCount --;
}
} catch (SocketTimeoutException e) {
e.printStackTrace();
}
}
socket.close();
if(searchListener!=null){
uiHandler.post(new Runnable() {
@Override
public void run() {
searchListener.onSearchFinish();
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 校验和解析应答的数据包
* @param pack udp数据包
* @return
*/
private Device parseRespData(DatagramPacket pack) {
if (pack.getLength() < 2) {
return null;
}
byte[] data = pack.getData();
int offset = pack.getOffset();
//检验数据包格式是否符合要求
if (data[offset++] != RemoteConst.PACKET_PREFIX || data[offset++] != RemoteConst.PACKET_TYPE_SEARCH_DEVICE_RSP) {
return null;
}
int length = data[offset++];
String uuid = new String(data, offset, length);
return new Device(pack.getAddress().getHostAddress(), pack.getPort(), uuid);
}
/**
* 生成搜索数据包
* 格式:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
* @param seq
* @return
*/
private byte[] packSearchData(int seq) {
byte[] data = new byte[6];
int offset = 0;
data[offset++] = RemoteConst.PACKET_PREFIX;
data[offset++] = RemoteConst.PACKET_TYPE_SEARCH_DEVICE_REQ;
data[offset++] = (byte) seq;
data[offset++] = (byte) (seq >> 8);
data[offset++] = (byte) (seq >> 16);
data[offset++] = (byte) (seq >> 24);
return data;
}
}
}
编写用于响应搜索广播的DeviceSearchResponser,监听局域网中的广播包,在收到符合要求的广播包后发送应答包,并带上自己的标识。
/**
* 用于响应局域网设备搜索
*/
public class DeviceSearchResponser {
private static SearchRespThread searchRespThread;
/**
* 启动响应线程,收到设备搜索命令后,自动响应
*/
public static void open() {
if (searchRespThread == null) {
searchRespThread = new SearchRespThread();
searchRespThread.start();
}
}
/**
* 停止响应
*/
public static void close() {
if (searchRespThread != null) {
searchRespThread.destory();
searchRespThread = null;
}
}
private static class SearchRespThread extends Thread {
DatagramSocket socket;
volatile boolean openFlag;
public void destory() {
if (socket != null) {
socket.close();
socket = null;
}
openFlag = false;
}
@Override
public void run() {
try {
//指定接收数据包的端口
socket = new DatagramSocket(RemoteConst.DEVICE_SEARCH_PORT);
byte[] buf = new byte[1024];
DatagramPacket recePacket = new DatagramPacket(buf, buf.length);
openFlag = true;
while (openFlag) {
socket.receive(recePacket);
//校验数据包是否是搜索包
if (verifySearchData(recePacket)) {
//发送搜索应答包
byte[] sendData = packSearchRespData();
DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, recePacket.getSocketAddress());
socket.send(sendPack);
}
}
} catch (IOException e) {
destory();
}
}
/**
* 生成搜索应答数据
* 协议:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
* @return
*/
private byte[] packSearchRespData() {
byte[] data = new byte[1024];
int offset = 0;
data[offset++] = RemoteConst.PACKET_PREFIX;
data[offset++] = RemoteConst.PACKET_TYPE_SEARCH_DEVICE_RSP;
// 添加UUID数据
byte[] uuid = getUuidData();
data[offset++] = (byte) uuid.length;
System.arraycopy(uuid, 0, data, offset, uuid.length);
offset += uuid.length;
byte[] retVal = new byte[offset];
System.arraycopy(data, 0, retVal, 0, offset);
return retVal;
}
/**
* 校验搜索数据是否符合协议规范
* 协议:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
*/
private boolean verifySearchData(DatagramPacket pack) {
if (pack.getLength() < 6) {
return false;
}
byte[] data = pack.getData();
int offset = pack.getOffset();
int sendSeq;
if (data[offset++] != '$' || data[offset++] != RemoteConst.PACKET_TYPE_SEARCH_DEVICE_REQ) {
return false;
}
sendSeq = data[offset++] & 0xFF;
sendSeq |= (data[offset++] << 8) & 0xFF00;
sendSeq |= (data[offset++] << 16) & 0xFF0000;
sendSeq |= (data[offset++] << 24) & 0xFF000000;
if (sendSeq < 1 || sendSeq > RemoteConst.SEARCH_DEVICE_TIMES) {
return false;
}
return true;
}
/**
* 获取设备uuid
* @return
*/
private byte[] getUuidData() {
return (Build.PRODUCT + Build.ID).getBytes();
}
}
}
代码实现设备通信
编写用于向Slave端发送命令的CommandSender
/**
* 用于发送命令
* Created by gw on 2017/11/4.
*/
public class CommandSender {
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new SendCommandThreadFactory(), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RejectedExecutionException();
}
});
public static void addCommand(final Command command){
addTask(new CommandRunnable(command));
}
private static void addTask(CommandRunnable runnable){
try{
threadPool.execute(runnable);
}catch (RejectedExecutionException e){
e.printStackTrace();
if(runnable.command.getCallback()!=null){
runnable.command.getCallback().onError("command is rejected");
}
}
}
private static class CommandRunnable implements Runnable{
Command command;
public CommandRunnable(Command command){
this.command = command;
}
@Override
public void run() {
Socket socket = new Socket();
try {
socket.connect(new InetSocketAddress(command.getDestIp(), RemoteConst.COMMAND_RECEIVE_PORT));
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024*8];
//发送命令内容
os.write(command.getContent().getBytes());
os.write(CommunicationKey.EOF.getBytes());
if(command.getCallback()!=null){
command.getCallback().onRequest(command.getContent());
}
//读取应答内容
int i=0;
while (true) {
buffer[i] = (byte) is.read();
if(buffer[i] == -1){
if(command.getCallback()!=null){
command.getCallback().onError("get response failed");
}
break;
}
if((char)buffer[i] != CommunicationKey.EOF.charAt(0)){
i++;
}else{
String response = new String(buffer, 0, i+1, Charset.defaultCharset()).replace(CommunicationKey.EOF, "");
if (response.startsWith(CommunicationKey.RESPONSE_OK)) {
if(command.getCallback()!=null){
command.getCallback().onSuccess(response.replace(CommunicationKey.RESPONSE_OK, ""));
}
break;
}else if (response.startsWith(CommunicationKey.RESPONSE_ECHO)) {
if(command.getCallback()!=null){
command.getCallback().onEcho(response.replace(CommunicationKey.RESPONSE_ECHO, ""));
}
break;
}else if (response.startsWith(CommunicationKey.RESPONSE_ERROR)) {
if(command.getCallback()!=null){
command.getCallback().onError(response.replace(CommunicationKey.RESPONSE_ERROR, ""));
}
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
if(command.getCallback()!=null){
command.getCallback().onError(e.getMessage());
}
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
编写用于接受Host端命令并回写应答的CommandReceiver
/**
* 用于接收命令和回写应答
* Created by gw on 2017/11/6.
*/
public class CommandReceiver {
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(7, 8, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ReceiveCommandThreadFactory(), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RejectedExecutionException();
}
});
private static CommandListener listener;
private static volatile boolean isOpen;
public static void open(CommandListener commandListener){
listener = commandListener;
isOpen = true;
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(RemoteConst.COMMAND_RECEIVE_PORT);
while(isOpen){
Socket socket = serverSocket.accept();
threadPool.execute(new CommandParseRunnable(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public static void close(){
isOpen = false;
threadPool.shutdown();
}
public static class CommandParseRunnable implements Runnable{
Socket socket;
public CommandParseRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
DataInputStream is = new DataInputStream(socket.getInputStream());
OutputStream os = socket.getOutputStream();
byte[] bytes = new byte[1024*8];
int i=0;
while(true){
bytes[i] = (byte) is.read();
if (bytes[i] == -1) {
break;
}
if((char)bytes[i] != CommunicationKey.EOF.charAt(0)){
i++;
}else{
String command = new String(bytes, 0, i+1, Charset.defaultCharset()).replace(CommunicationKey.EOF, "");
if(listener!=null){
os.write((listener.onReceive(command)).getBytes());
}else{
os.write((CommunicationKey.RESPONSE_OK+ CommunicationKey.EOF).getBytes());
}
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public interface CommandListener{
String onReceive(String command);
}
}