2024年1月

Yarp 与 Nginx 性能大比拼

测试环境:

Ubuntu 22.04.3 LTS (GNU/Linux 6.5.0-14-generic x86_64)

Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz
*2

运行内存:94.3G

yarp 环境

.NET 8 SDK

Program.cs
代码:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

Test.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <InvariantGlobalization>true</InvariantGlobalization>
        <PublishAot>true</PublishAot>


        <StackTraceSupport>false</StackTraceSupport>
        <OptimizationPreference>Size</OptimizationPreference>
        <PublishTrimmed>true</PublishTrimmed>
        <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
        <EventSourceSupport>false</EventSourceSupport>
        <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
        <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
        <MetadataUpdaterSupport>false</MetadataUpdaterSupport>
        <UseNativeHttpHandler>true</UseNativeHttpHandler>
        <TrimMode>link</TrimMode>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
    </ItemGroup>

</Project>

参考
Native AOT deployment overview - .NET | Microsoft Learn
在服务器中安装 aot 环境

使用以下指令构建 aot 程序

dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishAot=true --output ../output

Nginx 安装

在服务器中安装 nginx

sudo apt install nginx


/etc/nginx/conf.d
目录下创建一个 wwwroot.conf

server {
    listen 7771;
    server_name localhost;
    location / {
	    add_header 'Access-Control-Allow-Origin' 'http://localhost:8088';
	    add_header 'Cache-Control' 'public, max-age=604800';
	    add_header 'Access-Control-Allow-Credentials' 'true';
	    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
	    rewrite ^/proxy/bing/(.*)$ /$1 break;
	    proxy_pass http://127.0.0.1:7777/;
    }
}

代理的服务

.NET 8 SDK

创建一个用于测试的代理服务,提供一个简单的接口,直接返回空的字符串。我们将这个服务发布成 linux-64 的程序,

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();


app.MapGet("/weatherforecast", () => string.Empty)
    .WithName("GetWeatherForecast");

app.Run();

并且使用

chmod +x WebApplication1

然后启动我们的代理测试端点

./WebApplication1 urls="http://*:7777"

使用的测试工具

Apipost-Team/runnerGo: A tool similar to apache bench (ab) (github.com)

由 ApiPOST 开源的基于 Go 语言实现的压测工具,我们去 Release 下载发布好的 win-64 位程序,然后执行,

然后打开测试界面
runnerGo UI (apipost.cn)

压测结果

http://192.168.31.251:7772/weatherforecast
Yarp 代理的服务

http://192.168.31.251:7771/weatherforecast
Nginx 代理的服务

第一轮测试:

YARP 压测结果:

Nginx 压测结果:

第二轮测试:

Yarp 压测结果:

Nginx 压测结果:

第三轮压测:

Yarp 压测结果:

Nginx 压测结果:

结论

以上测试都是在内网测试,都属于同一个局域网,由测试结果得出 Yarp 基本完胜 Nginx,虽然说基本性能超越,但是 Yarp 也并发完全可替代 Nginx,Nginx 是支持 TCP/UDP 代理的,而 Yarp 默认是只支持 Http 协议的代理。

来自 token 的分享

技术交流群:737776595

当我们在一个Web应用中使用WebAssembly,最终的目的要么是执行wasm模块的入口程序(通过start指令指定的函数),要么是调用其导出的函数,这一切的前提需要创建一个通过WebAssembly.Instance对象表示的wasm模块实例(
源代码
)。

一、wasm模块实例化总体流程
二、利用WebAssembly.Module创建实例
三、通过字节内容创建创建实例
四、利用XMLHttpRequest加载wasm模块
五、极简编程方式

一、wasm模块实例化总体流程

虽然编程模式多种多样,但是wasm模块的实例化总体采用如下的流程:

  • 步骤一:下载wasm模块文件;
  • 步骤二:解析文件并创建通过WebAssembly.Module类型表示的wasm模块;
  • 步骤三:根据wasm模块,结合提供的导入对象,创建通过WebAssembly.Instance类型表示的模块实例。

二、利用WebAssembly.Module创建实例

我们照例通过一个简单的实例来演示针对wasm模块加载和模块实例创建的各种编程模式。我们首先利用WebAssembly Text Format(WAT)形式定义如下一个wasm程序,定义的文件名为app.wat。如代码所示,我们定义了一个用于输出指定浮点数(i64)绝对值的导出函数absolute。绝对值通过
f64.abs
指令计算,具体得输出则通过导入的print函数完成。

(module
   (func $print (import "imports" "print") (param $op f64) (param $result f64))
   (func (export "absolute") (param $op f64)
      (local.get $op)
      (f64.abs (local.get $op))
      (call $print)
   )
)

