2024年2月

一、简介

MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件 。

MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。

MQTT协议在物联网中应用广泛,下面使用插件式集成到IoTBrowser平台,提供JS API
即可发布broker和客户端实现发布、订阅等功能。

二、 开发插件

  1. 添加引用
    1. 添加MQTTNet,在NuGet搜索MQTTNet
    2. 添加Core,路径:\IoTBrowser\src\app_x64\Core.dll
    3. 添加Infrastructure,路径:\IoTBrowser\src\app_x64\Infrastructure.dll
    4. 添加Newtonsoft,路径:\IoTBrowser\src\app_x64\Newtonsoft.Json.dll
  2. 开发MqttHostCom和MqttClientCom插件
    1. MqttHostCom 服务端
      broker
usingDDS.IoT.Com;usingSystem;usingSystem.Collections.Generic;usingSystem.IO.Ports;usingSystem.Linq;usingSystem.Runtime.InteropServices;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;namespaceDDS.IoT.Mqtt
{
public classMqttHostCom : ComBase
{
public override string Type => "mqttHostCom";public override string Name => "Mqtt主机";privateMqttHostService hostService;public override bool Init(int port, int baudRate = 9600, string extendData = null)
{
this.Port =port;
hostService
= newMqttHostService();
hostService.PushId
= this.Id;
hostService.StartAsync(extendData, OnPushData);
Console.WriteLine(
"初始化MqttHostCom驱动程序成功!");return true;
}
public override eventPushData OnPushData;public override boolOpen()
{
var b = false;try{
b
= true;
IsOpen
= true;
}
catch(Exception ex)
{
string msg = string.Format("MqttHostCom串口打开失败:{0}", ex.Message);
Console.WriteLine(msg);
}
returnb;
}
public override boolClose()
{
hostService.Dispose();
hostService
= null;
IsOpen
= false;
OnPushData
= null;return true;
}
public override string Command(string name, stringdata)
{
var outData = string.Empty;returnoutData;
}
}
}

2.
MqttClientCom 客户端

实现发布和订阅接口

usingDDS.IoT.Com;usingSystem;usingSystem.Collections.Generic;usingSystem.IO.Ports;usingSystem.Linq;usingSystem.Runtime.InteropServices;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;namespaceDDS.IoT.Mqtt
{
public classMqttClientCom : ComBase
{
public override string Type => "mqttClientCom";public override string Name => "Mqtt客户端";privateMqttClientService mqttClientService;public override bool Init(int port, int baudRate = 9600, string extendData = null)
{
mqttClientService
= newMqttClientService();
mqttClientService.PushId
= this.Id;this.Port =port;
mqttClientService.MqttClientStart(extendData,
this.OnPushData);
Console.WriteLine(
"初始化MqttClientCom驱动程序成功!");return true;
}
public override eventPushData OnPushData;public override boolOpen()
{
var b = false;try{
mqttClientService.Open();
b
= true;
IsOpen
= true;
}
catch(Exception ex)
{
string msg = string.Format("MqttClientCom串口打开失败:{0}", ex.Message);
Console.WriteLine(msg);
}
returnb;
}
public override boolClose()
{
mqttClientService.Close();
mqttClientService
= null;
IsOpen
= false;
OnPushData
= null;return true;
}
public override string Command(string name, stringdata)
{
var outData = string.Empty;var dataObj = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(data);switch(name)
{
case "Publish":string topic =dataObj.topic;string payload =dataObj.data;int? level =dataObj.level;bool? retain =dataObj.retain;if (!level.HasValue)
{
level
= 1;
}
if (!retain.HasValue)
{
retain
= false;
}
outData
=mqttClientService.Publish(topic, payload, level.Value, retain.Value).ToString();break;case "Subscribe":
topic
=dataObj.topic;
level
=dataObj.level;if (!level.HasValue)
{
level
= 0;
}
outData
=mqttClientService.Subscribe(topic, level.Value).ToString();break;
}
returnoutData;
}
}
}

3.前端测试

