从任何网络 Flutter 访问 HTTP 服务器

Accessing HTTP server from any network Flutter

我正在尝试创建一个应用程序来远程控制一些灯。 至于现在一切顺利,我可以通过按下我在应用程序上设计的按钮来控制控制灯的继电器的状态,而不管安装应用程序的设备连接的是什么网络。

我想要的是:
多个设备将安装此应用程序并连接到同一个地方(我的意思是将控制相同的灯)。因此,当一个人改变一盏灯的状态时,应用程序的所有其他实例(包括进行更改的实例)都应该更新为新的灯状态。

我想通过在应用程序内部放置一个服务器来实现这一点,这样当真实服务器发生变化时,托管在控制一堆中继的覆盆子上,可以向所有应用程序发送消息并更新它们.每次应用程序打开或 IP 更改时,应用程序都会发送其 IP 和存储在服务器上的 ID。

我面临的问题是:
在本地工作时一切正常,但当安装该应用程序的设备连接到另一个网络时,它将不再接收更新。当然,我知道这是因为,由于我使用的是 Flutter 插件,我只能获取设备的本地 IP,因此无法通过互联网访问它。我可以检索设备的 public 和私有 IP 地址,但是我不知道如何 "ask" 到路由器,"owns" public IP 让我使用私有 IP 访问设备而无需端口转发(端口转发无法完成,因为我正在使用移动设备)。

我的问题:
有没有办法实现我想要的?或者在有变化时动态更新应用程序的不同方法?或者获取 "external" 和本地 IP 然后使用它调用应用程序的方法? 提前致谢。

pubspec.yaml 依赖关系:

dependencies:
  flutter:
    sdk: flutter
  get_ip: ^0.3.0
  shared_preferences: ^0.4.3

AndroidManifest.xml 权限:

<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

Flutter服务器及ip发送方式:

void listenForUpdates()
  {
    HttpServer.bind(InternetAddress.anyIPv4, int.parse(port)).then((server)
    {
      server.listen((HttpRequest request)
      {
        MapEntry<String, String> parameter = request.uri.queryParameters.entries.single;
        print(parameter);
        setState(() {
          lightsOn[int.parse(parameter.key) - 1] = parameter.value.contains('true');
          noResponse = false;
        });
        request.response.close();
      });
    });
  }

void initPlatformState(bool resync) async
  {
    String ipAddress;
    ipAddress = await GetIp.ipAddress;
    HttpClient client = new HttpClient();
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState((){ deviceId = resync ? "new" : (prefs.getString('deviceId') ?? "new"); });

    client.postUrl(Uri.parse('http://NameOfTheServer:Port/?q=device&ip=' + ipAddress + ':' + port + '&id=' + deviceId))
    .catchError((onError)
    {
      setState((){ noResponse = true; });
    })
    .then((HttpClientRequest request)
    {
      request.headers.set(HttpHeaders.userAgentHeader, 'LumenApp - Dart:Flutter');
      return request.close();
    })
    .then((HttpClientResponse response)
    {
      response.transform(utf8.decoder).listen((contents) async
      {
        if(response.statusCode == 200)
        {
          setState((){
            deviceId = contents.split('|')[1];
            noResponse = false;
          });
          await prefs.setString('deviceId', deviceId);

        }
        else
          setState((){ noResponse = true; });
      });
    });
}

类 处理来自应用程序(Raspberry,Java)的请求的方法:

@Override
    public void run()
    {
        System.out.println("Successfully started thread with ThreadId " + this.threadId + ".");

        if(http.res.getCurrentResponse().size() > 0)
        {
            http.res.send();
        }
        else
        {
            http.res.setHeaders(http.req.getHttpVersion(), 200);

            try
            {
                String qParam = http.req.getParameterValueByName("q");
                String deviceId = http.req.getParameterValueByName("id");
                Boolean devPerm = AppUpdater.getDevicesId().contains(deviceId);
                if(!devPerm && !deviceId.equalsIgnoreCase("new"))
                {
                    http.res.setBody("ID EXPIRED");
                }
                else
                {
                    if(qParam.equalsIgnoreCase("device"))
                    {
                        String deviceIp = http.req.getParameterValueByName("ip");
                        String toDelete = http.req.getParameterValueByName("delete");
                        if(toDelete == null)
                        {
                            toDelete = "false";
                        }
                        Integer id = AppUpdater.updateDevicesIp(deviceId, deviceIp, Boolean.parseBoolean(toDelete));
                        http.res.setBody("DeviceId|" + id);
                    }
                    else if(Integer.parseInt(qParam) >= 0 && devPerm)
                    {
                        Integer lParam = Integer.parseInt(http.req.getParameterValueByName("l"));
                        gpioHandler.changeGpiosState(qParam, lParam);
                    }
                    else if(qParam.equals("-1") && devPerm)
                    {
                        HashMap<Integer, Boolean> gpiosState = gpioHandler.getGpiosState();
                        http.res.setBody(qParam);
                        for(HashMap.Entry<Integer, Boolean> gpio : gpiosState.entrySet())
                        {
                            http.res.addBody("|" + gpio.getKey().toString() + "-" + gpio.getValue().toString());
                        }
                    }
                }
            }
            catch(RuntimeException e)
            {
                e.printStackTrace();
                System.err.println("One or more required parameters were missing.");
                http.res.setHeaders(http.req.getHttpVersion(), 400);
            }
            finally
            {
                http.res.send();
            }
        }
public class AppUpdater
{
    static final GpioHandler gpioHandler = new GpioHandler();

    static HashMap<String, String> devices = new HashMap<String, String>();

    public static Integer updateDevicesIp(String deviceId, String deviceIp, boolean toDelete)
    {
        Integer id = 0;
        if(toDelete)
        {
            devices.remove(deviceIp);
        }
        else if(deviceId.equalsIgnoreCase("new"))
        {
            id = devices.size() + 1;
            devices.put(id.toString(), deviceIp);
        }
        else if(devices.containsKey(deviceId))
        {
            devices.replace(deviceId, deviceIp);
            id = Integer.parseInt(deviceId);
        }
        return id;
    }

    public static void notifyApp(String lightIndex, String lightStatus) throws IOException
    {
        for(HashMap.Entry<String, String> device : devices.entrySet())
        {
            String urlParameters = "?" + lightIndex + "=" + lightStatus;
            URL url = new URL("http://" + device.getValue() + urlParameters);
            System.out.println(url);
            url.openStream();
        }
    }

    public static ArrayList<String> getDevicesId()
    {
        ArrayList<String> devicesId = new ArrayList<String>();
        for(HashMap.Entry<String, String> device : devices.entrySet())
        {
            devicesId.add(device.getKey());
        }
        return devicesId;
    }

    public static HashMap<String, String> getDevices()
    {
        return devices;
    }
}
public void gpioListener()
    {
        for(GpioPinDigitalOutput pin : gpios)
        {
            pin.addListener(new GpioPinListenerDigital() {
                @Override
                public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event)
                {
                    String gpioName = event.getPin().getName();
                    System.out.println(" --> GPIO PIN STATE CHANGE: " + gpioName + " = " + event.getState());
                    try
                    {
                        String lightStatus = event.getState().toString().equals("HIGH") ? "true" : "false";
                        String pinIndex = pinIndexes.get(gpioName.substring(gpioName.length() - 1));
                        AppUpdater.notifyApp(pinIndex, lightStatus);
                    }
                    catch(IOException e)
                    {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

欢迎使用 Whosebug。

你描述了一些在工业自动化或家庭自动化领域很常见的东西,一个或几个设备监控一个machine/system的状态,在你的情况下它们只是几盏灯,但系统通常配备PLC(可编程逻辑控制器),它也执行程序。

A Raspberry PI 可以作为一个可编程的 PLC 与非常强大的 Codesys 软件和 free/cheap。

许多人用它来构建基于 Raspberry 的小型自动化或机器人。

您还可以使用标准协议(例如 Modbus 和 OPC UA)与 PLC 进行通信。

这里是一个 android 可以通过这些协议进行监控的应用程序:https://www.suppanel.com/index.php/en/

虽然我承认我的提议是进入一个非常不同的编程领域

我找到了解决方案。 网络套接字。
我在 Node JS 中写了一个 "double" 服务器,在 Java 中写了另一个服务器,这次 Java 服务器没有以前那么复杂了。 我使用 NodeJs 是因为在 Java 中实现 websockets 很痛苦。 我称它为 "double server" 是因为我没有找到(也没有搜索)通过简单的 Java ServerSocket 与 NodeJs WebSocket 进行通信的方法,所以我还在同一个 NodeJs 文件中创建了一个小的 HTTP 服务器。 我使用 flutter websocket 教程修改了应用程序(使用 web_socket_channel 插件)。 这也让应用程序变得非常简单。

所以,总结一下:
我在 NodeJs 中使用 ws 库为 WebSocket 服务器编写代码,在 NodeJs 中也为 HTTP 服务器编写代码,使用 expresshttp 库。 我自己编写了 Java 类 来处理 HTTP 连接,我使用 Pi4j 来处理我的 RaspberryPi 的引脚。 该应用程序是使用 Flutter 框架在 Android Studio 中编写的,仅具有 web_socket_channel 依赖项。

过程是:
1) 应用程序连接到 WebSocket 服务器。
2) WebSocket 服务器从文件中读取并将当前中继的状态发送到新连接的应用程序。
3) 当应用程序请求更改中继状态时,请求通过 WebSocket 发送并使用 NodeJs http 库重定向到 Java 服务器。
4) Java 服务器进行所需的更改,使用新中继的状态和 returns 200 HTTP 响应更新文件。
5) 变化被 Pi4j 库提供的 GPIO Listener 捕获,并作为字符串发送到 NodeJs HTTP Express 服务器。
6) NodeJs HTTP Express 服务器通知 WebSocket 服务器将更改通知所有应用程序。
7)只要有变化,应用程序就会成功更新!