我们通过指定wat2wasm (源代码压缩包种提供了对应的.exe)命令(wat2wasm app.wat –o app.wasm)编译app.wat并生成app.wasm后,定义如下这个index.html页面,作为宿主程序的JavaScript脚本完全按照上面所示的步骤完成了针对wasm模块实例的创建。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
           fetch("app.wasm")
                .then((response) => response.arrayBuffer())
                .then(bytes => {
                    var module = new WebAssembly.Module(bytes);
                    var instance = new WebAssembly.Instance(module, {"imports":{"print": print}});
                    instance.exports.absolute(-3.14);
                })
        </script>
    </body>
</html>

具体来说,我们调用fetch函数将app.wasm文件下载下来后,我们将获得的字节内容作为参数调用构建函数创建了一个
WebAssembly.Module
对象。然后将这个Module对象和创建的导入对象({"
imports
":{"
print
":
print
}})作为参数调用构造函数创建了一个
WebAssembly.Instance
对象,该对象正是我们需要的wasm模块实例。我们从模块实例中提取并执行导出的absolute函数。导入的print函数会将绝对值计算表达式以如下的形式输出到页面中。

image

除了调用构造函数以同步(阻塞)的方式根据WebAssembly.Module对象创建WebAssembly.Instance对象外,我们还可以调用
WebAssembly.instantiate
静态方法以异步的方式“激活”wasm模块实例,它返回一个Promise<WebAssembly.Instance>对象。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
fetch("app.wasm")
    .then((response) => response.arrayBuffer())
    .then(bytes => {
        var module = new WebAssembly.Module(bytes);
        return WebAssembly.instantiate(module, { "imports": { "print": print } });
    })
    .then(instance => instance.exports.absolute(-3.14));

三、通过字节内容创建创建实例

静态方法WebAssembly.instantiate还提供了另一个重载,我们可以直接指定下载wasm模块文件得到的字节内容作为参数。这个重载返回一个Promise<WebAssembly.
WebAssemblyInstantiatedSource
>对象,WebAssemblyInstantiatedSource对象的instance属性返回的正是我们需要的wasm模块实例。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
fetch("app.wasm")
    .then((response) => response.arrayBuffer())
    .then(bytes => WebAssembly.instantiate(bytes, {"imports":{"print": print}}))
    .then(result =>result.instance.exports.absolute(-3.14));

四、利用XMLHttpRequest加载wasm模块

fetch函数是我们推荐的用于下载wasm模块文件的方式,不过我们一定义要使用传统的XMLHttpRequest对象也未尝不可。上面的三种激活wasm模块实例的方式可以采用如下的形式来实现。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
const request = new XMLHttpRequest();
request.open("GET", "app.wasm");
request.responseType = "arraybuffer";
request.send();

request.onload = () => {
    var bytes = request.response;
    var module = new WebAssembly.Module(bytes);
    var instance = new WebAssembly.Instance(module, {"imports":{"print": print}});
    instance.exports.absolute(-3.14);
};

上面演示的利用创建的WebAssembly.Module对象和导入对象调用构造函数创建WebAssembly.Instance的同步形式。下面则是将二者作为参数调用静态方式WebAssembly.instantiate以异步方式激活wasm模块实例的方式。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
const request = new XMLHttpRequest();
request.open("GET", "app.wasm");
request.responseType = "arraybuffer";
request.send();

request.onload = () => {
    var bytes = request.response;
    WebAssembly
        .instantiate(request.response, {"imports":{"print": print}})
        .then(result => result.instance.exports.absolute(-3.14));
};

下面演示WebAssembly.instantiate静态方法的另一个重载。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
const request = new XMLHttpRequest();
request.open("GET", "app.wasm");
request.responseType = "arraybuffer";
request.send();

request.onload = () => {
    var bytes = request.response;
    WebAssembly
        .instantiate(request.response, {"imports":{"print": print}})
        .then(result => result.instance.exports.absolute(-3.14));
};

五、极简编程方式

其实我们有“一步到位”的方式,那就是按照如下的形式执行静态方法WebAssembly.
instantiateStreaming
。该方法的第一个参数用于提供下载.wasm模块文件的PromiseLike<Response>对象,第二个参数则用于指定导入对象。该方法同样返回一个Promise<WebAssembly.WebAssemblyInstantiatedSource>对象,WebAssemblyInstantiatedSource的instance属性返回的正是我们所需的wasm模块实例。

var print = (op, result) => document.getElementById("container").innerText = `abs(${op}) = ${result}`;
WebAssembly
    .instantiateStreaming(fetch("app.wasm"), {"imports":{"print": print}})
    .then(result => result.instance.exports.absolute(-3.14))

1.简介