<!DOCTYPE HTML PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">
<html>
<headlang="en">
    <title>Mqtt</title>
    <metacharset="UTF-8">
    <metahttp-equiv="X-UA-Compatible"content="IE=edge">
    <metaname="viewport"content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <metaname="format-detection"content="telephone=no">
    <!--Set render engine for 360 browser-->
    <metaname="renderer"content="webkit">
    <!--No Baidu Siteapp-->
    <!--<meta http-equiv="Cache-Control" content="no-siteapp" />-->
    <METAHTTP-EQUIV="pragma"CONTENT="no-cache">
    <METAHTTP-EQUIV="Cache-Control"CONTENT="no-cache, must-revalidate">
    <METAHTTP-EQUIV="expires"CONTENT="0">
    <linkrel="alternate icon"type="image/png"href="favicon.png">
    <linkrel="stylesheet"href="/scripts/amazeui/amazeui.min.css" />
    <linkrel="stylesheet"href="../css/main.css" />
    <scriptsrc="/scripts/jquery-3.3.1.min.js"></script>
    <scriptsrc="/scripts/amazeui/amazeui.min.js"></script>

    <scriptsrc="/scripts/jquery.signalR-2.4.1.min.js"></script>
    <style>.list{width:1000px !important;
        }.am-form{width:100%;background:#fff;
        }.refresh-port{width:80px !important;height:30px !important;
        }#msg, #msgWrite{clear:both;
        }.am-u-sm-4{padding:3px;
        }
    </style>
    <scripttype="text/javascript">
        varhostid;//主机id
        varclientid;//客户端id

        functionstartHost() {varargs=$('#txtHostArgs').val();
dds.iot.com.open({
type:
'mqttHostCom',//mqtt主机 port:1,
baudRate:
1,
extendData: args,
//extendData: JSON.stringify({ server: "*", port: 1883 }), onReceive:function(res) {
addMsg(
'host:' +JSON.stringify(res.data))
console.log(
'host', res.data)
},
onOpen:
function(ar) {if(ar.Success) {
hostid
=ar.Data;
addMsg(
'连接成功!')
}
else{
alert(ar.Message)
}
}
})
}
functioncloseHost() {
dds.iot.com.close(hostid)
}
functionstartClient() {varargs=$('#txtClientArgs').val();
dds.iot.com.open({
type:
'mqttClientCom',//mqtt客户端 port:1,
baudRate:
1,
extendData: args,
//extendData: JSON.stringify({ server: "localhost", port: 1883, clientid: "1", username: "", password:""}), onReceive:function(res) {
addMsg(
'client:' +JSON.stringify(res.data))
console.log(
'client',res.data)
},
onOpen:
function(ar) {if(ar.Success) {
clientid
=ar.Data;
addMsg(
'连接成功!')
}
else{
alert(ar.Message)
}
}
})
}
functionsubscribe() {vartopic=$('#txtTopic').val();
dds.iot.com.exeCommand({ id: clientid, name:
"Subscribe", data: { topic: topic, level:0} },function(ar) {if(ar.Success) {
addMsg(
'订阅成功!')
}
else{
addMsg(
'操作失败:' +ar.Message)
}
})
}
functionpublish() {vartopic=$('#txtTopic').val();varcontents=$('#txtContents').val();
dds.iot.com.exeCommand({ id: clientid, name:
"Publish", data: { topic: topic, data: contents } },function(ar) {if(ar.Success) {
addMsg(
'发布成功!')
}
else{
addMsg(
'操作失败:' +ar.Message)
}
})
}
functioncloseClient() {
dds.iot.com.close(clientid)
}
var$msg;functionaddMsg(msg) {
$msg.val($msg.val()
+"\n"+msg);
}
functionclearLog() {
$msg.val(
'');
}
//窗口初始化事件(操作窗口大小、标题) $(document).bind('dds.window.init',function(e, win) {
$msg
=$("#msg");
})
</script> </head> <body> <divclass="fun_bd"style="padding:10px;"> <formclass="am-form"> <h3>数据读取</h3> <fieldset> <divclass="am-form-group"> <labelfor="doc-ipt-email-1"class="am-u-sm-4">主机参数</label> <divclass="am-u-sm-6"> <inputid="txtHostArgs"type="text"value='{server: "*", port: 1883 }'/> </div> <buttononclick="startHost()"class="am-btn-primary"type="button">启动主机</button> <buttononclick="closeHost()"class="am-btn-danger"type="button">关闭主机</button> </div> <divclass="am-form-group"> <labelfor="doc-ipt-email-1"class="am-u-sm-4">客户端参数</label> <divclass="am-u-sm-6"> <inputid="txtClientArgs"type="text"value='{server: "localhost", port: 1883, clientid: "1", username: "", password:""}'/> </div> <buttononclick="startClient()"class="am-btn-primary"type="button">启动客户端</button> <buttononclick="closeClient()"class="am-btn-danger"type="button">关闭客户端</button> </div> <divclass="am-form-group"> <labelfor="doc-ipt-email-1"class="am-u-sm-4">主题</label> <divclass="am-u-sm-6"> <inputid="txtTopic"type="text"value="/dds/iot/mqtt/test" /> </div> <buttononclick="subscribe()"class="am-btn-primary"type="button">订阅主题</button> </div> <divclass="am-form-group"> <labelfor="doc-ipt-email-1"class="am-u-sm-4">主题内容</label> <divclass="am-u-sm-6"> <inputid="txtContents"type="text"value="测试测试" /> </div> <buttononclick="publish()"class="am-btn-primary"type="button">发布主题</button> </div> <!--<div id="msg"></div>--> <textareaid="msg"rows="18"></textarea> <divclass="am-form-group"> <buttononclick="clearLog()"class="am-btn-default"type="button">清除日志</button> </div> </fieldset> </form> </div> </body> </html>

