您的位置:首页 > 移动开发 > Android开发

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

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

搜索端线程:

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:



本文到此结束,谢谢大家。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