前边已经介绍过两款抓包工具,应该是够用了,也能够处理在日常工作中遇到的问题了,但是还是有人留言让宏哥要讲解讲解Wireshark这一款抓包工具,说实话宏哥之前也没有用过这款工具,只能边研究边分享。换句话说就是现学现卖,希望大家不要介意,宏哥这里的分享仅供你参考学习,有错误的地方也欢迎你指出。你自己也可以深入的研究一下。

2.软件介绍

Wireshark(前称Ethereal)是一款免费开源的网络嗅探抓包工具,世界上最流行的网络协议分析器!网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。Wireshark网络抓包工具使用WinPCAP作为接口,直接与网卡进行数据报文交换,可以实时检测网络通讯数据,检测其抓取的网络通讯数据快照文件,通过图形界面浏览这些数据,可以查看网络通讯数据包中每一层的详细内容。它的强大特性:例如包含有强显示过滤器语言和查看TCP会话重构流的能力,支持几百种协议和流媒体类型。

网络封包分析软件的功能可想像成 "电工技师使用电表来量测电流、电压、电阻" 的工作 - 只是将场景移植到网络上,并将电线替换成网络线。在过去,网络封包分析软件是非常昂贵的,或是专门属于营利用的软件。Ethereal的出现改变了这一切。在GNUGPL通用许可证的保障范围底下,使用者可以以免费的代价取得软件与其源代码,并拥有针对其源代码修改及客制化的权利。Ethereal是目前全世界最广泛的网络封包分析软件之一。

Wireshark 在网络排障中使用非常频繁,显示了网络模型中的第 2 层到第 5 层(链路层、网络层、传输层、应用层),不管是网络工程师、网络安全工程师、黑客、软件开发工程师,平时都会用到Wireshark。

3.WireShark简史

1997年,Gerald Combs 需要一个工具追踪网络问题,并且想学习网络知识。所以他开始开发Ethereal (Wireshark项目以前的名称) 以解决自己的需求。

1998年,Ethreal0.2.0版诞生了。此后不久,越来越多的人发现了它的潜力,并为其提供了底层分析。

Wireshark 是一款免费开源的数据包嗅探器/分析器,可用于捕获网络上的数据包。Wireshark 最初的版本叫做 Ethereal,由 Gerald Combs 于1998年发布,Wireshark 软件和官网上都可以看到他的名字排在首位:

2006年5月,Gerald Combs前往CACE Technologies工作,但是他没能拿到 Ethereal 的商标权。为了保证项目成功运行下去,Combs 和他的开发团队在 2006 年年中将这个项目重新命名为 Wireshark。

随着 Combs 和其他贡献者在接下来十几年内持续维护 Wireshark 的代码并发布新版本,Wireshark 已经成为世界上最流行的数据包分析/嗅探软件之一。

上述的 Wireshark 历史在网络上很容易查到,但是这些资料并未提及 Combs 为何要将该软件取名为 Wireshark(如果有读者能找到请在评论区指出)。

不过官网上有这样一个问题可以参考:

这个回答中的 carcharodon 无疑让人想起了最有名的鲨鱼——大白鲨 (Carcharodon carcharias)。大家都知道鲨鱼具有极其灵敏的嗅觉,能在几公里外嗅到受伤的猎物,用 shark来命名数据包嗅探器真是再合适不过了。

连 Wireshark 图标都是鲨鱼鳍的形状,如下图所示:

2008年,在经过了十年的发展后,Wireshark发布了1.0版本。

4.WireShark可以做什么

简而言之,WireShark主要作用就是可以抓取各种端口的报文,包括有线网口、无线网口、USB口、LoopBack口等等,从而就可以很方便地进行协议学习、网络分析、系统排错等后续任务。

支持实时捕获数据并保存为pcap文件

支持从已经捕获的数据包中读取数据;

支持超过1000种标准/专用协议解析

支持创建插件解析私有协议;

支持使用捕获和显示过滤器细化数据;

支持TLS协议解密(设置比较复杂,不如使用charles/burp/fidder方便)

上边说了一堆,换句话主要就是三点:

①分析网络底层协议,

②解决网络故障问题,

③找寻网络安全问题。

5.Wireshark不能做的

为了安全考虑,wireshark只能查看封包,而不能修改封包的内容,或者发送封包。

不支持编辑修改数据包(需要编辑数据包建议使用 WireEdit)

不支持入侵/检测异常流量

6.Wireshark VS Fiddler

Fiddler是在windows上运行的程序,专门用来捕获HTTP,HTTPS的。

wireshark能获取HTTP,也能获取HTTPS,但是不能解密HTTPS,所以wireshark看不懂HTTPS中的内容。

7.同类的其他工具