代码地址:https://gitee.com/yizhuqing/IoTBrowser

基于Chromium内核使用H5快速开发工控系统界面,使用JS API前端人员既可以完成界面展示与硬件控制。系统自带串口、RFID、电子秤等硬件协议支持,并且支持二次定制开发。可以用来开发人机界面(HMI)或数据采集与监督控制系统(SCADA) 。 使用H5或Vue可以本地打包离线应用,也可以在线加载Web网页来控制设备硬件。

问题描述

近期家里更新了千兆宽带,电信光猫换成了中兴 F7010C,默认光猫拨号,安装人员不给改桥接,而且路由器还获取不到 IPv6 地址

尝试过对路由器的 IPv6 进行配置,但是发现无论怎么配置,都无法获取到 IPv6 地址,并且也尝试了设备直连光猫,也无法获取到 IPv6 地址,因此怀疑是光猫的问题,所以才有了这一段折腾

主要参考了网上的一些资料,个人根据自己的情况和理解做了一些修改

解锁超管密码

网上给出的解锁 F7010C 的解锁方式都是通过对电信的官方 APP《小翼管家》进行抓包,然后借助抓包软件的断点和重写功能,来修改请求的内容,从而获取超管密码

前言

现在对安卓的 APP 进行抓包,其实还是比较困难的。
首先当前的网络包基本都是 HTTPS 加密的,为了能够抓到 HTTPS 的包,需要通过中间人攻击的方式来查看 HTTPS 的明文内容

当客户端与抓包软件的代理服务器进行通信时,抓包软件需要重签远程服务器的 SSL 证书。
为了保证客户端与抓包软件成功进行 SSL 握手通信,需要将抓包软件的 CA 根证书安装到客户端本地的证书管理中心

但是安卓 7.0 之后,由于 Google 的安全策略,用户自行安装的用户证书不再被信任,因此需要进行额外的配置

配置安卓抓包环境

由于自行安装的用户证书不再被信任,但是系统证书还是可以被信任的,因此需要将抓包软件的 CA 根证书导入到系统证书中

但是系统证书通常在
/system
目录下,而
/system
目录是只读的,因此需要先将
/system
目录挂载为可读写的目录,然后再将证书导入到系统证书中

这意味着需要 root 权限,所以个人改用安卓模拟器来进行抓包。
安装模拟器的过程就不再赘述了,个人选择了 MuMu 模拟器

首先在设置中开启 root 权限和 system 读写权限

MuMu-开启root权限

MuMu-开启system读写权限

抓包工具选择了
Reqable
,这是一款国人开发的抓包工具,有中文界面,并且支持断点和重写功能,而且免费的社区版功能也足够使用了

安装证书的方式,Reqable 给了非常详细的图文教程,主要的不同在于需要对
/system
重新挂载,然后再通过
adb
将证书导入到系统证书中

# 下载证书 reqable-ca.crt

# 通过 adb 连接模拟器,端口号可以在模拟器的【问题诊断】中查看
adb connect 127.0.0.1:xxxxx

adb shell
# 切换到 root 用户,注意需要在模拟器中确认
su
# 重新挂载 /system 目录
mount -o remount,rw /system
# 退出 root 用户

exit
# 退出 adb shell
exit

