2018年的第一篇文章,最近在使用UDP协议进行硬件通信,大家都知道UDP协议通信必须在同一个局域网内,但是每个用户家的wifi都是不一样的,硬件设备是无法只值连接到用户家的wifi的。所以为了解决这个问题,提出一个思路,让手机开启热点,然后把硬件链接到手机的热点上。再由手机告诉硬件去链接用户家里的wifi,这样手机和设备就都能连接到用户家的wifi了,就能愉快的进行通信了。那么怎么解决这个问题呢?继续往下看!
测试真机: 魅族4 Android5.1 、小米5 Android 7.0
这里就不说怎么进行UDP通信了,只说在中间遇到的问题。两个坑吧,一个是开启热点兼容6.x+,另一个是获取广播地址,兼容wifi环境,以太网环境,无网络环境。
这里先提供一个开启/关闭热点的工具类WifiUtils:
import android.net.wifi.WifiConfiguration;import android.net.wifi.WifiManager;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * 作者:dell or Xiaomi Li * 时间: 2018/1/17 * 内容:打开/关闭热点 * 最后修改: */public class WifiUtils { private final static String APName = "XiaomiLi8"; private final static String APPassword = "5311925577"; /** * 创建热点 * * @return */ public static boolean CreatHotspot(WifiManager wifiManager) { boolean request; //开启热点 if (wifiManager.isWifiEnabled()) { //如果wifi处于打开状态,则关闭wifi, wifiManager.setWifiEnabled(false); } WifiConfiguration config = new WifiConfiguration(); config.SSID = APName; config.preSharedKey = APPassword; config.hiddenSSID = false;//是否隐藏网络 config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);//开放系统认证 config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); config.status = WifiConfiguration.Status.ENABLED; //通过反射调用设置热点 try { Method method = wifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, Boolean.TYPE); boolean enable = (Boolean) method.invoke(wifiManager, config, true); if (enable) { request = true; } else { request = false; LogUtils.Loge("创建失败"); } } catch (Exception e) { e.printStackTrace(); LogUtils.Loge(e.toString() + "创建失败"); request = false; } return request; } /** * 关闭热点,并开启wifi */ public static void closeWifiHotspot(WifiManager wifiManager) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = wifiManager.getClass().getMethod("getWifiApConfiguration"); method.setAccessible(true); WifiConfiguration config = (WifiConfiguration) method.invoke(wifiManager); Method method2 = wifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class); method2.invoke(wifiManager, config, false); //开启wifi wifiManager.setWifiEnabled(true); }}
需要用到权限:
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/><uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.WRITE_SETTINGS" />
这里来说坑,经过测试发现在Android6.0一下的手机是可以正常开启热点的,但是在6.0以上的手机。开启热点会报如下错误:
java.lang.reflect.InvocationTargetException
大家都能猜到是权限的问题,确实如此,经过查找资料很多博客也都说是权限的问题,也有人说直接把版本设置在22,这样就不用管权限问题了,但是这种方法也行可以归纳为不正常手段。所以这里就不用这种方法了。然后继续查询,发现是android.permission.WRITE_SETTINGS这个权限发生的错误,既然是权限问题,那就去动态申请权限就好了。然后就会发现,申请之后根本没有用。还是没有权限。那这是为何呢?
因为在android 6.0及以后,WRITE_SETTINGS权限的保护等级已经由原来的dangerous升级为signature,这意味着我们的APP需要用系统签名或者成为系统预装软件才能够申请此权限,并且还需要提示用户跳转到修改系统的设置界面去授予此权限。所以我们动态申请权限是没有用的。这里先给出参考地址:http://blog.csdn.net/XieGaoXiong/article/details/52317155 然后给出申请的方法,如下:
/** * WIFI设置请求码 */ private final int REQUEST_CODE_ASK_WRITE_SETTINGS = 0X1; /** * 请求权限 */ private void getWifiPreMission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.System.canWrite(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:" + getPackageName())); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivityForResult(intent, REQUEST_CODE_ASK_WRITE_SETTINGS); } else { //有了权限去做什么呢? getConnectWifiSsid(); } } else { getConnectWifiSsid(); } }
然后在Activity的onActivityResult()方法中去操作一波:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_ASK_WRITE_SETTINGS || requestCode == PreMissionDialog.ACTION_APPLICATION_DETAILS_SETTINGS) { if (!Settings.System.canWrite(this)) { //如果还是没有权限,就弹框提醒用户 PreMissionDialog.showPermissionDialog(SearchEqListActivity.this, "系统设置"); } else { getConnectWifiSsid(); } } }
这里把提醒用户的弹框也给出来,可以拿去用,觉得丑可以自己写一个:
import android.app.Activity;import android.content.DialogInterface;import android.content.Intent;import android.net.Uri;import android.provider.Settings;import android.support.v7.app.AlertDialog;/** * 作者:dell or Xiaomi Li * 时间: 2018/1/17 * 内容:提醒用户开启权限弹框 * 最后修改: */public class PreMissionDialog { public final static int ACTION_APPLICATION_DETAILS_SETTINGS = 0x100; /** * 申请权限 * * @param message */ public static void showPermissionDialog(final Activity mActivity, String message) { AlertDialog.Builder dialog = new AlertDialog.Builder(mActivity); dialog.setTitle("权限提醒") .setMessage("请在权限管理中允许" + message + "权限") .setPositiveButton("权限设置", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", mActivity.getPackageName(), null); intent.setData(uri); mActivity.startActivityForResult(intent, ACTION_APPLICATION_DETAILS_SETTINGS); } }) .setNegativeButton("取消", null) .create() .show(); }}
进行上述操作,就可以成功开启热点了。
UDP发送一个广播命令,需要有一个广播地址。先说一下广播地址是怎么组成的。通常大家连接到的wifi是IP地址是:1xx.1xx.2xx.45 这样样子的。那么广播地址就是:1xx.1xx.2xx.255 这样。那么问题来了,这个广播地址是怎么来的呢?是用IP地址的最后一个“.”后边的数字改成“255” 么?不是的,这里就要提一下子网掩码了。先看一下图片:
通过上边的图片大家可以发现,并不是所有的子网掩码都是255.255.255.0 这样子的。那么广播地址和子网掩码又有什么关系呢?其实广播地址是当前网段的最后一个地址,而通过子网掩码就能看出来当前网段有多多少个地址。像最后一个是0的,就是有1-255个地址,最后一个.255就是广播地址。而第二个240的呢,就是有1-15个地址,最后一个.15就是广播地址。
也就是说用255-子网掩码的最后一个值就是广播地址。
那么结论就出来了,最后的广播地址等于IP地址的前三个+(255-子网掩码的最后一个)。这样拼起来就是真正的广播地址。(这里不知道解释的清楚不清楚,或者说的就有错误,欢迎各位大牛提意见!)
然后咱们就去去子网掩码和IP地址就好了。那么坑又来了。在wifi环境下,取到这两个值是没问题的,但是在以太网环境下,怎么获取IP地址呢,是不是需要必须有网络呢?为什么在手机信息里边看到的IP地址和电脑链接手机热点的IP地址不在一个网段呢?唉,问题还真多!直接放一个工具类出来好了,通过这个工具类就能截至获取到广播地址了,包括wifi环境和以太网环境!代码如下:
import android.content.Context;import android.net.DhcpInfo;import android.net.wifi.WifiInfo;import android.net.wifi.WifiManager;import java.net.InetAddress;import java.net.InterfaceAddress;import java.net.NetworkInterface;import java.net.SocketException;import java.util.Enumeration;import java.util.List;/** * 类描述:获取ip * 作 者:Admin or 李小米 * 时 间:2018/1/11 * 修改备注: */public class IPUtils { public static String getIp(Context mContext) throws SocketException { String ip = ""; //获取wifi服务 WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); if (wifiManager.isWifiEnabled()) { WifiInfo wifiInfo = wifiManager.getConnectionInfo(); int ipAddress = wifiInfo.getIpAddress(); DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); String dhcpInfos = intToIp(dhcpInfo.netmask); String[] split = intToIp(ipAddress).split("\\."); ip = split[0] + "." + split[1] + "." + split[2] + "." + (255 - Integer.parseInt(dhcpInfos.split("\\.")[3]));//根据子网掩码获取广播的IP地址 } else { String asd = getInfo(); String[] split = asd.split(","); String ipStr = split[0]; String NetMask = split[1]; String[] split1 = ipStr.split("\\."); ip = split1[0] + "." + split1[1] + "." + split1[2] + "." + (255 - Integer.parseInt(NetMask.split("\\.")[3]));//根据子网掩码获取广播的IP地址 } return ip; } private static String intToIp(int paramInt) { return (paramInt & 0xFF) + "." + (0xFF & paramInt >> 8) + "." + (0xFF & paramInt >> 16) + "." + (0xFF & paramInt >> 24); } public static String getInfo() throws SocketException { String ipAddress = ""; String maskAddress = ""; for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { NetworkInterface intf = en.nextElement(); List<InterfaceAddress> mList = intf.getInterfaceAddresses(); for (InterfaceAddress l : mList) { InetAddress inetAddress = l.getAddress(); if (!inetAddress.isLoopbackAddress()) { String hostAddress = inetAddress.getHostAddress(); if (hostAddress.indexOf(":") > 0) { continue; } else { ipAddress = hostAddress; maskAddress = calcMaskByPrefixLength(l.getNetworkPrefixLength()); } } } } return ipAddress + "," + maskAddress; } private static String calcMaskByPrefixLength(int length) { int mask = -1 << (32 - length); int partsNum = 4; int bitsOfPart = 8; int maskParts[] = new int[partsNum]; int selector = 0x000000ff; for (int i = 0; i < maskParts.length; i++) { int pos = maskParts.length - 1 - i; maskParts[pos] = (mask >> (i * bitsOfPart)) & selector; } String result = ""; result = result + maskParts[0]; for (int i = 1; i < maskParts.length; i++) { result = result + "." + maskParts[i]; } return result; }}
这里因为是直接在项目里写的,就不写demo了,遇到的问题都贴出来代码了。希望可以帮到小伙伴们。哦,如果文中有错误,尤其是对子网掩码的解释,欢迎大牛们提出批评意见!
联系客服