1.微软的network monitor

2.Sniffer

3.Omnipeek

4.Fiddler

5.Httpwatch

6.科来网络分析系统

8.什么人会用到wireshark

1. 网络管理员会使用wireshark来检查网络问题

2. 软件测试工程师使用wireshark抓包,来分析自己测试的软件

3. 从事socket编程的工程师会用wireshark来调试

4. IT运维工程师

5.网络工程师

6.安全工程师

7.听说,华为,中兴的大部分工程师都会用到wireshark。

总之跟网络相关的东西,都可能会用到wireshark。

9.平台支持

Wireshark可以在以下平台运行:

①Windows

②MacOS

③Linux/Unix

10.相关网址

1.官网:
https://www.wireshark.org/

2.书籍:
http://www.wiresharkbook.com/

3.维基文档:
https://wiki.wireshark.org/

11.
Wireshark抓包原理

Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换。Wireshark使用的环境大致分为两种,一种是电脑直连网络的单机环境,另外一种就是应用比较多的网络环境,即连接交换机的情况。

「单机情况」
下,Wireshark直接抓取本机网卡的网络流量;

「交换机情况」
下,Wireshark通过端口镜像、ARP欺骗等方式获取局域网中的网络流量。

端口镜像:
利用交换机的接口,将局域网的网络流量转发到指定电脑的网卡上。

ARP欺骗:
交换机根据MAC地址转发数据,伪装其他终端的MAC地址,从而获取局域网的网络流量。

抓包原理分为网络原理和底层原理1.网络原理: 1)本机环境-直接抓本机网卡进出的流量:直接在终端安装ws,然后ws抓本机网卡的与互联网通信的流量。 2)集线器环境(老网络)-集线器:向其他所有端口都会泛洪,抓整个局域网里面的包。

11.1网络抓包原理

网络中不论传输什么,最终通过物理介质发送的都是二进制,类似于0101的Bit流。纯文本(字符串)中文通常采用UTF-8编码,英文用ASCII编码;非纯文本音频、视频、图片、压缩包等按不同编码封装好,转换成二进制传输。在IP网络中,通过Wireshark抓包,获取的原始数据都是二进制。

哪种网络情况下能够抓取到包呢?下面结合网络原理讲解。网络抓包主要存在三种情况:本机环境、集线器环境和交换机环境。

11.1.1本机环境

本机环境直接抓包本机网卡进出的流量。Wireshark会绑定我们的网卡,不需要借助第三方设备(交换机、集线路由器)就能抓取我们的网络通信流量,这是最基本的抓包方式。即直接抓取进出本机网卡的流量包。这种情况下,wireshark会绑定本机的一块网卡。

11.1.2集线器环境

集线器环境可以做流量防洪,同一冲突域。集线器的英文是“Hub”,“Hub”是“中心”的意思,集线器的主要功能是对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上。它工作于OSI参考模型第一层,即“物理层”。

假设三台电脑通信,PC1处安装Wireshark,当PC2、PC3发送数据包到集线器网络里面(冲突域或广播域),由于集线器是物理层产品,不能识别MAC地址、IP地址,它会将接收包往其他所有接口泛洪,此时Wireshark就能抓到从同一个集线器其他电脑发过来的数据包,即局域网中的数据包。这是一种典型的老网络做法,现在基本淘汰。

用于抓取流量泛洪,冲突域内的数据包,即整个局域网的数据包。

11.1.3交换机环境

交换机环境是更加常见的方式,包括端口镜像、ARP欺骗、MAC泛洪。

11.1.3.1端口镜像

交换机是一种数据链路层甚至网络层的产品,它的转包接包严格按照交换机上的MAC地址表通信。所以正常情况下,PC2和PC3通信流量是很难流到PC1的网卡上。当PC2和PC3通信时,PC1是无法通过Wireshark抓到包。但是我们可以在交换机端口做SAPN端口镜像操作,它会将其他两个口的流量复制一份到PC1处,PC1的网卡和Wireshark设置为混插模式,此时就能进行抓包。该模式常用于很多付费的流量分析软件。这种方式下,交换机严格按照tenlnet表和mac地址表进行转发数据包。当pc2和pc3通信的时候,默认是pc1是无法抓取数据包的,但是可以通过在交换机上设置策略,即端口镜像。这样Pc2和Pc3通信的数据包就被复制一份到连接pc2的那个交换机端口,这样pc2就可以抓取到Pc2和Pc3的通信数据包了。

11.1.3.2ARP欺骗

