WIFI通讯:双网络通讯
随着公司产品的不断迭代,发现上位机(App)与下位机(Vci)通讯,在收发长包数据时,蓝牙通道出现了丢字节,丢包现象。而这时候会提示用户使用USB串口通讯方式。但是USB串口通讯,必须局限于线束的连接。
为了优化这个问题,增加引入了WIFI上下位机通讯通道。
应用场景
App与硬件使用WIFI(无网)通道进行命令收到,并在App内强制使用4G通道进行Http请求
1,接收到WIFI接连的广播后,执行连接操作
/**
* 发现wifi设备
*
* @param wifiScanResultEvent
*/
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onRecivedBluetoothDevice(WifiScanResultEvent wifiScanResultEvent) {
if (deviceInfo == null) {
Log.e(TAG, "WIFI信息为空,停止扫描");
return;
}
if (isConnecting()) {
Log.e(TAG, "WIFI连接中,停止此次扫描");
return;
}
//如果WIFI已连接上则退出
if (isConnected()) {
Log.e(TAG, "WIFI已连接上,不在扫描");
return;
}
if (addNetworkConfiguration(deviceInfo.getBssid())) {
stopScanDevice();
connecting();
if (connectToNetwork(deviceInfo.getSsid(), deviceInfo.getPassword(), deviceInfo.getWifiType()) < 0) {
connectionError(-3, "无法添加wifi配置信息");
} else {
stopDiscovery();
Observable<String> observable = Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> emitter) {
createScokect();
emitter.onNext("");
}
});
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String bean) {
}
};
observable.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(consumer);
}
}
}
/**
* 添加wifi链接配置
*
* @param bssid 下位机wifi mac地址
* @return 有该wifi设备信号
*/
private boolean addNetworkConfiguration(String bssid) {
//获取当前wifi 列表,有可能要链接的wifi不处于当前列表中
List<ScanResult> wifiList = mWifiManager.getScanResults();
if (wifiList == null || wifiList.size() == 0) {
return false;
}
ScanResult scanResul;
for (int i = 0, size = wifiList.size(); i < size; i++) {
scanResul = wifiList.get(i);
Log.e(TAG, "wifi name " + scanResul.SSID + " mac " + scanResul.BSSID);
Loger.printLog("scanWifiList", "wifi name " + scanResul.SSID + " mac " + scanResul.BSSID);
if (scanResul.BSSID.equals(bssid)) {
Log.e(TAG, "匹配到wifi name " + scanResul.SSID + " mac " + scanResul.BSSID);
return true;
}
}
return false;
}
/**
* 链接到wifi网络
*
* @param ssid
* @param password
* @param type
* @return 成功链接到wifi后返回
*/
private int connectToNetwork(String ssid, String password, @WifiType int type) {
WifiConfiguration configuration = createWifiInfo(ssid, password, type);
int networkId = mWifiManager.addNetwork(configuration); // 添加WIFI网络
if (networkId < 0) {
Log.e(TAG, "WIFI已连接失败 networkId:" + networkId);
return -1;
}
// 使WIFI网络有效
mWifiManager.enableNetwork(networkId, true);
Log.e(TAG, "WIFI已连接成功 networkId:" + networkId);
return networkId;
}
/**
* 创建WiFi配置
*
* @param ssid
* @param password
* @param type
* @return
*/
private WifiConfiguration createWifiInfo(String ssid, String password, @WifiType int type) {
clearOldConfiguration(ssid);
WifiConfiguration config = new WifiConfiguration();
config.allowedAuthAlgorithms.clear();
config.allowedGroupCiphers.clear();
config.allowedKeyManagement.clear();
config.allowedPairwiseCiphers.clear();
config.allowedProtocols.clear();
config.SSID = "\"" + ssid + "\"";
// 分为三种情况:1没有密码2用wep加密3用wpa加密
if (type == WifiType.TYPE_NO_PASSWD) {// WIFICIPHER_NOPASS
config.wepKeys[0] = "";
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
config.wepTxKeyIndex = 0;
} else if (type == WifiType.TYPE_WEP) { // WIFICIPHER_WEP
config.hiddenSSID = true;
config.wepKeys[0] = "\"" + password + "\"";
config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
config.wepTxKeyIndex = 0;
} else if (type == WifiType.TYPE_WPA) { // WIFICIPHER_WPA
config.preSharedKey = "\"" + password + "\"";
config.hiddenSSID = true;
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.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
config.status = WifiConfiguration.Status.ENABLED;
config.status = WifiConfiguration.Status.ENABLED;
} else if (type == WifiType.TYPE_WPA2) {
config.preSharedKey = "\"" + password + "\"";
config.hiddenSSID = true;
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;
}
return config;
}
/**
* 删除之前的链接配置信息
*
* @param ssid
*/
private void clearOldConfiguration(String ssid) {
List<WifiConfiguration> existingConfigs = mWifiManager.getConfiguredNetworks();
if (existingConfigs == null || existingConfigs.size() == 0) {
return;
}
WifiConfiguration wiifConfig;
for (int i = 0, size = existingConfigs.size(); i < size; i++) {
wiifConfig = existingConfigs.get(i);
if (wiifConfig.SSID.equals("\"" + ssid + "\"")) {
mWifiManager.removeNetwork(wiifConfig.networkId); //TODO 删除之前保存的wifi链接信息 ?
}
}
}
2.连接成功WIFI后创建Socket连接(这里是启动ServerSocket,等待下位机连接),下位机连接成功创建IO
private void createScokect() {
closeSocket();
try {
Log.i(TAG, "server已启动,等待客户端连接");
int port = 8084;
serverSocket = new ServerSocket(port);
Log.i(TAG, serverSocket.toString());
//侦听对此套接字的连接并接受它。 该方法将阻塞,直到建立连接。
mWifiSocket = serverSocket.accept();
Log.i(TAG, "client 已连接");
//设置服务器等待超时时间,0:表示无限等待
mWifiSocket.setSoTimeout(30000);
if (mWifiSocket.isConnected()) {
mOutputStream = mWifiSocket.getOutputStream();
mInputStream = mWifiSocket.getInputStream();
connected();
Log.e(TAG, "Socket连接成功");
} else {
Log.e(TAG, "Socket连接失败1");
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "ServerSocket" + e.getMessage());
}
}
3.数据收发
public class WifiTransfer extends BaseWifiConnceter {
@Override
public boolean sendData(byte[] data) {
if (isConnected()) {
if (mOutputStream != null) {
try {
mOutputStream.write(data);
mOutputStream.flush();
onDataSended(data);
return true;
} catch (IOException e) {
onSendDataError(-3, "写入时IO异常:" + e.getLocalizedMessage());
return false;
}
}
onSendDataError(-2, "程序异常,数据流断开");
return false;
} else {
onSendDataError(-1, "连接已断开");
return false;
}
}
@Override
protected byte[] readData() {
if (mInputStream != null) {
try {
int length = mInputStream.available();
if (length > 1) {
byte[] temp = new byte[length];
mInputStream.read(temp);
return temp;
}
} catch (IOException e) {
onRecivedDataErrror(-4, "读取数据时IO异常");
}
}
return null;
}
}
4.开启V&P&N
通常Android设备连接了一个无法联网的WIFI时,即使4G打开的情况下,应用内的网络请求也是优先走WIFI通道,导致无法进行常规的Http连接请求。
解决办法:开启V&P&N
if (ret.getDeviceType() == DeviceType.WIFI) {
journal.setCatalog1("WIFI连接");
hideBluetoothConnectDialog();
if (VpnUtils.lacksPermission(VpnUtils.permissions, BaseTopBarActivity.this)) {//判断是否拥有权限
//请求权限,第二参数权限String数据,第三个参数是请求码便于在onRequestPermissionsResult 方法中根据code进行判断
ActivityCompat.requestPermissions(BaseTopBarActivity.this, permissions, OPEN_SET_REQUEST_CODE);
} else if (VpnUtils.lacksPermission(VpnUtils.PERMISSIONS_STORAGE, BaseTopBarActivity.this)) {//判断是否拥有权限
ActivityCompat.requestPermissions(BaseTopBarActivity.this, VpnUtils.PERMISSIONS_STORAGE, VpnUtils.REQUEST_PERMISSION_CODE);
} else {
//拥有权限执行操作
Intent vpnIntent = VpnService.prepare(BaseTopBarActivity.this);
if (vpnIntent != null)
startActivityForResult(vpnIntent, VpnUtils.VPN_REQUEST_CODE);
else {
startService(new Intent(BaseTopBarActivity.this, LocalVPNService.class));
}
}
}
public class LocalVPNService extends VpnService {
private static final String TAG = LocalVPNService.class.getSimpleName();
private static final String VPN_ADDRESS = "10.0.0.2"; // Only IPv4 support for now
private static final String VPN_ROUTE = "0.0.0.0"; // Intercept everything
public static final String BROADCAST_VPN_STATE = "com.commonrail.mft.decoder.localvpn.VPN_STATE";
private static boolean isRunning = false;
private ParcelFileDescriptor vpnInterface = null;
private ConcurrentLinkedQueue<Packet> deviceToNetworkUDPQueue;
private ConcurrentLinkedQueue<Packet> deviceToNetworkTCPQueue;
private ConcurrentLinkedQueue<ByteBuffer> networkToDeviceQueue;
private ExecutorService executorService;
private Selector udpSelector;
private Selector tcpSelector;
public static Network networkId;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onCreate() {
super.onCreate();
isRunning = true;
setupVPN();
try {
udpSelector = Selector.open();
tcpSelector = Selector.open();
deviceToNetworkUDPQueue = new ConcurrentLinkedQueue<>();
deviceToNetworkTCPQueue = new ConcurrentLinkedQueue<>();
networkToDeviceQueue = new ConcurrentLinkedQueue<>();
executorService = Executors.newFixedThreadPool(5);
executorService.submit(new UDPInput(networkToDeviceQueue, udpSelector));
executorService.submit(new UDPOutput(deviceToNetworkUDPQueue, udpSelector, this));
executorService.submit(new TCPInput(networkToDeviceQueue, tcpSelector));
executorService.submit(new TCPOutput(deviceToNetworkTCPQueue, networkToDeviceQueue, tcpSelector, this));
executorService.submit(new VPNRunnable(vpnInterface.getFileDescriptor(),
deviceToNetworkUDPQueue, deviceToNetworkTCPQueue, networkToDeviceQueue));
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_VPN_STATE).putExtra("running", true));
requestWorkNetForHigh(this);
} catch (IOException e) {
Log.e(TAG, "Error starting service", e);
cleanup();
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setupVPN() {
if (vpnInterface == null) {
Builder builder = new Builder();
builder.addAddress(VPN_ADDRESS, 32);
builder.addRoute(VPN_ROUTE, 0);
builder.addDnsServer("8.8.8.8");
builder.addDnsServer("8.8.4.4");
builder.addDnsServer("208.67.222.222");
builder.addDnsServer("114.114.114.114");
builder.setMtu(1280);
vpnInterface = builder.setSession(getString(R.string.app_name)).establish();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
public static boolean isRunning() {
return isRunning;
}
@Override
public void onDestroy() {
super.onDestroy();
isRunning = false;
executorService.shutdownNow();
cleanup();
Log.i(TAG, "Stopped");
}
private void cleanup() {
deviceToNetworkTCPQueue = null;
deviceToNetworkUDPQueue = null;
networkToDeviceQueue = null;
ByteBufferPool.clear();
closeResources(udpSelector, tcpSelector, vpnInterface);
}
// TODO: Move this to a "utils" class for reuse
private static void closeResources(Closeable... resources) {
for (Closeable resource : resources) {
try {
resource.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static class VPNRunnable implements Runnable {
private static final String TAG = VPNRunnable.class.getSimpleName();
private FileDescriptor vpnFileDescriptor;
private ConcurrentLinkedQueue<Packet> deviceToNetworkUDPQueue;
private ConcurrentLinkedQueue<Packet> deviceToNetworkTCPQueue;
private ConcurrentLinkedQueue<ByteBuffer> networkToDeviceQueue;
public VPNRunnable(FileDescriptor vpnFileDescriptor,
ConcurrentLinkedQueue<Packet> deviceToNetworkUDPQueue,
ConcurrentLinkedQueue<Packet> deviceToNetworkTCPQueue,
ConcurrentLinkedQueue<ByteBuffer> networkToDeviceQueue) {
this.vpnFileDescriptor = vpnFileDescriptor;
this.deviceToNetworkUDPQueue = deviceToNetworkUDPQueue;
this.deviceToNetworkTCPQueue = deviceToNetworkTCPQueue;
this.networkToDeviceQueue = networkToDeviceQueue;
}
@Override
public void run() {
Log.i(TAG, "Started");
FileChannel vpnInput = new FileInputStream(vpnFileDescriptor).getChannel();
FileChannel vpnOutput = new FileOutputStream(vpnFileDescriptor).getChannel();
try {
ByteBuffer bufferToNetwork = null;
boolean dataSent = true;
boolean dataReceived;
while (!Thread.interrupted()) {
if (dataSent)
bufferToNetwork = ByteBufferPool.acquire();
else
bufferToNetwork.clear();
// TODO: Block when not connected
int readBytes = vpnInput.read(bufferToNetwork);
if (readBytes > 0) {
dataSent = true;
bufferToNetwork.flip();
Packet packet = new Packet(bufferToNetwork);
if (packet.isUDP()) {
deviceToNetworkUDPQueue.offer(packet);
} else if (packet.isTCP()) {
deviceToNetworkTCPQueue.offer(packet);
} else {
Log.w(TAG, "Unknown packet type");
Log.w(TAG, packet.ip4Header.toString());
dataSent = false;
}
} else {
dataSent = false;
}
ByteBuffer bufferFromNetwork = networkToDeviceQueue.poll();
if (bufferFromNetwork != null) {
bufferFromNetwork.flip();
while (bufferFromNetwork.hasRemaining())
vpnOutput.write(bufferFromNetwork);
dataReceived = true;
ByteBufferPool.release(bufferFromNetwork);
} else {
dataReceived = false;
}
// TODO: Sleep-looping is not very battery-friendly, consider blocking instead
// Confirm if throughput with ConcurrentQueue is really higher compared to BlockingQueue
if (!dataSent && !dataReceived)
Thread.sleep(10);
}
} catch (InterruptedException e) {
Log.i(TAG, "Stopping");
} catch (IOException e) {
Log.w(TAG, e.toString(), e);
} finally {
closeResources(vpnInput, vpnOutput);
}
}
}
@SuppressLint("WrongConstant")
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void requestWorkNetForHigh(Context context) {
final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
Log.i(TAG, "ConnectivityManager is null, cannot try to force a mobile connection");
return;
}
Log.i(TAG, "post50MobileEnable, requesting Cellular network");
NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addCapability(NET_CAPABILITY_INTERNET);
//强制使用蜂窝数据网络-移动数据
builder.addTransportType(TRANSPORT_CELLULAR);
NetworkRequest build = builder.build();
try {
Log.i(TAG, "Requesting Connectivity Manager");
cm.requestNetwork(build, new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
networkId = network;
Log.i(TAG, TAG + "已根据功能和传输类型找到合适的网络 NetWorkCallBack " + network);
if (Build.VERSION.SDK_INT >= 23) {
cm.bindProcessToNetwork(network);
} else {// 23后这个方法舍弃了
ConnectivityManager.setProcessDefaultNetwork(network);
}
}
});
} catch (SecurityException e) {
e.printStackTrace();
Log.i(TAG, "com.speedify.speedifyAndroid.MobileController Cannot write settings. Requesting Permission");
} catch (Exception e2) {
e2.printStackTrace();
Log.e(TAG, "Unknown Exception", e2);
}
}
}