写这篇文章的缘由:在Android原生端,iOS原生端,实现udp 组播相信很多小伙伴都会的,但在flutter 端实现这个对我们来说还是一个未知之数吧;
前期是想着去网上找一个第三方库 或者找一个第三方的插件 来完成任务;结果经过几天的调研下来,最终都是Android手机上可以正常使用,但在iOS手机上都不能正常使用,报以下的错误
Unhandled Exception: OS Error: Can't assign requested address, errno = 49
沿着这个错误跟踪下去 ,网上讨论的结果貌似是Dart SDK层报出来的错误,很多热心人事也与Dart团队在网上沟通,希望他们能解决此bug;不过大半年过去了,这个问题依旧存在。
看来指望Dart团队解决此事,有点遥遥无期的感觉,怎么办呢,我们不能因为这个bug ,就停止不前了吧;最终决定自己来造轮子----写udp组播插件;
写flutter插件入门,在这里就不多说了,小伙伴们自己去探索一下吧。下面就开始今天真正的代码环节了
import 'dart:async';
import 'package:flutter/services.dart';
class Pluginudp {
static const MethodChannel _channel =
const MethodChannel('pluginudp');
static Future startScan(String ipString) async {
await _channel.invokeMethod('startScan',{"data":ipString});
}
static Future stopScan() async {
await _channel.invokeMethod('stopScan');
}
static Future onlyToReceive() async {
await _channel.invokeMethod('onlyToReceive');
}
}
上面的代码是flutter层, 定义了3个方法,分别为
开始udp扫描--startScan
停止udp扫描--stopScan
只接收数据--onlyToReceive
iOS层的核心代码
#import "PluginudpPlugin.h"
#import "ScanX3UdpSocket.h"
@interface PluginudpPlugin ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) FlutterEventSink eventSink;
@property (nonatomic, copy) NSString *ipString;
@end
@implementation PluginudpPlugin
+ (void)registerWithRegistrar:(NSObject*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"pluginudp"
binaryMessenger:[registrar messenger]];
PluginudpPlugin* instance = [[PluginudpPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
FlutterEventChannel *eventChannel = [FlutterEventChannel
eventChannelWithName:@"FlutterUdpEvent"
binaryMessenger:[registrar messenger]];
[eventChannel setStreamHandler:instance];
}
#pragma mark - FlutterStreamHandler methods
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(nonnull FlutterEventSink)sink {
_eventSink = sink;
return nil;
}
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
_eventSink = nil;
return nil;
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
}else if ([@"startScan" isEqualToString:call.method]) {
NSDictionary *dict = call.arguments;
NSString *ipString = [dict valueForKey:@"data"];
_ipString = ipString;
[self startToScan];
}else if ([@"stopScan" isEqualToString:call.method]) {
[self stopToScan];
}else if ([@"onlyToReceive" isEqualToString:call.method]) {
[self onlyToReceive];
}
else {
result(FlutterMethodNotImplemented);
}
}
#pragma mark - ScanX3UdpSocketDelegate methods
- (void)udpSocket:(GCDAsyncUdpSocket *_Nullable)sock didReceiveData:(NSData *_Nullable)data
fromAddress:(NSData *_Nullable)address
withFilterContext:(nullable id)filterContext {
NSString *host = [GCDAsyncUdpSocket hostFromAddress:address];
NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSString *str2 = [self trimIndicatorWithString:str];
NSError *error;
NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:[str2 dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:&error];
[dict setValue:host forKey:@"host"];
NSLog(@"udpSocket didReceiveData dict = %@",dict);
if (!error) {
if(_eventSink){
_eventSink(dict);
}
}
}
- (void)startToScan {
[self stopTimer];
[self startTimer];
}
- (void)stopToScan {
[self stopTimer];
[ScanX3UdpSocket shareScanX3UdpSocket].delegate = nil;
[[ScanX3UdpSocket shareScanX3UdpSocket] cutOffUdpSocket];
}
- (void)onlyToReceive {
[ScanX3UdpSocket shareScanX3UdpSocket].delegate = self;
[[ScanX3UdpSocket shareScanX3UdpSocket] toReceiveData];
}
#pragma mark - private method
- (void)startTimer {
[self timer];
}
- (void)stopTimer {
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
- (NSString *)trimIndicatorWithString: (NSString *)string {
string = [string stringByReplacingOccurrencesOfString:@"\x02" withString:@""];
string = [string stringByReplacingOccurrencesOfString:@"\x03" withString:@""];
return string;
}
- (void)scanX3 {
[ScanX3UdpSocket shareScanX3UdpSocket].delegate = self;
[[ScanX3UdpSocket shareScanX3UdpSocket] startScanX3WithIpString:self.ipString];
}
#pragma mark - getter/setter
-(NSTimer *)timer{
if (!_timer) {
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(scanX3) userInfo:nil repeats:YES];
}
return _timer;
}
@end
安卓层的核心代码
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
/** PluginudpPlugin */
public class PluginudpPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MulticastSocket multicastSocket = null;
private InetAddress group;
private boolean isReceiving = true;/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private static EventChannel.EventSink mEventSink = null;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "pluginudp");
channel.setMethodCallHandler(this);
final EventChannel eventChannel = new EventChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "FlutterUdpEvent");
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
mEventSink = eventSink;
}
@Override
public void onCancel(Object o) {
mEventSink = null;
}
});
}
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
// plugin registration via this function while apps migrate to use the new Android APIs
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
//
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
// in the same class.
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "pluginudp");
channel.setMethodCallHandler(new PluginudpPlugin());
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
}else if(call.method.equals("startScan")) {
Map m = (Map) call.arguments;
String ipString = m.get("data");
System.out.println("ipString = " + ipString);
stop();
initMultiCaseSocket();
startScan(ipString);
receive();
}else if(call.method.equals("stopScan")) {
stop();
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
public void initMultiCaseSocket() {
try {
group = InetAddress.getByName("231.0.1.1");
} catch (UnknownHostException e) {
e.printStackTrace();
}
try {
multicastSocket = new MulticastSocket(11555);
} catch (IOException e) {
e.printStackTrace();
}
try {
multicastSocket.joinGroup(group);// 加入该组
} catch (IOException e) {
e.printStackTrace();
}
}
public void startScan(String ipString) {
isReceiving = true;
new Thread(new Runnable() {
public void run() {
int I=0;
System.out.println("run send");
while(true && i<10 && isReceiving){
String sendMessage = null;
try {
sendMessage = UDPServer("192.168.0.111","11555");
System.out.println(sendMessage);
} catch (JSONException e) {
e.printStackTrace();
}
I++;
System.out.println("i ="+ i);
DatagramPacket datagramPacket = new DatagramPacket(
sendMessage.getBytes(), sendMessage.length(), group,
21555);
try {
multicastSocket.send(datagramPacket);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
System.out.println("send over");
}
}).start();
}
public void receive() {
new Thread(new Runnable() {
public void run() {
int I=0;
byte[] arb = new byte[200];
System.out.println("run receive");
while (true && i<20 && isReceiving) {
DatagramPacket datagramPacket = new DatagramPacket(arb,
arb.length);
try {
multicastSocket.receive(datagramPacket);
Thread.sleep(1000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
I++;
String host = datagramPacket.getAddress().getHostName();
System.out.println("host====++++++" + host);
System.out.println("re i ="+ i);
System.out.println(arb.length);
Map m = changeByteToMap(arb);
m.put("host",host);
Message msg = new Message();
msg.obj = m;
msg.what = 1000;
handler.sendMessage(msg);
}
}
}).start();
}
@SuppressLint("HandlerLeak")
Handler handler = new Handler() {
public void handleMessage(Message msg) {
Map mm = (Map) msg.obj;
switch (msg.what){
case 1000:
System.out.println("=====>"+ mm);
if (mEventSink != null) {
System.out.println("send before");
mEventSink.success(mm);
System.out.println("send after");
}
break;
}
}
};
public void stop() {
isReceiving = false;
if (multicastSocket != null) {
try {
multicastSocket.leaveGroup(group);
multicastSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
byte[] startFlag = {0x02};
byte[] endFlag = {0x03};
String start = new String(startFlag);
String end = new String(endFlag);
public String UDPServer(String ip, String port) throws JSONException {
JSONObject mapBuf = new JSONObject();
mapBuf.put("type", "scan");
mapBuf.put("ip", ip);
mapBuf.put("port", port);
String value = start + mapBuf.toString() + end;
return value;
}
public Map UDPServer(String data) throws JSONException {
Map map=new HashMap();
JSONTokener jsonParser = new JSONTokener(data);
JSONObject jsonObj = (JSONObject) jsonParser.nextValue();
map.put("result",jsonObj.optBoolean("result"));
map.put("description",jsonObj.optString("description"));
Map tempMap=new HashMap();
JSONObject job=jsonObj.getJSONObject("response");
tempMap.put("mac", job.optString("mac"));
tempMap.put("name", job.optString("name"));
tempMap.put("version",job.optString("version"));
tempMap.put("netMode",job.optString("netMode"));
map.put("response",tempMap);
return map;
}
//将byte[]转换成map
public Map changeByteToMap(byte[] bytes) {
ArrayList tempList = new ArrayList<>();
boolean isStart = false;
boolean isEnd = false;
for (int i = 0; i < bytes.length; i++) {
Byte item = bytes[I];
if (item == 0x02) {
isStart = true;
continue;
}
if (isStart) {
if (item == 0x03) {
isEnd = true;
break;
}
if (!isEnd) {
tempList.add(item);
}
}
}
System.out.println("------>" + tempList.size());
Map retmap = null;
try {
retmap = UDPServer(new String(listTobyte(tempList)));
} catch (JSONException e) {
e.printStackTrace();
}
return retmap;
}
private byte[] listTobyte(List list) {
if (list == null || list.size() < 0)
return null;
byte[] bytes = new byte[list.size()];
int i = 0;
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
bytes[i] = iterator.next();
I++;
}
return bytes;
}
}
安卓里面
开始udp扫描--startScan
停止udp扫描--stopScan
只接收数据--onlyToReceive (这个暂时没有实现)
插件写好了,在需要使用的地方使用起来
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:res/constant.dart';
import 'package:utils/api_helper.dart';
import 'package:pluginudp/pluginudp.dart';
import 'utils/socket_client.dart';
import 'utils/http.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
final EventChannel _eventChannel = EventChannel('FlutterUdpEvent');
String _host = "";
@override
void initState() {
super.initState();
Pluginudp udp = Pluginudp();
Future.delayed(Duration(seconds: 2), () {
_eventChannel.receiveBroadcastStream().listen(eventListener,
onError: (Object obj) => throw obj as PlatformException);
});
}
void onData(dynamic data) {
print("data:$data");
Uint8List responseData = data;
List responseDataList = responseData.toList();
responseDataList.removeAt(0);
responseDataList.removeAt(responseDataList.length - 1);
String str = String.fromCharCodes(responseDataList);
Map map = jsonDecode(str);
print(map);
}
void eventListener(dynamic data) {
if (data.runtimeType.toString() == '(dynamic) => Null') {
return;
}
Map event = data as Map;
print("object=${event['host']}");
print("object = $event");
String mac = event["response"]['MAC'];
if (mac.contains("EF71A")) {
_host = event['host'];
print("destines host = $_host");
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Udp scan'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RaisedButton(
child: Text("start"),
onPressed: () {
Pluginudp.startScan("192.168.2.245");
}),
SizedBox(
width: 1,
),
RaisedButton(
child: Text("stop"),
onPressed: () {
Pluginudp.stopScan();
}),
SizedBox(
width: 1,
),
RaisedButton(
child: Text("onlyToReceive"),
onPressed: () {
Pluginudp.onlyToReceive();
})
],
),
SizedBox(
height: 50,
),
Column(
children: [
RaisedButton(child: Text("建立Tcp链接"), onPressed: _connectTcp),
RaisedButton(
child: Text("配置服务器信息"), onPressed: _setServerInfo),
RaisedButton(
child: Text("配置设备owner"), onPressed: _setOwnerInfo),
RaisedButton(
child: Text("配置设备时区与位置"),
onPressed: _setTimeZoneLocation),
RaisedButton(
child: Text("启动设备向服务器注册"), onPressed: _startRegister),
],
)
],
),
),
),
);
}
_connectTcp() {
OwonSocketClient.getInstance()
.startConnect(_host, OwonConstant.tcpReceivePort)
.then((value) {
OwonSocketClient.getInstance().addListener(onData);
});
}
_setServerInfo() {
// Map map = Map();
// map["username"] = "xiaoming";
// map["password"] = "123456";
//
// Map apiDict = Map();
// apiDict["type"] = "login";
// apiDict["argument"] = map;
// apiDict["sequence"] = 10000;
Map apiDict = Map();
apiDict["host"] = "192.168.0.111";
apiDict["sslport"] = 8883;
apiDict["port"] = 1883;
Map p = ApiHelper.apiHelper(
type: "wizardConfig", command: "serverCfg", argument: apiDict);
print("p = $p");
SocketClient.getInstance().sendData(p);
}
_setOwnerInfo() {
Map apiDict = Map();
apiDict["Owner"] = "86-18559697013";
apiDict["agentid"] = "Test";
Map p = ApiHelper.apiHelper(
type: "wizardConfig", command: "DevOwnerCfg", argument: apiDict);
print("p = $p");
SocketClient.getInstance().sendData(p);
}
_setTimeZoneLocation() {
Map apiDict = Map();
apiDict["Area"] = "Asia/Shanghai";
apiDict["Addid"] = "11100000";
Map p = ApiHelper.apiHelper(
type: "wizardConfig", command: "DevLocationCfg", argument: apiDict);
print("p = $p");
SocketClient.getInstance().sendData(p);
}
_startRegister() {
Map p = ApiHelper.apiHelper(
type: "wizardConfig", command: "startRegister");
print("p = $p");
SocketClient.getInstance().sendData(p);
}
}
image1.png
注意使用真机来调试,点击start按钮,控制台打印信息
image2.png
写在结尾
代码里面会有一部分是与我司的业务挂钩,大家可以忽略,这也是发布的第一篇文章,可能有诸多考虑不周的地方,请见谅,只想在flutter udp 组播 这个地方做一个抛砖引玉的作用.需要源码的话,请在[email protected]上留言吧
http://note.youdao.com/s/DnDiFfGT