假设我们没有权限在交换机上做端口镜像技术,因为有MAC地址表,又想获取整个局域网中的流量,窃取到PC2、PC3上的流量。这可以通过著名的ARP攻击软件Cain&Abel实现,其流程是:

  • 首先,PC2发送ARP请求广播包,交换机收到包之后会发给PC1和PC3。
  • PC1和PC3接收到,正常情况下PC1会丢弃该包,因为询问的是PC3,但ARP欺骗会回复“我是IP3,对应地址为MAC1”,这就是典型的ARP欺骗或ARP病毒。
  • 最后PC2会将流量封装成底层的MAC1回复过去。如果PC3和PC1都回应,但APR有个特性叫后到优先,PC1会做一个错误的绑定,将数据包发到MAC1,从而导致PC2和PC3的通信流量都会经过PC1,这也是典型的流量劫持、局域网攻击。

步骤如下:

(1)PC2想和PC3通信,故而向交换机发送广播

(2)正常情况下PC1会将此包丢弃掉(因为要找的不是它),但是这里的PC1会进行ARP欺骗,告诉PC2它就是PC2要找的PC3

(3)因为ARP后到优先的特性,PC2很大可能会认为PC1的MAC地址是自己要找的PC3

(4)就这样,PC2和PC3的通信就变成了PC2和PC1的通信了。

(5)至于后续PC1要不要把数据(可能被修改的数据)交给PC3,那完全取决于PC1的心情。

以上便是局域网ARP攻击的典型情况。

11.1.3.3MAC泛洪

通过工具泛洪带来大量垃圾包,产生大量MAC地址,此时交换机MAC地址表会变为右边这张表(爆表),MAC2和MAC3被挤出了MAC地址表。一旦这种MAC地址被挤出MAC地址表,按照交换机原理,如果收到的数据包是未知,它会对外泛洪,此时PC2和PC3对外流量泛洪。

这种情况下,PC1没有端口镜像的权限,所以它不能直接截取全网的流量,那么该怎么做呢?

(1)PC1发送大量垃圾包,这里产生了大量的MAC地址,导致MAC表爆表

(2)PC2和PC3发出的数据找不到目的地址就会进行全网泛洪,被PC1截取全网流量

11.2底层原理

那么,抓包的底层架构是怎样的?下面开始讲解Wireshark的底层原理。

底层原理:wireshark底层抓包工具。

12.wireshark整体架构

Wireshark包括5层架构:

最底层Win-/libpcap:wireshark底层驱动软件,Wireshark抓包时依赖的库文件(驱动文件、库文件)

Capture:抓包引擎,利用libpcap/WinPcap底层抓取网络数据包,libpcap/WinPcap提供了通用的抓包接口,能从不同类型的网络接口(包括以太网、令牌环网、ATM网等)获取数据包

Wiretap:将抓来的二进制数据转换成需要的格式文件,此时获取的是一些比特流,通过Wiretap(格式支持引擎)能从抓包文件中读取数据包,支持多种文件格式

Core:核心引擎,通过函数调用将其他模块连接在一起,起到联动调用的作用,Epan(包分析引擎涉):Protocol-Tree(保存数据包的协议信息,协议结构采用树形结构,解析协议报文时只需从根节点通过函数句柄依次调用各层解析函数即可)、Dissectors(各种协议解码器,支持700多种协议解析,解码器能识别出协议字段,并显示出字段值,Wireshark采用协议树的形式对数据流量的各层次协议逐层处理)、Plugins(一些协议解码器以插件形式实现,源码在plugins目录)、Display-Filters(显示过滤引擎,源码在epan/dfilter目录)。

GTK1/2:图形处理工具,处理用户的输入输出显示,最终存储至Harddisk硬盘中。

PS:了解基本的原理知识挺重要的,尤其为后续。

13.功能模块

模块名

功能

源码子目录

GTK/Qt

处理所有的用户输入/输出(所有的窗口,对话框等等)

/ui
GTK: /ui/gtk
Qt: /ui/qt

Core

主要的"粘合代码"(glue code),它把其他的块组合到一起

/

Epan

(Ethereal Packet Analyzer)

协议树(Protocol-Tree) - 保存捕获文件的协议信息数据

/epan

解析器(Dissectors) - 多种协议的解析器

/epan/dissectors

插件(Plugins) - 一些用插件实现的协议解析器

/plugins

显示过滤器(Display-Filters) - 显示过滤器引擎

/epan/dfilter

Wiretap

wiretap库用于读/写libpcap格式或者其他文件格式的捕获文件

/wiretap

Capture

抓包引擎相关接口

/

Dumpcap

抓包引擎. 这是唯一需要提升权限来执行的部

/

WinPcap/libpcap

(不是Wireshark包的一部分) - 依赖于平台的包捕获库,包含捕获过滤器引擎.这就是我们为什么有不同的显示和捕获 两套过滤语法的原因 - 因为用了两种不同的过滤引擎

