寫這篇文章的緣由:在Android原生端鸣峭,iOS原生端,實現(xiàn)udp 組播相信很多小伙伴都會的酥艳,但在flutter 端實現(xiàn)這個對我們來說還是一個未知之?dāng)?shù)吧摊溶;
前期是想著去網(wǎng)上找一個第三方庫 或者找一個第三方的插件 來完成任務(wù);結(jié)果經(jīng)過幾天的調(diào)研下來充石,最終都是Android手機上可以正常使用莫换,但在iOS手機上都不能正常使用,報以下的錯誤
Unhandled Exception: OS Error: Can't assign requested address, errno = 49
沿著這個錯誤跟蹤下去 ,網(wǎng)上討論的結(jié)果貌似是Dart SDK層報出來的錯誤拉岁,很多熱心人事也與Dart團(tuán)隊在網(wǎng)上溝通坷剧,希望他們能解決此bug;不過大半年過去了喊暖,這個問題依舊存在惫企。
看來指望Dart團(tuán)隊解決此事,有點遙遙無期的感覺陵叽,怎么辦呢狞尔,我們不能因為這個bug ,就停止不前了吧巩掺;最終決定自己來造輪子----寫udp組播插件偏序;
寫flutter插件入門,在這里就不多說了锌半,小伙伴們自己去探索一下吧。下面就開始今天真正的代碼環(huán)節(jié)了
import 'dart:async';
import 'package:flutter/services.dart';
class Pluginudp {
static const MethodChannel _channel =
const MethodChannel('pluginudp');
static Future<void> startScan(String ipString) async {
await _channel.invokeMethod('startScan',{"data":ipString});
}
static Future<void> stopScan() async {
await _channel.invokeMethod('stopScan');
}
static Future<void> onlyToReceive() async {
await _channel.invokeMethod('onlyToReceive');
}
}
上面的代碼是flutter層寇漫, 定義了3個方法刊殉,分別為
開始udp掃描--startScan
停止udp掃描--stopScan
只接收數(shù)據(jù)--onlyToReceive
iOS層的核心代碼
#import "PluginudpPlugin.h"
#import "ScanX3UdpSocket.h"
@interface PluginudpPlugin ()<FlutterStreamHandler,ScanX3UdpSocketDelegate>
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) FlutterEventSink eventSink;
@property (nonatomic, copy) NSString *ipString;
@end
@implementation PluginudpPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)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<NSString*,id> *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<String,String> m = (Map<String, String>) 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<String, Object> 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<String, Object> mm = (Map<String, Object>) 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<String,Object> UDPServer(String data) throws JSONException {
Map<String,Object> map=new HashMap<String,Object>();
JSONTokener jsonParser = new JSONTokener(data);
JSONObject jsonObj = (JSONObject) jsonParser.nextValue();
map.put("result",jsonObj.optBoolean("result"));
map.put("description",jsonObj.optString("description"));
Map<String,Object> tempMap=new HashMap<String,Object>();
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[]轉(zhuǎn)換成map
public Map<String,Object> changeByteToMap(byte[] bytes) {
ArrayList<Byte> 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<String, Object> retmap = null;
try {
retmap = UDPServer(new String(listTobyte(tempList)));
} catch (JSONException e) {
e.printStackTrace();
}
return retmap;
}
private byte[] listTobyte(List<Byte> list) {
if (list == null || list.size() < 0)
return null;
byte[] bytes = new byte[list.size()];
int i = 0;
Iterator<Byte> iterator = list.iterator();
while (iterator.hasNext()) {
bytes[i] = iterator.next();
I++;
}
return bytes;
}
}
安卓里面
開始udp掃描--startScan
停止udp掃描--stopScan
只接收數(shù)據(jù)--onlyToReceive (這個暫時沒有實現(xiàn))
插件寫好了,在需要使用的地方使用起來
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<MyApp> {
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<int> 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<dynamic, dynamic> event = data as Map<dynamic, dynamic>;
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: <Widget>[
Text('Udp scan'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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: <Widget>[
RaisedButton(child: Text("建立Tcp鏈接"), onPressed: _connectTcp),
RaisedButton(
child: Text("配置服務(wù)器信息"), onPressed: _setServerInfo),
RaisedButton(
child: Text("配置設(shè)備owner"), onPressed: _setOwnerInfo),
RaisedButton(
child: Text("配置設(shè)備時區(qū)與位置"),
onPressed: _setTimeZoneLocation),
RaisedButton(
child: Text("啟動設(shè)備向服務(wù)器注冊"), 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<String, dynamic> 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);
}
}
注意使用真機來調(diào)試州胳,點擊start按鈕记焊,控制臺打印信息
寫在結(jié)尾
代碼里面會有一部分是與我司的業(yè)務(wù)掛鉤,大家可以忽略栓撞,這也是發(fā)布的第一篇文章遍膜,可能有諸多考慮不周的地方,請見諒瓤湘,只想在flutter udp 組播 這個地方做一個拋磚引玉的作用.需要源碼的話瓢颅,請在zz7516018@163.com上留言吧
http://note.youdao.com/s/DnDiFfGT