# 将证书导入到系统证书中
adb push reqable-ca.crt /system/etc/security/cacerts/xxxxx.0

抓包获取超管密码

配置好 CA 证书之后,就可以开始尝试抓包了,流程如下:

  1. 先打开《小翼管家》APP,绑定好电信光猫,先点进去【网关设置】界面
    小翼管家-网关设置

  2. 配置 WiFi 的代理后,在抓包软件中启动抓包,然后在《小翼管家》中切换【指示灯】

  3. 查看抓到的包中,找到形如
    https://xxxx.189cube.com/device/api?token=xxxx
    的请求,并配置断点
    Reqable-找请求
    Reqable-配置断点

  4. 再次切换【指示灯】,请求就会在断点停下,复制下面的请求内容,并执行(如果手速慢了,可能会导致请求超时,再次切换【指示灯】即可)
    Reqable-替换请求内容

    {
       "Params": [],
       "MethodName": "GetTAPasswd",
       "RPCMethod": "CallMethod",
       "ObjectPath": "/com/ctc/igd1/Telecom/System",
       "InterfaceName": "com.ctc.igd1.SysCmd",
       "ServiceName": "com.ctc.igd1"
    }
    
  5. 查看返回的结果,其中一段看似随机的字符串就是超管密码
    Reqable-获取超管密码

IPv6 配置

由于设备直连都无法获取 IPv6 地址,但是光猫自身又有 IPv6 地址,并且可以访问 IPv6 网络,因此怀疑是光猫的 IPv6 分配有问题

光猫拨号

在【网络-网络设置-网络连接】中没有做任何修改,维持了光猫拨号的方案,如图

光猫-网络-网络设置-网络连接

IP模式: IPv4&IPv6

IPv6信息: 
   地址获取方式: AutoConfigured
   获取前缀: √
   前缀获取方式: PrefixDelegation
   DNS获取方式: DHCPv6

改用 SLAAC

在【网络-用户侧管理-IPv6设置】中,关闭 DHCPv6,改用 SLAAC 方式,如图

光猫-网络-用户侧管理-IPv6设置

启动DHCPv6服务器: X

slaac前缀使能: √

二者都是 IPv6 网络中用于为主机分配 IPv6 地址的协议,但它们的工作方式、类型和功能略有不同。具体差别就不再赘述了,可以参考
slaac vs dhcpv6

路由器配置

路由器型号还是之前的红米 AC2100,不过刷了 ImmortalWrt 的最新版(23.05.1),仅作参考

wan6 配置

首先要在配置下 wan6 接口,没有的话就添加一个,如图

ImmortalWRT-接口-wan6-常规

接口: wan6
协议: DHCPv6 客户端
设备: wan

【高级设置】中,保持默认配置

ImmortalWRT-接口-wan6-高级

委托 IPv6 前缀: √
IPv6 分配长度: 已禁用

【防火墙设置】选择和 wan 接口一样的防火墙,如图

ImmortalWRT-接口-wan6-防火墙

【DHCP 服务器】中,默认关闭

ImmortalWRT-接口-wan6-DHCP

wan 配置

wan 接口的【高级设置】中,保持默认配置

ImmortalWRT-接口-wan-高级

委托 IPv6 前缀: √
IPv6 分配长度: 已禁用

【DHCP 服务器 - 常规设置】选中
忽略此接口

ImmortalWRT-接口-wan-DHCP-常规

【DHCP 服务器 - IPv6 设置】中,勾上
指定的主接口
,并且全部选择
中继模式

ImmortalWRT-接口-wan-DHCP-IPv6

lan 配置

最后在 lan 接口的【DHCP 服务器 - IPv6 设置】中,也全部选择
中继模式
,就完事了,之后重启路由器就 ok 了

ImmortalWRT-接口-lan-DHCP-IPv6

验证

可以在下面的网站中验证 IPv6 是否配置成功

参考资料

本文作者:
ywang_wnlo
本文链接:
https://ywang-wnlo.github.io/posts/6f8b7b82/
版权声明:
本博客所有文章除特别声明外,均采用
BY-NC-SA
许可协议。转载请注明出处!

在我本人研究Yarp的时候经常用于公司项目的业务网关代理,这时候就个大佬问我是否可以实现动态加载HTTPS证书?那时候我说不太可能实现,然而在某一天我看到

微软使用Yarp代替了Nginx吞吐量提升了百分之八十!