-

14.WireShark代码流程图

15.小结

好了,到此宏哥就将Wireshark工具的前世今生基本上全都讲解和分享完了,今天时间也不早了,就到这里!感谢您耐心的阅读~~

Modbus协议在应用中一般用来与PLC或者其他硬件设备通讯,Modbus集成到IoTBrowser使用串口插件模式开发,不同的是采用命令函数,具体可以参考前面几篇文章。目前示例实现了Modbus-Rtu和Modbus-Tcp两种,通过js可以与Modbus进行通讯控制。

一、开发插件

    1. 添加引用
      1. 添加NModbus4,在NuGet搜索NModbus4
      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. 开发ModbusRtu和ModbusTcp插件
      1. ModbusRtu

    public classModbusRtuCom : ComBase
{
public override string Type => "modbusRtuCom";public override string Name => "ModbusRtuCom";private object _locker = new object();public override bool Init(int port, int baudRate = 9600, string extendData = null)
{
this.Port =port;var portName = "COM" +port;base.PortName =portName;
ModbusRtuService.Init(portName, baudRate);
Console.WriteLine(
"初始化ModbusRtuCom驱动程序成功!");return true;
}
public override eventPushData OnPushData;public override boolOpen()
{
var b = false;try{
ModbusRtuService.Open();
b
= true;
IsOpen
= true;
}
catch(Exception ex)
{
string msg = string.Format("ModbusRtuCom串口打开失败:{0}", ex.Message);
Console.WriteLine(msg);
}
returnb;
}
public override boolClose()
{
ModbusRtuService.Close();
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 "ReadCoils"://01 var readData = ModbusRtuService.ReadCoils(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadInputs"://02 readData = ModbusRtuService.ReadInputs(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadHoldingRegisters"://03 readData = ModbusRtuService.ReadHoldingRegisters(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadInputRegisters"://04 readData = ModbusRtuService.ReadInputRegisters(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "WriteSingleCoil"://05 ModbusRtuService.WriteSingleCoil(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ModbusHelper.BoolParse(dataObj.value.ToString()));break;case "WriteSingleRegister"://06 ModbusRtuService.WriteSingleRegister(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.value.ToString()));break;case "WriteMultipleCoils"://0F 写一组线圈 var values = dataObj.value.ToString().Split(' ');var datas = new bool[values.Length];for (var i = 0; i < values.Length; i++)
{
datas[i]
=ModbusHelper.BoolParse(values[i]);
}
ModbusRtuService.WriteMultipleCoils(
byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), datas);break;case "WriteMultipleRegisters"://10 写一组保持寄存器 values = dataObj.value.ToString().Split(' ');var udatas = new ushort[values.Length];for (var i = 0; i < values.Length; i++)
{
udatas[i]
= ushort.Parse(values[i]);
}
ModbusRtuService.WriteMultipleRegisters(
byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), udatas);break;

}
returnoutData;
}
}

b.ModbusTcp

    public classModbusTcpCom : ComBase
{
public override string Type => "modbusTcpCom";public override string Name => "ModbusTcpCom";private object _locker = new object();public override bool Init(int port, int baudRate = 9600, string extendData = null)
{
this.Port =port;
ModbusTcpService.Init(extendData, port);
Console.WriteLine(
"初始化ModbusTcpCom驱动程序成功!");return true;
}
public override eventPushData OnPushData;public override boolOpen()
{
var b = false;try{
ModbusTcpService.Open();
b
= true;
IsOpen
= true;
}
catch(Exception ex)
{
string msg = string.Format("ModbusTcpCom串口打开失败:{0}", ex.Message);
Console.WriteLine(msg);
}
returnb;
}
public override boolClose()
{
ModbusTcpService.Close();
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 "ReadCoils"://01 var readData = ModbusTcpService.ReadCoils(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadInputs"://02 readData = ModbusTcpService.ReadInputs(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadHoldingRegisters"://03 readData = ModbusTcpService.ReadHoldingRegisters(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "ReadInputRegisters"://04 readData=ModbusTcpService.ReadInputRegisters(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.numberOfPoints.ToString()));
outData
=ModbusHelper.ToString(readData);break;case "WriteSingleCoil"://05 ModbusTcpService.WriteSingleCoil(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ModbusHelper.BoolParse(dataObj.value.ToString()));break;case "WriteSingleRegister"://06 ModbusTcpService.WriteSingleRegister(byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), ushort.Parse(dataObj.value.ToString()));break;case "WriteMultipleCoils"://0F 写一组线圈 var values = dataObj.value.ToString().Split(' ');var datas =new bool[values.Length];for(var i=0;i< values.Length;i++)
{
datas[i]
=ModbusHelper.BoolParse(values[i]);
}
ModbusTcpService.WriteMultipleCoils(
byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), datas);break;case "WriteMultipleRegisters"://10 写一组保持寄存器 values = dataObj.value.ToString().Split(' ');var udatas = new ushort[values.Length];for (var i = 0; i < values.Length; i++)
{
udatas[i]
= ushort.Parse(values[i]);
}
ModbusTcpService.WriteMultipleRegisters(
byte.Parse(dataObj.slaveAddress.ToString()), ushort.Parse(dataObj.startAddress.ToString()), udatas);break;

}
returnoutData;
}
}

