Android安卓开发之WIFI通讯(上)--搜索区域网内所有设备

xiaoxiao2021-02-28  14

我的上一篇文章写的是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连接时,会出现一个问题:到底是由谁来建立服务端?这个在之后在进行处理。

 

有了思路就开始撸代码了--记住本文只是做到能搜索区域网内的所有设备!

搜索端线程:

 

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(StateChangeListener 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:

本文到此结束,谢谢大家。  

转载请注明原文地址: https://www.6miu.com/read-1100079.html

最新回复(0)