这个时候我就萌生了自己使用yarp造一个Gateway的项目,应为我本身也经常使用nginx作为网关,但是nginx的使用总得写conf,然后重启我的nginx,并且还需要配置证书重启,我就在想是否可以实现界面管理,并且完全动态管理?

然后我们的FastGateway项目就诞生了,下面我们介绍一下我们的FastGateway的项目简单案例

FastGateway

我们的FastGateway提供了俩个容器,一个是代理服务,一个是前端服务

一般简单使用就可以直接使用我们的docker-compose构建,非常简单

FastGateway支持哪些功能?

  • 登录授权


    • 通过环境变量简单设置账号密码
  • 动态路由管理


    • 完全界面操作并且实时更新
  • 动态配置证书管理


    • 完全界面操作并且实时更新
  • dashboard监控


    • 提供了简单数据监控,后续还会提供更完善的功能
  • 静态文件服务代理


    • 使用中间件简单实现了静态文件代理(Yarp默认是不支持静态文件代理)
  • 穿透隧道功能


    • 只需要提供一个公网的Gateway服务端,然后下载我们的Gateway的客户端就可以将客户端的局域网的服务映射到外网(只支持http协议的服务)
  • 出入口流量监控


    • 基于Kestrel实现出入口流量监控,还提供当前服务器的所有网卡的出入流量实时监控

技术栈

后端技术栈

  • Asp.Net 8.0 用于提供基础服务
  • Yarp 用于提供反向代理服务
  • FreeSql 用于提供数据库服务
  • JWT 用于提供登录授权服务
  • MiniApis 提供WebApi服务

前端技术栈

  • reset-css 用于重置浏览器默认样式
  • axios 用于发送http请求
  • semi 用于提供基础组件
  • react-router-dom 用于路由管理

镜像执行指令

mkdir data
docker run -d --restart always --name gateway-api -p 8000:8000 -p 8200:8080 -p 8300:8081 -v $(pwd)/data:/data/ registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api
docker run -d --restart always --privileged --name gateway-web -p 10800:80 -e api_url=http://localhost:8000 registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web

Docker-Compose文件

services:
  gateway-api:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api
    restart: always
    container_name: gateway-api
    environment:
      USER: root
      PASS: Aa010426.
    ports:
      - 8000:8000 # 提供给web端调用的管理接口
      - 8200:8080 # Http代理端口
      - 8300:8081 # Https代理端口
    volumes:
      - ./data:/data/ # 请注意手动创建data目录,负责在Linux下可能出现权限问题导致无法写入

  gateway-web:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web
    restart: always
    build:
      context: ../web
      dockerfile: Dockerfile
    privileged: true
    environment:
      api_url: http://localhost:8000
    ports:
      - 10800:80

如果并没有提供账号密码则默认

账号:root

密码:Aa010426.

替换默认的https证书

由于需要使用https,为了方便系统默认提供了一个pfx证书,如果你需要提供的话可以按照以下操作进行,如果是Docker执行的话证书的目录则是
/app/certificates/gateway.pfx

services:
  gateway-api:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api
    restart: always
    container_name: gateway-api
    ports:
      - 8000:8000 # 提供给web端调用的管理接口
      - 8200:8080 # Http代理端口
      - 8300:8081 # Https代理端口
    environment:
      USER: root
      PASS: Aa010426.
      HTTPS_PASSWORD: dd666666
      HTTPS_FILE: gateway.pfx
    ports:
      - 8200:8080
    volumes:
      - ./data:/data/
      - ./app/certificates:/app/certificates

  gateway-web:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web
    restart: always
    container_name: gateway-web
    privileged: true
    environment:
      api_url: http://localhost:8000
    ports:
      - 10800:80

参考上面的docker-compose文件,我们提供了俩个环境变量
HTTPS_PASSWORD

HTTPS_FILE

HTTPS_FILE

  • 在系统中已经指定了容器的
    /app/certificates
    目录,你只想要挂在目录中的文件名即可

HTTPS_PASSWORD

  • Pfx证书的密码,如果修改了证书请填写证书的密码。

/app/certificates

  • 这个是系统证书默认存放目录,如果映射了目录则需要提供自己的证书。

使用隧道