3.功能

        1. 读单个线圈
        2. 读取输入线圈/离散量线圈
        3. 读取保持寄存器
        4. 读取输入寄存器
        5. 写单个线圈
        6. 写单个输入线圈/离散量线圈
        7. 写一组线圈
        8. 写一组保持寄存器

源代码位置:\Plugins\DDS.IoT.Modbus

二、本机测试

1.测试前准备

需要安装虚拟串口和modbusslave,可以在源代码中下载:

https://gitee.com/yizhuqing/IoTBrowser/tree/master/soft

2.串口测试

modbus03
modbus04

3.TCP测试

modbus02
modbus01

三、部署到IoTBrowser

1.编译

(建议生产环境使用Release模式)

2.拷贝到Plugins文件夹

也可以放到com文件夹。

注意:需要拷贝NModbus4.dll到\IoTBrowser\src\app_x64目录下

四、IoTBrowser集成测试

1.串口测试

modbus05

写入多个数据写入以空格分割,写入线圈数据支持0/1或false/true。

modbus06

2.TCP测试

modbus07

TCP注意ip地址通过扩展数据传入,端口号就是串口号。

创建maven工程,pom.xml中引入连接k8s的客户端jar包:

<properties>
  <maven.compiler.source>8</maven.compiler.source>
  <maven.compiler.target>8</maven.compiler.target>
  <fabric.io.version>6.10.0</fabric.io.version>
</properties>

<dependencies>
  <dependency>
    <groupId>io.fabric8</groupId>
    <artifactId>kubernetes-client</artifactId>
    <version>${fabric.io.version}</version>
  </dependency>
</dependencies>

目标:使用尽可能少的配置项来完成初始化工作,保证后续部署工作量最小

使用kubeconfig文件连接集群

使用场景

可以获取集群的kubeconfig文件

环境变量配置

默认配置项:

KUBECONFIG=~/.kube/config

自定义配置:

KUBECONFIG=/opt/file/kubeconfig

示例代码

import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;

public class K8sClientWithKubeConfig {
    public static void main(String[] args) {
        KubernetesClient client = new KubernetesClientBuilder().build();
        System.out.println(client.getMasterUrl());
        NamespaceList myNs = client.namespaces().list();

        for (Namespace ns : myNs.getItems()) {
            System.out.println(ns.getMetadata().getName());
        }
        client.close();
    }
}

其中master url为kubeconfig文件中的server字段;

使用ServiceAccount连接集群

使用场景

应用部署在k8s集群内,且可以创建Service Account和进行授权等操作(RBAC)

创建SA

apiVersion: v1
kind: ServiceAccount
metadata:
 name: fmuser

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
 name: fmuser-role
