Android/安卓开发之WIFI通讯(上)--搜索区域网内所有设备
2017-08-02 10:59
691 查看
我的上一篇文章写的是WIFI的基本应用。基本连上WIFI之后,设备就能联网了,直接用常规的联网方式即可访问互联网(例如okhttp、httpurlconnection等)。
但是我们写个wifi应用不可能是用来联网这么简单哈。设想一下,假如我们连接wifi,但是这个wifi没有连接互联网,我们能用他来干嘛?这就涉及到区域网的应用了。
这不需要连接互联网,只要在大家连接在同一个区域网内的wifi就可以通讯,目前类似应用场景已经很多了。例如各种软件的面对面快传、各种游戏的区域网联机、还有目前常用的智能家居的家具管理、基于wifi的视频监控等等。
这里主要是针对android端,当然移植到其他平台上也可以,毕竟有了算法思路。这里打算模拟连接上wifi之后和同在一个区域网的其他用户进行聊天、传输文件等。
要想和同一个区域网内的其他客户端进行通讯,首先要做的就是找到它们啊!所以本文讲的是如何搜索同一区域网内的所有设备。
----要怎么才能做到找到区域网内的所有设备呢?
天马行空1:区域网内建立一个固定ip的服务器,一旦有新的设备连接,就给服务器发送自己的ip和端口存储起来。下线时服务器再把它移除。然后需要搜索区域网内的设备时,只需要查询服务器就行了。
条件:服务器ip不能被占用,需要一个服务器!而且感觉在移动设备上,这个方案不行啊。
天马行空2:类似天马行空1,不需要在本地建立服务器,而是在云端建立数据表...
条件:我在云端查询都需要联网了,你还跟我说不连接互联网也能通讯?
天马行空3:我一个一个ping一下...0-255多试几次嘛
条件:搜索时间那么长,看到我直接卸载这个破应用了
天马行空4:你当面告诉我ip和端口,或者生成二维码给我扫一扫(类似qq快传)嘛,然后在建立链接
条件:我有一句mmp不知道要不要讲...
有没有一种方法既不用建立服务器也不用链接互联网,更不用输入ip的方式?
那必须的啊,那就是用广播!我发一个广播,你们收到了就给我回应,这不是简单方便嘛。那怎么发广播呢?这里就涉及到UDP的应用了。关于socket通讯和udp的内容我就不多说了,还那句话不怎么了解的话先看看其他博客或者百度。
---还有一句话:我只是个Android小白,出错或者算法不够好在所难免,请大家多多指教---
思路:
搜索端:
1.建立一个UDP广播,并广播出去
2.建立接收端口,收到回应再给对方发一条确认消息(当然以后是用来约定TCP的传输端口的,这样就能进行通讯了)
2.1.接收端口循环接收,除非用户中断
响应端:
1.建立响应端口,收到广播之后响应消息
1.1循环等待响应,除非用户中断
2.如果收到的是确认消息,则根据约定的规则建立TCP连接(这个文章主要时搜索设备,所以这个没写)
同时,因为是运行在android上,我的要求是要做到 我可以搜索你也可以响应你,反之你可以搜索我也可以响应我。
如果只是一个搜索端,其他都只能是响应端,那不是c/s模型,只有一个能搜索了,这显然不太符合实际要求,所以其实每个设备都有一个响应端和一个搜索端。
这样的话到时候建立TCP连接时,会出现一个问题:到底是由谁来建立服务端?这个在之后在进行处理。
有了思路就开始撸代码了--记住本文只是做到能搜索区域网内的所有设备!
搜索端线程:
响应端代码:
这样就完成了一个简单搜索区域网中的所有用户了,实践证明这个方法高效可用。在同一个wifi可以用,我在公司中测试,不同wifi下也可以用(因为公司网络本身就是区域网)。
其他也没啥需要解释的,看看代码就理解过程了。这里说明几点:
1.两个类里面都有一个Handler对象和sendMsg()的方法,这里是为了在android中工作线程和Ui线程进行通讯。如果实在其他平台上可以删除掉。
2.搜索端和响应端的发送和搜索端口刚好是对调,因为不能在同一个程序的创建同一个端口的套接字
3.这个例子中响应的消息只是为了方便测试所以以:分割键值对,实际使用大家可以根据自己的协议发送json数据,例如约定通讯时谁当服务端,连接端口是什么等。
还有关于关闭线程的,我们来讨论一下:
在这里例子中,没有设置超时时间,完全是阻塞式接收,那么关闭线程的权利也留给了用户。但是这里我只是 用一个标志位来控制会有什么问题呢?
首先,receive()方法是阻塞的,就算我改变了标志位可是线程依然没结束啊,一直等待一个数据包才回进行下一轮循环。所以我的解决方法是自己给自己发个结束的数据包。
然后,有的人说不可以直接强制结束线程么?调用线程的结束方法。那么在java中线程确实有个stop()的方法,但是不推荐使用,具体原因官方有解释。能用的方法是使用interrupt()方法抛出一个异常退出,奇怪的是我用了没作用,不知问题出在什么地方,希望有人可以测试一下告诉我答案..
那么,能不能直接关闭xxxsocket.close()套接字呢?似乎可以吧!这个我是试过了,但是结果还是让你们自己去试试吧...
最后,线程是能结束了,可是在android中,你新建了这个线程对象调用了.start(),然后在调用.stop(),最后想重新开启直接再次调用.start()行不行呢?
答案是不行!会抛出java.lang.IllegalThreadStateException: Thread already started,即使你的run方法逻辑已经执行完了,但是你还是用不能直接再次start()必须重新new 一个对象。这样的话,我设置的回调又要重新设置了!
后来我想到一个方法,用户停止之后,我不是退出循环,而是进入一个休眠状态,当用户再次开启时在唤醒继续工作。这样应该就能解决了。如果大家有什么更好的方法,欢迎分享出来讨论讨论。
简单测试代码:
界面就是四个按钮,然后下方显示日志。
运行截图,懒得一步一步截图,大家按照下面的顺序对照图片看看就行了,实在看不明白自己运行试试咯:
1.设备1上线:
2.设备2搜索
3.设备2自己也上线,再次搜索
4.设备1也发起搜索:
5.两个设备都停止搜索并且下线
截图的日志有点混乱,主要是假如本机也上线,搜索也是能搜索出本机的,这个本机屏蔽的逻辑还没做。
设备1:
设备2:
本文到此结束,谢谢大家。
但是我们写个wifi应用不可能是用来联网这么简单哈。设想一下,假如我们连接wifi,但是这个wifi没有连接互联网,我们能用他来干嘛?这就涉及到区域网的应用了。
这不需要连接互联网,只要在大家连接在同一个区域网内的wifi就可以通讯,目前类似应用场景已经很多了。例如各种软件的面对面快传、各种游戏的区域网联机、还有目前常用的智能家居的家具管理、基于wifi的视频监控等等。
这里主要是针对android端,当然移植到其他平台上也可以,毕竟有了算法思路。这里打算模拟连接上wifi之后和同在一个区域网的其他用户进行聊天、传输文件等。
要想和同一个区域网内的其他客户端进行通讯,首先要做的就是找到它们啊!所以本文讲的是如何搜索同一区域网内的所有设备。
----要怎么才能做到找到区域网内的所有设备呢?
天马行空1:区域网内建立一个固定ip的服务器,一旦有新的设备连接,就给服务器发送自己的ip和端口存储起来。下线时服务器再把它移除。然后需要搜索区域网内的设备时,只需要查询服务器就行了。
条件:服务器ip不能被占用,需要一个服务器!而且感觉在移动设备上,这个方案不行啊。
天马行空2:类似天马行空1,不需要在本地建立服务器,而是在云端建立数据表...
条件:我在云端查询都需要联网了,你还跟我说不连接互联网也能通讯?
天马行空3:我一个一个ping一下...0-255多试几次嘛
条件:搜索时间那么长,看到我直接卸载这个破应用了
天马行空4:你当面告诉我ip和端口,或者生成二维码给我扫一扫(类似qq快传)嘛,然后在建立链接
条件:我有一句mmp不知道要不要讲...
有没有一种方法既不用建立服务器也不用链接互联网,更不用输入ip的方式?
那必须的啊,那就是用广播!我发一个广播,你们收到了就给我回应,这不是简单方便嘛。那怎么发广播呢?这里就涉及到UDP的应用了。关于socket通讯和udp的内容我就不多说了,还那句话不怎么了解的话先看看其他博客或者百度。
---还有一句话:我只是个Android小白,出错或者算法不够好在所难免,请大家多多指教---
思路:
搜索端:
1.建立一个UDP广播,并广播出去
2.建立接收端口,收到回应再给对方发一条确认消息(当然以后是用来约定TCP的传输端口的,这样就能进行通讯了)
2.1.接收端口循环接收,除非用户中断
响应端:
1.建立响应端口,收到广播之后响应消息
1.1循环等待响应,除非用户中断
2.如果收到的是确认消息,则根据约定的规则建立TCP连接(这个文章主要时搜索设备,所以这个没写)
同时,因为是运行在android上,我的要求是要做到 我可以搜索你也可以响应你,反之你可以搜索我也可以响应我。
如果只是一个搜索端,其他都只能是响应端,那不是c/s模型,只有一个能搜索了,这显然不太符合实际要求,所以其实每个设备都有一个响应端和一个搜索端。
这样的话到时候建立TCP连接时,会出现一个问题:到底是由谁来建立服务端?这个在之后在进行处理。
有了思路就开始撸代码了--记住本文只是做到能搜索区域网内的所有设备!
搜索端线程:
static class SearchThread extends Thread { private boolean flag = true; private byte[] recvDate = null; private byte[] sendDate = null; private DatagramPacket recvDP = null; private DatagramSocket recvDS = null; private DatagramSocket sendDS = null; private Handler mHandler; private StateChangeListener onStateChangeListener; private int state; private int maxDevices;//防止广播攻击,设置最大搜素数量 public static final int STATE_INIT_FINISH = 0; public static final int STATE_SEND_BROADCAST = 1; public static final int STATE_WAITE_RESPONSE = 2; public static final int STATE_HANDLE_RESPONSE = 3; public SearchThread(Handler handler, int max) { recvDate = new byte[256]; recvDP = new DatagramPacket(recvDate, 0, recvDate.length); mHandler = handler; maxDevices = max; } public void setOnStateChangeListener(StateChangeListene dcaf r onStateChangeListener) { this.onStateChangeListener = onStateChangeListener; } public void run() { try { recvDS = new DatagramSocket(54000);//接收响应套接口 sendDS = new DatagramSocket();//广播发送套接口 changeState(STATE_INIT_FINISH);//更新线程状态 //发送一次广播:广播地址255.255.255.255和组播地址224.0.1.140 -- 为了防止丢包,理应多次发送 sendDate = "name:服务器:msg:你好啊:type:search".getBytes();//设置发送数据 DatagramPacket sendDP = new DatagramPacket(sendDate, sendDate.length, InetAddress.getByName("255.255.255.255"), 53000);//广播UDP数据包 sendDS.send(sendDP);//发送数据包 changeState(STATE_SEND_BROADCAST);//更新线程状态 sendMsg("等待接收-----");//日志打印 int curDevices = 0;//当前搜索到的设备数量 while (flag) { changeState(STATE_WAITE_RESPONSE); recvDS.receive(recvDP);//阻塞等待接收响应 changeState(STATE_HANDLE_RESPONSE); String recvContent = new String(recvDP.getData()); //判断是不是本机发起的结束搜索请求--处理响应内容 if (recvContent.contains("stop_search")) { sendMsg("停止搜索:" + flag); } else { if (curDevices >= maxDevices) { break; } sendMsg("收到:" + recvDP.getAddress() + ":" + recvDP.getPort() + " 发来:" + recvContent); //回应 sendDate = "name:服务器:msg:你好啊:type:response".getBytes();//回应内容 DatagramPacket responseDP = new DatagramPacket(sendDate, sendDate.length, recvDP.getAddress(), 53000);//回应数据包 sendDS.send(responseDP);//发送回应 curDevices++; } } } catch (IOException e) { e.printStackTrace(); } finally { if (recvDS != null) recvDS.close(); if (sendDS != null) sendDS.close(); } } private void sendMsg(String string) { Message msg = Message.obtain(mHandler); msg.obj = string; mHandler.sendMessage(msg); } public void stopSearch() { flag = false; //由于在等待接收数据包时阻塞,无法达到关闭线程效果,因此给本机发送一个消息取消阻塞状态 //为了避免用户在UI线程调用,所以新建一个线程 new Thread() { @Override public void run() { if (sendDS != null) { sendDate = "name:服务器:msg:stop_search:type:stop".getBytes(); try { DatagramPacket sendDP = new DatagramPacket(sendDate, sendDate.length, InetAddress.getByName("localhost"), 54000); sendDS.send(sendDP); } catch (IOException e) { e.printStackTrace(); } } } }.start(); } public void startSearch() { flag = true; start(); sendMsg("开始搜索"); } private void changeState(int state) { this.state = state; if (onStateChangeListener != null) { onStateChangeListener.onStateChanged(this.state); } } //搜索状态更新回调 public interface StateChangeListener { void onStateChanged(int state); } }
响应端代码:
/** * 等待搜索线程 */ static class ResponseThread extends Thread { private byte[] recvDate = null; private byte[] sendDate = null; private DatagramPacket recvDP; private DatagramSocket recvDS = null; private DatagramSocket sendDS = null; private boolean flag = true; private Handler mHandler; public ResponseThread(Handler handler) { recvDate = new byte[256]; recvDP = new DatagramPacket(recvDate, 0, recvDate.length); mHandler = handler; } public void run() { try { sendMsg("设备已经开启,等待其他设备搜索..."); recvDS = new DatagramSocket(53000);//用于接收搜索端的套接口 sendDS = new DatagramSocket();//用于给搜索端发送确认信息 while (flag) { recvDS.receive(recvDP);//阻塞等待搜索广播 String content = new String(recvDP.getData()); if (content.contains("response")) { sendMsg("确认收到回应"); } else if (content.contains("stop_receive")) { sendMsg("下线:" + flag); } else { sendMsg("收到:" + recvDP.getAddress() + ":" + recvDP.getPort() + " 发来连接请求:" + content); sendDate = "name:客户端:msg:我收到了:type:response".getBytes(); sendMsg("回应>>"); DatagramPacket sendDP = new DatagramPacket(sendDate, sendDate.length, recvDP.getAddress(), 54000); sendDS.send(sendDP); } } } catch (IOException e) { e.printStackTrace(); } finally { if (recvDS != null) recvDS.close(); if (sendDS != null) sendDS.close(); } } private void sendMsg(String string) { Message msg = Message.obtain(mHandler); msg.obj = string; mHandler.sendMessage(msg); } public void startResponse() { flag = true; start(); sendMsg("上线"); } public void stopResponse() { flag = false; //为了避免用户在UI线程调用,所以新建一个线程 new Thread() { @Override public void run() { if (sendDS != null) { sendDate = "name:客户端:msg:stop_receive:type:stop".getBytes(); try { DatagramPacket sendDP = new DatagramPacket(sendDate, sendDate.length, InetAddress.getByName("localhost"), 53000); sendDS.send(sendDP); } catch (IOException e) { e.printStackTrace(); } } } }.start(); } }
这样就完成了一个简单搜索区域网中的所有用户了,实践证明这个方法高效可用。在同一个wifi可以用,我在公司中测试,不同wifi下也可以用(因为公司网络本身就是区域网)。
其他也没啥需要解释的,看看代码就理解过程了。这里说明几点:
1.两个类里面都有一个Handler对象和sendMsg()的方法,这里是为了在android中工作线程和Ui线程进行通讯。如果实在其他平台上可以删除掉。
2.搜索端和响应端的发送和搜索端口刚好是对调,因为不能在同一个程序的创建同一个端口的套接字
3.这个例子中响应的消息只是为了方便测试所以以:分割键值对,实际使用大家可以根据自己的协议发送json数据,例如约定通讯时谁当服务端,连接端口是什么等。
还有关于关闭线程的,我们来讨论一下:
在这里例子中,没有设置超时时间,完全是阻塞式接收,那么关闭线程的权利也留给了用户。但是这里我只是 用一个标志位来控制会有什么问题呢?
首先,receive()方法是阻塞的,就算我改变了标志位可是线程依然没结束啊,一直等待一个数据包才回进行下一轮循环。所以我的解决方法是自己给自己发个结束的数据包。
然后,有的人说不可以直接强制结束线程么?调用线程的结束方法。那么在java中线程确实有个stop()的方法,但是不推荐使用,具体原因官方有解释。能用的方法是使用interrupt()方法抛出一个异常退出,奇怪的是我用了没作用,不知问题出在什么地方,希望有人可以测试一下告诉我答案..
那么,能不能直接关闭xxxsocket.close()套接字呢?似乎可以吧!这个我是试过了,但是结果还是让你们自己去试试吧...
最后,线程是能结束了,可是在android中,你新建了这个线程对象调用了.start(),然后在调用.stop(),最后想重新开启直接再次调用.start()行不行呢?
答案是不行!会抛出java.lang.IllegalThreadStateException: Thread already started,即使你的run方法逻辑已经执行完了,但是你还是用不能直接再次start()必须重新new 一个对象。这样的话,我设置的回调又要重新设置了!
后来我想到一个方法,用户停止之后,我不是退出循环,而是进入一个休眠状态,当用户再次开启时在唤醒继续工作。这样应该就能解决了。如果大家有什么更好的方法,欢迎分享出来讨论讨论。
简单测试代码:
public class ConnActivity extends AppCompatActivity implements View.OnClickListener { private LinearLayout logContainer; private SearchThread searchThread; private ResponseThread responseThread; private boolean in_searching, in_response; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_conn); findViewById(R.id.start).setOnClickListener(this); findViewById(R.id.stop).setOnClickListener(this); findViewById(R.id.online).setOnClickListener(this); findViewById(R.id.offline).setOnClickListener(this); logContainer = (LinearLayout) findViewById(R.id.log); } private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); showLog((String) msg.obj); } }; @Override public void onClick(View v) { switch (v.getId()) { case R.id.start: if (!in_searching) { searchThread = new SearchThread(mHandler, 20); searchThread.startSearch(); in_searching = true; } else { Toast.makeText(this, "线程已经启动", Toast.LENGTH_SHORT).show(); } break; case R.id.stop: if (in_searching) { searchThread.stopSearch(); in_searching = false; } else { Toast.makeText(this, "线程未启动", Toast.LENGTH_SHORT).show(); } break; case R.id.online: if (!in_response) { responseThread = new ResponseThread(mHandler); responseThread.startResponse(); in_response = true; } else { Toast.makeText(this, "线程已经启动", Toast.LENGTH_SHORT).show(); } break; case R.id.offline: if (in_response) { responseThread.stopResponse(); in_response = false; } else { Toast.makeText(this, "线程未启动", Toast.LENGTH_SHORT).show(); } break; } } private void showLog(final String msg) { TextView tv = new TextView(ConnActivity.this); tv.setText(msg); logContainer.addView(tv); }xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:id="@+id/start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="开始搜索" /> <Button android:id="@+id/stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止搜索" /> <Button android:id="@+id/online" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="上线" /> <Button android:id="@+id/offline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下线" /> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/log" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > </LinearLayout> </ScrollView> </LinearLayout>
界面就是四个按钮,然后下方显示日志。
运行截图,懒得一步一步截图,大家按照下面的顺序对照图片看看就行了,实在看不明白自己运行试试咯:
1.设备1上线:
2.设备2搜索
3.设备2自己也上线,再次搜索
4.设备1也发起搜索:
5.两个设备都停止搜索并且下线
截图的日志有点混乱,主要是假如本机也上线,搜索也是能搜索出本机的,这个本机屏蔽的逻辑还没做。
设备1:
设备2:
本文到此结束,谢谢大家。
相关文章推荐
- Android/安卓开发之WIFI通讯(下)--与搜索到的设备进行通讯
- 14天学会安卓开发(第十天)Android网络与通讯
- 安卓USB HOST开发之Android平板+OTG线+4口USB hub+4个HID设备
- Android百度公交开发(七)——百度地图API之城市POI搜索-获取所有结果
- Android官方开发文档Training系列课程中文版:连接无线设备之通过P2P搜索网络服务
- Android蓝牙开发——搜索设备
- 【Android 应用开发】分析各种Android设备屏幕分辨率与适配 - 使用大量真实安卓设备采集真实数据统计
- Android WiFi开发教程(二)——WiFi的搜索和连接
- Android官方开发文档Training系列课程中文版:连接无线设备之网络服务搜索功能
- Android官方开发文档Training系列课程中文版:连接无线设备之通过WIFI创建P2P连接
- Android开发蓝牙篇之蓝牙设备开启、搜索周边蓝牙设备
- Android蓝牙开发教程(三)——蓝牙设备相互通讯
- android开发之蓝牙初步 扫描已配对蓝牙、更改蓝牙可见性、搜索外部蓝牙设备
- Android 蓝牙通信开发(一) 搜索蓝牙设备
- Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
- 支持5G-WiFi的安卓设备搜索不到5G信号解决方法
- 支持5G-WiFi的安卓设备搜索不到5G信号解决方法
- 【Android 应用开发】分析各种Android设备屏幕分辨率与适配 - 使用大量真实安卓设备采集真实数据统计
- Android蓝牙开发教程(一)——搜索蓝牙设备
- Android(安卓)系统USB-OTG-HID外设通讯开发