services:
  gateway-api:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api
    restart: always
    container_name: gateway-api
    environment:
      USER: root
      PASS: Aa010426.
      HTTPS_PASSWORD: dd666666
      TUNNEL_PASSWORD: dd666666
      HTTPS_FILE: gateway.pfx
    ports:
      - 8000:8000 # 提供给web端调用的管理接口
      - 8200:8080 # Http代理端口
      - 8300:8081 # Https代理端口
    volumes:
      - ./data:/data/
      - ./app/certificates:/app/certificates

  gateway-web:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web
    restart: always
    container_name: gateway-web
    privileged: true
    environment:
      api_url: http://localhost:8000
    ports:
      - 10800:80

增加
TUNNEL_PASSWORD
环境变量,默认为空不设置密码

下载隧道客户端
https://gitee.com/hejiale010426/Gateway/releases
然后解压压缩包,打开appsettings.json文件修改Tunnel节点的Url,如果Gateway使用了TUNNEL_PASSWORD,那么你的URL应该是
https://localhost:8081/api/gateway/connect-h2?host=backend1.app&password=dd666666

host
是在集群中的集群端点的域名,这个域名就是定义到我们的隧道客户端的
host
的这个参数,请保证值的唯一性,当绑定集群的路由匹配成功以后则会访问图片定义的端点,如果并没有存在节点那么他会直接代理。

出入流量监控

使用环境变量控制是否启用流量监控,使用环境变量
ENABLE_FLOW_MONITORING
设置我们是否启用流量监控,如果为空则默认启动流量监控,然后可以打开我们的控制面板查看流量监控的数据。

services:
  gateway-api:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api
    restart: always
    container_name: gateway-api
    environment:
      USER: root
      PASS: Aa010426.
      HTTPS_PASSWORD: dd666666
      HTTPS_FILE: gateway.pfx
      ENABLE_FLOW_MONITORING: true
    ports:
      - 8000:8000 # 提供给web端调用的管理接口
      - 8200:8080 # Http代理端口
      - 8300:8081 # Https代理端口
    volumes:
      - ./data:/data/
      - ./app/certificates:/app/certificates

  gateway-web:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web
    restart: always
    container_name: gateway-web
    privileged: true
    environment:
      api_url: http://localhost:8000
    ports:
      - 10800:80

效果图

开源地址:
FastGateway: FastGateway 一个超级简单方便的网关,基于Kestrel+Yarp实现的网关。 支持动态配置路由,支持动态配置集群,支持动态配置HTTPS证书,无需重启即可使用。 (gitee.com)

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。

周刊全文:
https://pythoncat.top/posts/2024-02-03-weekly

!!本期有赠书福利,送书5本,请阅读原文了解参与方式!!

以下是本期摘要:


写在开头


《深入剖析Java中的反射,由浅入深,层层剥离!》
这篇文章中我们讲反射时,曾提到过Java的动态代理中使用了反射技术,那么好,今天我们要就着反射的索引,来学习一下Java中的代理!

代理模式

在Java中有多达23种的设计模式(后面Java基础更新完后,会找个时间详细的去写写这些设计模式),恰当的设计模式的使用能够提升代码的效率,简化代码的复杂性。

而今天我们要说的
代理模式
就是其中之一,所谓代理是为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。

大白话
:买房的(客户方),房产销售(代理方),卖房的(委托方)

在Java中有静态代理和动态代理两种实现方式,继续放下看!!!

静态代理

所谓静态代理,一般是针对编译期就已经完成了接口,实现类,代理类的定义,我们对目标对象的增强需要手工去完成,一个目标对象就要有个代理类,非常不灵活。

静态代理的实现步骤

1,因为代理类与被目标对象有相似的行为(共同),所以我们先创建一个接口。

public interface SaleHouse {
     String saleHouse();
}

2,提供接口的实现类,当做目标对象

public class SaleHouseImpl implements SaleHouse{
    @Override
    public String saleHouse() {
        return "我要卖房子啦!!!";
    }
}

3,代理类同样也要实现接口,并在目标方法前后做一些控制操作

public class SaleHouseProxy implements SaleHouse{

    private SaleHouse saleHouse;
	//提供一个包含目标对象的有参构造
    public SaleHouseProxy(SaleHouse saleHouse) {
        this.saleHouse = saleHouse;
    }

    @Override
    public String saleHouse() {
        //调用方法前,我们可以加一些自己的控制
        System.out.println("我要收代理费!!!");
        System.out.println("--------------------");
        String s = saleHouse.saleHouse();
        System.out.println(s);
        System.out.println("--------------------");
        //调用方法后,我们依旧可以操作
        System.out.println("我要拿提成!!!");
        return "这就是静态代理";
    }
}