rules:
 - apiGroups: [""]
   resources: ["pods", "namespaces"]
   verbs: ["get", "list", "watch"]
 - apiGroups: [""]
   resources: ["pods/log"]
   verbs: ["get", "list", "watch"]
 - apiGroups: [""]
   resources: ["pods"]
   verbs: ["delete"]
 - apiGroups: [""]
   resources: ["namespaces"]
   verbs: ["get", "list", "watch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 name: fmuser-rolebinding
subjects:
 - kind: ServiceAccount
   name: fmuser
   namespace: default
roleRef:
 kind: ClusterRole
 name: fmuser-role
 apiGroup: rbac.authorization.k8s.io

资源授权根据实际使用情况设置即可,Role(当前空间,权限较小)和ClusterRole(整个集群,权限较大)根据需要创建;在代码中使用时,不需要额外配置,客户端会从以下路径自动读取:

/home/app # ls -l /var/run/secrets/kubernetes.io/serviceaccount/
total 0
lrwxrwxrwx    1 root     root            13 Jan 22 02:55 ca.crt -> ..data/ca.crt
lrwxrwxrwx    1 root     root            16 Jan 22 02:55 namespace -> ..data/namespace
lrwxrwxrwx    1 root     root            12 Jan 22 02:55 token -> ..data/token
/home/app #

所需的文件就是ca.crt和token;
如果需要在集群外验证这种认证方式,需要设置如下环境变量:

KUBERNETES_MASTER=https://191.168.7.132:6443;
KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN=/opt/file/serviceaccount-token;
KUBERNETES_CERTS_CA_FILE=/opt/file/serviceaccount-ca.key;
KUBERNETES_AUTH_TRYKUBECONFIG=false

其中serviceaccount-ca.key和serviceaccount-token对应的是上面的ca.crt和token,需要注意的是,使用这种方式,master的url中的端口是6443,而不是默认的443,需要注意;

示例代码

import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;

public class K8sClientWithServiceAccount {
    public static void main(String[] args) {
        KubernetesClient client = new KubernetesClientBuilder().build();
        System.out.println(client.getConfiguration().getCaCertFile());
        System.out.println(client.getConfiguration().getMasterUrl());
        System.out.println(client.getConfiguration().getAutoOAuthToken());
        NamespaceList myNs = client.namespaces().list();

        for (Namespace ns : myNs.getItems()) {
            System.out.println(ns.getMetadata().getName());
        }
        client.close();
    }
}

打印的值就是环境变量中设置的;

获取sa的ca.crt和token

root@tianshu-ai-platform:~# kubectl get sa
NAME      SECRETS   AGE
default   1         13d
fmuser    1         7d
test      1         7d
root@tianshu-ai-platform:~# kubectl describe sa fmuser
Name:                fmuser
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   fmuser-token-9qb8n
Tokens:              fmuser-token-9qb8n
Events:              <none>
root@tianshu-ai-platform:~# kubectl describe secret fmuser-token-9qb8n
Name:         fmuser-token-9qb8n
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: fmuser
              kubernetes.io/service-account.uid: 012db65a-e482-4626-ba01-fc136786c5b1

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1066 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IjJ5SVZq......

root@tianshu-ai-platform:~# kubectl get secret fmuser-token-9qb8n -o jsonpath='{.data.ca\.crt}'
LS0tLS1CRUdJTiBDRVJ....
root@tianshu-ai-platform:~# kubectl get secret fmuser-token-9qb8n -o jsonpath='{.data.token}'
ZXlKaGJHY2lPaUpTVXpJ.....

root@tianshu-ai-platform:~# kubectl get secret fmuser-token-9qb8n -o jsonpath='{.data.ca\.crt}' | base64 --decode
-----BEGIN CERTIFICATE-----
MIIC5zCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTI0MDExNjA1NDUwMloXDTM0MDExMzA1NDUwMlowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGw
FLPnomd41JAIw7jOVrz4Wm+7RwAZanQpOzvmR+SOZTjq5F2Lu/OXa9JhNNisJ2f1
R/MYNRDxCgeTid7iiG9s5OIS+4TFj5S36kXTYmZ51mQXohqjcZeVPgL+Vb8Jz2lS
uXPBGWwjj8ROfjWQcVE49V0DmybdPpZgjEUIxFVFcp3O7jtfl+oecRz55p4bYUrM
KsTXimmKEOcr4t1J6/DlvaXbZVCpJ28iIaDP2Z8qJT6/fg+jnevXr8QiedEsbx1i
D/FmIdBS2O+UsG9Gs+gUr2SA37UmRxelFiQQlgs/yC0/UEh8mrSkslN+Y5qlHEmI
/ntY6ZySzQhatcNEKMkCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFEChpxTbnR+JgB0qm/iHrmTjb+//MA0GCSqGSIb3
DQEBCwUAA4IBAQBcY+JrXiT+OHRhpFV4wdoxh4YKBGbzHNAC+d1mSoJE9K4lbK/n
7SWscW2SdIiCVwmH8VQyH1LKLGQZePuQ64yKetakCD6KCJ40IRY3MQR1lTM5K8pO
KRduy2dLxyfvMFNjjSBiDZnhd9XA1UcqTlwe6o9KAYOvBTePsalS6v7U7tzp1fBI
vtwicakxThcs5jAwb5HA/CHS3VFlAU7g3UZlFgIBvOuLAoUuxxkNIfMoYP/gPNKR
IB1wJBaCHIKcpUtzNH6ejhWIy8cGUeXAYXiL10gQdg20Nnmr3kdnXDzAipvOPspb
kGM9g4KWiXlUqwlq36btT9HHBC5shC4IzYL/
-----END CERTIFICATE-----

root@tianshu-ai-platform:~# kubectl get secret fmuser-token-9qb8n -o jsonpath='{.data.token}' | base64 --decode
eyJhbGciOiJSUzI1NiIsImtpZCI6IjJ5SVZqUjBOLVdwbUluU2F5M....

在使用中的ca.crt和token都是base64解码过的,存储在secret中的都是经过编码的;