4,客户端调用代理类,并传入目标对象

public class Test {
    public static void main(String[] args) throws FileNotFoundException {
        //客户端调用静态代理
        SaleHouse saleHouse = new SaleHouseImpl();
        SaleHouseProxy saleHouseProxy = new SaleHouseProxy(saleHouse);
        saleHouseProxy.saleHouse();
    }
}

5,控制台查看打印结果

我要收代理费!!!
--------------------
我要卖房子啦!!!
--------------------
我要拿提成!!!

动态代理

其实无论是静态代理还是静态代理,在我们的日常开发中,使用的都是很多,但对于SpringAop、RPC等框架来说,动态代理发挥着相当大的作用,动态代理具有:
运行时控制,灵活性更好
的特点。
那怎么实现动态代理呢?
如下三种方式:

JDK 动态代理
CGLib 动态代理
使用 Spring aop 模块完成动态代理功能 //今天先不说这个

JDK动态代理

实现步骤:
1,定义一个接口及其实现类;
代码同静态代理中步骤1,步骤2;

2,自定义 InvocationHandler (调用处理器)并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;

public class JdkDynamicInvocationHandler implements InvocationHandler {

    //代理类中的真实对象
    private final Object target;

    public JdkDynamicInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //调用方法前,我们可以加一些自己的控制
        System.out.println("我要收代理费!!!");
        Object invoke = method.invoke(target, args);
        //调用方法后,我们依旧可以操作
        System.out.println("我要拿提成!!!");
        return invoke;
    }
}

3,通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;其实,这一步也可以写在第2步的代码里,不过为了代码的可读性,我们进行解耦实现!
3.1,定义一个工厂类,在工厂类中通过Proxy.newProxyInstance()方法获取某个类的代理对象

public class JdkDynamicProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 目标类的类加载器
            target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
            new JdkDynamicInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }

3.2,客户端传入目标对象,实现代理扩展

  //客户端调用静态代理
SaleHouse proxySaleHouse = (SaleHouse) JdkDynamicProxyFactory.getProxy(new SaleHouseImpl());
proxySaleHouse.saleHouse();

4,控制台输出

我要收代理费!!!
我要卖房子啦!!!
我要拿提成!!!

【扩展】
关于Proxy类的静态工厂方法newProxyInstance()如何创建代理实例的过程,感兴趣的可以去读源码,或者参考下面这篇文章
《代理模式在开源代码中的应用》

CGLIB 动态代理

其实在JDK动态代理中有一个弊端,那就是只能代理接口或接口的实现类,那么未实现任何接口的类就不能代理了吗?答案是否定的,因为咱们有
CGLIB
!

CGLIB (Code Generation Library) 是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成,CGLIB 通过继承方式实现代理。

实现步骤:
1,引入cglib依赖
因为是第三方实现的动态代理,所以在使用前先引入依赖包

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

2,定义一个类;

public class Person {
    public void eat(){
        System.out.println("我在吃饭!!!");
    }
}

3,自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;

public class CglibMethodInterceptor implements MethodInterceptor {

    /**
     * @param o           被代理的对象(需要增强的对象)
     * @param method      被拦截的方法(需要增强的方法)
     * @param args        方法入参
     * @param methodProxy 用于调用原始方法
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //调用方法前,我们可以加一些自己的控制
        System.out.println("饭前先洗手");
        Object object = methodProxy.invokeSuper(o, args);
        //调用方法前,我们可以加一些自己的控制
        System.out.println("饭后要擦嘴");
        return object;
    }
}

4,创建一个工厂类,用来构建代理对象,通过 Enhancer 类的 create()方法实现;

public class CglibProxyFactory {

    public static Object getProxy(Class<?> clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new CglibMethodInterceptor());
        // 创建代理类
        return enhancer.create();
    }
}

5、客户端调用,通过反射传入Person类信息

public static void main(String[] args) throws FileNotFoundException {
        //客户端调用静态代理
        Person person = (Person) CglibProxyFactory.getProxy(Person.class);
        person.eat();
    }

6、输出

饭前先洗手
我在吃饭!!!
饭后要擦嘴

OK,终于码完了动态代理,自己还去看了很久的源码,头昏脑涨!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!