2024年1月

RapidFuzz
是一个用于快速字符串模糊匹配的Python库,它能够快速计算两个字符串之间的相似度,并提供与
Fuzzywuzzy(已停用)

TheFuzz(Fuzzywuzzy的升级版)
类似的接口。RapidFuzz和TheFuzz功能相似,只是提供的接口不同。RapidFuzz和TheFuzz安装指令如下:

pip install rapidfuzz
pip install thefuzz

RapidFuzz库是一个经过高度优化的C++实现,为用户提供了以Python编写代码的速度和灵活性。该库还提供了一个与TheFuzz和Fuzzywuzzy中所有算法兼容的API,因此强烈推荐使用RapidFuzz库进行文本模糊匹配。

# 查看rapidfuzz版本
import rapidfuzz
rapidfuzz.__version__
'3.4.0'
# 查看thefuzz版本
import thefuzz
thefuzz.__version__
'0.20.0'

1 基础使用

在RapidFuzz3版本中,默认情况下不会对字符串进行预处理(删除所有非字母数字字符、修剪空白、将所有字符转换为小写)。这意味着,当比较具有相同字符但英文大小写不同的两个字符串时,它们的相似性分值可能不同。

1.1 字符串相似度对比

RapidFuzz提供了多种方法来比较字符串之间的相似度,包括
ratio

partial_ratio

partial_ratio_alignment

token_set_ratio

partial_token_set_ratio

token_sort_ratio

partial_token_sort_ratio

token_ratio

partial_token_ratio

WRatio

QRatio

下面是对这些方法进行简要对比的说明:

  • ratio:计算两个字符串之间的相似度得分,返回一个介于0和100之间的浮点数。这个方法使用了Levenshtein距离来测量字符串之间的编辑距离。
  • partial_ratio:与ratio类似,但它只考虑其中一个字符串是否是另一个字符串的子集。
  • partial_ratio_alignment:计算两个字符串之间的相似度得分,并返回对齐后的字符串。该方法使用了Levenshtein距离和动态规划算法来对齐字符串。
  • token_set_ratio:将两个字符串拆分为单词或标记,并计算它们之间的相似度得分。它使用了集合操作和ratio方法来计算相似度。
  • partial_token_set_ratio:与token_set_ratio类似,但它只考虑其中一个字符串是否是另一个字符串的子集。
  • token_sort_ratio:将两个字符串拆分为单词或标记,并按字母顺序对它们进行排序,然后计算它们之间的相似度得分。它使用了排序和ratio方法来计算相似度。
  • partial_token_sort_ratio:与token_sort_ratio类似,但它只考虑其中一个字符串是否是另一个字符串的子集。
  • token_ratio:将两个字符串拆分为单词或标记,并计算它们之间的相似度得分。它使用了ratio方法来计算相似度。
  • partial_token_ratio:与token_ratio类似,但它只考虑其中一个字符串是否是另一个字符串的子集。
  • WRatio:使用了不同的权重来计算字符串之间的相似度得分。
  • QRatio:一个更快但不太准确的版本的ratio方法。

这些方法提供了不同的计算方式和适用场景,具体使用哪种方法取决于需求和数据特点,一般从ratio、partial_ratio、token_set_ratio、token_sort_ratio和token_ratio之间选择。TheFuzz模块也提供类似的接口。

# 导入模块
from rapidfuzz import fuzz
# ratio
fuzz.ratio("Hello hello 世界", "hello 世界!")
69.56521739130434
# partial_ratio
fuzz.partial_ratio("Hello hello 世界", "hello 世界!")
94.11764705882352
# partial_ratio_alignment
fuzz.partial_ratio_alignment("Hello hello 世界", "hello 世界!")
ScoreAlignment(score=94.11764705882352, src_start=6, src_end=14, dest_start=0, dest_end=9)
# token_set_ratio
fuzz.token_set_ratio("Hello hello 世界", "hello 世界!")
71.42857142857143
# partial_token_set_ratio
fuzz.partial_token_set_ratio("Hello hello 世界", "hello 世界!")
100.0
# token_sort_ratio
fuzz.token_sort_ratio("Hello hello 世界", "hello 世界!")
69.56521739130434
# partial_token_sort_ratio
fuzz.partial_token_sort_ratio("Hello hello 世界", "hello 世界!")
94.11764705882352
# token_ratio
fuzz.token_ratio("Hello hello 世界", "hello 世界!")
71.42857142857143
# partial_token_ratio
fuzz.partial_token_ratio("Hello hello 世界", "hello 世界!")
100.0
# WRatio
fuzz.WRatio("Hello hello 世界", "hello 世界!")
85.5
# QRatio
fuzz.QRatio("Hello hello 世界", "hello 世界!")
69.56521739130434

1.2 字符串搜索

RapidFuzz的extract模块提供了一种基于模糊匹配的字符串集合匹配方法,可以根据目标字符串以及一个或多个字符串集合,并返回与查询字符串匹配度高的多个候选字符串。extractOne模块也是基于模糊匹配的字符串匹配方法,但它只返回与目标字符串最相似的字符串,而不是一组匹配结果。TheFuzz模块也提供类似的接口。

from rapidfuzz import process

query = "世界 Hello"
choices = ["你好世界", "hello 世界", "世界你好", "你好Hello", "Hello", "你好"]
result = process.extract(query, choices)
# 默认返回前五相似结果
print(result)
[('Hello', 90.0, 4), ('hello 世界', 83.125, 1), ('你好Hello', 66.66666666666667, 3), ('你好世界', 60.00000000000001, 0), ('世界你好', 60.00000000000001, 2)]
from rapidfuzz import process

query = "世界 Hello"
choices = ["你好世界", "hello 世界", "世界你好", "你好Hello", "Hello", "你好"]
# 默认返回前三相似结果
result = process.extract(query, choices, limit=3)
print(result)
[('Hello', 90.0, 4), ('hello 世界', 83.125, 1), ('你好Hello', 66.66666666666667, 3)]
# 以下方法返回最匹配的结果
result = process.extractOne(query, choices)
# 类似于
# result = process.extract(query, choices, limit=3)
print(result)
('Hello', 90.0, 4)
# 指定匹配算法
result = process.extract(query, choices, scorer=fuzz.partial_token_ratio)
print(result)
[('hello 世界', 100.0, 1), ('Hello', 100.0, 4), ('你好Hello', 83.33333333333334, 3), ('你好世界', 66.66666666666667, 0), ('世界你好', 66.66666666666667, 2)]

1.3 距离计算

除了相似字符串查找,RapidFuzz也提供了几种字符串相似性度量的方法。下面是对其中几个主要函数的使用说明:

distance(s1, s2)

这个函数计算两个字符串s1和s2之间的Levenshtein距离(编辑距离)。编辑距离使用说明见:
Levenshtein Distance算法与使用场景
。简单来说Damerau-Levenshtein距离是一种编辑距离度量方法,用于计算将一个字符串转换为另一个字符串所需的最小编辑操作次数。这些编辑操作包括插入、删除、替换和相邻字符交换。Damerau-Levenshtein距离越小,表示两个字符串越相似或接近。

from rapidfuzz import distance

s1 = "你好 世界"
s2 = "你好 world!"

dist = distance.DamerauLevenshtein.distance(s1, s2)
print(dist)  # 输出结果为7
6

normalized_distance(s1, s2)

计算范围为[1,0]的归一化Damerau-Levenstein距离。

from rapidfuzz import distance

s1 = "你好 世界"
s2 = "你好 world!"

dist = distance.DamerauLevenshtein.normalized_distance(s1, s2)
print(dist)
0.6666666666666666

similarity(s1, s2)

计算范围为[max,0]的Damerau-Levenstein相似度。这被计算为max(len1,len2)-distance。

from rapidfuzz import distance

s1 = "你好 世界"
s2 = "你好 world!"

sim = distance.DamerauLevenshtein.similarity(s1, s2)
print(sim)
3

normalized_similarity(s1, s2)

计算在[0,1]范围内的归一化Damerau-Levenstein相似性。

from rapidfuzz import distance

s1 = "你好 世界"
s2 = "你好 world!"

sim = distance.DamerauLevenshtein.normalized_similarity(s1, s2)
print(sim)
0.33333333333333337

2 参考

1、SUBMIT说明

在ABAP中,SUBMIT关键字用于运行另一个ABAP程序。通过SUBMIT关键字,可以在当前程序内部调用其他程序,而无需关闭当前程序。

SUBMIT语句的一般语法如下:

"--------------------@斌将军--------------------
SUBMIT <program> [VIA SELECTION-SCREEN|USING SELECTION-SET <selection_set>]
 [VIA JOB LV_JOBNAME
NUMBERLV_JOBCOUNT]
[
AND RETURN] [EXPORTING LIST TO MEMORY]
[
WITH p1 = <value1> ... pn = <value_n>]
[
WITH p1 = <value1> ... WITH pn = <value_n>]."--------------------@斌将军--------------------

2、指定账号

需求说明:在后台JOB中使用SUBMIT调用程序时,程序中需要指定某个账号来执行某些逻辑,所以使用USE LV_UNAME,用于指定运行目标程序的用户名。不管登录账号是哪个,都是使用账号BINGENERAL来执行目标程序YTESTJOB。

源程序代码如下:

"--------------------@斌将军--------------------
DATA: lv_number           TYPE tbtcjob-jobcount,
lv_name
TYPE tbtcjob-jobname,
lv_uname
TYPEsyst_uname,
lv_print_parameters
TYPEpri_params.CLEAR:lv_name ,lv_number.
lv_name
=|后台作业测试|.CALL FUNCTION 'JOB_OPEN' EXPORTINGjobname=lv_nameIMPORTINGjobcount=lv_numberEXCEPTIONScant_create_job= 1invalid_job_data= 2jobname_missing= 3OTHERS= 4.IF sy-subrc = 0.

lv_uname
= 'BINGENERAL'."调用ytestjob时,用账号:BINGENERAL执行程序 SUBMIT ytestjob TO SAP-SPOOL
SPOOL
PARAMETERSlv_print_parameters
WITHOUT SPOOL DYNPRO
VIA JOB lv_name
NUMBERlv_number
USER lv_uname
ANDRETURN.IF sy-subrc = 0.CALL FUNCTION 'JOB_CLOSE' EXPORTINGjobcount=lv_number
jobname
=lv_name
strtimmed
= 'X' EXCEPTIONScant_start_immediate= 1invalid_startdate= 2jobname_missing= 3job_close_failed= 4job_nosteps= 5job_notex= 6lock_failed= 7OTHERS= 8.IF sy-subrc <> 0.ENDIF.ENDIF.ENDIF."--------------------@斌将军--------------------

在YTESTJOB中校验用户是否有权限,目标程序代码如下:

"--------------------@斌将军--------------------
REPORTytestjob.DATA(lv_message) = '执行本程序的账号:' && sy-uname.WRITE: lv_message.AUTHORITY-CHECK OBJECT 'M_MRES_WWA'
 ID 'ACTVT'DUMMYID 'WERKS' FIELD 'P010'.IF sy-subrc <> 0.WRITE: / '权限校验失败'.ELSE.WRITE: / '权限校验成功'.ENDIF."--------------------@斌将军--------------------

3、存在问题

测试时,使用SAP_ALL权限的账号,没有问题,后台作业创建并执行成功

运行结果

但是此方法对于非SAP_ALL权限的账号TEST1103,执行时就出现了问题

作业没有正常执行

检查后发现原来是权限问题,使用SU53发现缺少权限

官方解释

而且指定用户名时,要用大写,否则仍然检查失败

所以大家在使用此关键字指定用户时,需要注意以上问题。

而对于想通过指定大权限账号来跳过权限校验这个方案,也要考虑给登录用户分配S_BTCH_NAM权限的合理性。​

定期更文,
欢迎关注

跨域问题是浏览器为了保护用户的信息安全,实施了同源策略(Same-Origin Policy),即只允许页面请求同源(相同协议、域名和端口)的资源,当 JavaScript 发起的请求跨越了同源策略,即请求的目标与当前页面的域名、端口、协议不一致时,浏览器会阻止请求的发送或接收。

解决跨域问题方案

跨域问题可以从以下方面解决:

  1. 应用层面解决
    :例如 Spring Boot 项目中解决跨域问题。
  2. 反向代理解决
    :例如 Nginx 中解决跨域问题。
  3. 网关中解决
    :例如 Spring Cloud Gateway 中解决跨域问题。

而这 3 类解决方案,总共包含了 8 种解决方案,一起来看。

1.Spring Boot 中解决跨域

在 Spring Boot 中跨域问题有以下 5 种解决方案:

  1. 使用 @CrossOrigin 注解实现跨域【局域类跨域】
  2. 通过配置文件实现跨域【全局跨域】
  3. 通过 CorsFilter 对象实现跨域【全局跨域】
  4. 通过 Response 对象实现跨域【局域方法跨域】
  5. 通过实现 ResponseBodyAdvice 实现跨域【全局跨域】

接下来详细来看。

1.1 通过注解跨域

使用 @CrossOrigin 注解可以轻松的实现跨域,此注解既可以修饰类,也可以修饰方法。当修饰类时,表示此类中的所有接口都可以跨域;当修饰方法时,表示此方法可以跨域,它的实现如下:

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@CrossOrigin(origins = "*")
public class TestController {
    @RequestMapping("/test")
    public HashMap<String, Object> test() {
        return new HashMap<String, Object>() {{
            put("state", 200);
            put("data", "success");
            put("msg", "");
        }};
    }
}

以上代码的执行结果如下图所示:

从上图中可以看出,前端项目访问另一个后端项目成功了,也就说明它解决了跨域问题。

优缺点分析

此方式虽然虽然实现(跨域)比较简单,但细心的朋友也能发现,
使用此方式只能实现局部跨域,当一个项目中存在多个类的话,使用此方式就会比较麻烦(需要给所有类上都添加此注解)。

1.2 通过配置文件跨域

通过设置配置文件的方式就可以实现全局跨域了,它的实现步骤如下:

  • 创建一个新配置文件。
  • 添加 @Configuration 注解,实现 WebMvcConfigurer 接口。
  • 重写 addCorsMappings 方法,设置允许跨域的代码。

具体实现代码如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration // 一定不要忽略此注解
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 所有接口
        .allowCredentials(true) // 是否发送 Cookie
        .allowedOriginPatterns("*") // 支持域
        .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) // 支持方法
        .allowedHeaders("*")
        .exposedHeaders("*");
    }
}

1.3 通过 CorsFilter 跨域

此实现方式和上一种实现方式类似,
它也可以实现全局跨域
,它的具体实现代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration // 一定不能忽略此注解
public class MyCorsFilter {
    @Bean
    public CorsFilter corsFilter() {
        // 1.创建 CORS 配置对象
        CorsConfiguration config = new CorsConfiguration();
        // 支持域
        config.addAllowedOriginPattern("*");
        // 是否发送 Cookie
        config.setAllowCredentials(true);
        // 支持请求方式
        config.addAllowedMethod("*");
        // 允许的原始请求头部信息
        config.addAllowedHeader("*");
        // 暴露的头部信息
        config.addExposedHeader("*");
        // 2.添加地址映射
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**", config);
        // 3.返回 CorsFilter 对象
        return new CorsFilter(corsConfigurationSource);
    }
}

1.4 通过 Response 跨域

此方式是解决跨域问题最原始的方式,但它可以支持任意的 Spring Boot 版本(早期的 Spring Boot 版本也是支持的)。但此方式也是局部跨域,它应用的范围最小,设置的是方法级别的跨域,它的具体实现代码如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
@RestController
public class TestController {
    @RequestMapping("/test")
    public HashMap<String, Object> test(HttpServletResponse response) {
        // 设置跨域
        response.setHeader("Access-Control-Allow-Origin", "*");
        return new HashMap<String, Object>() {{
            put("state", 200);
            put("data", "success");
            put("msg", "");
        }};
    }
}

1.5 通过 ResponseBodyAdvice 跨域

通过重写 ResponseBodyAdvice 接口中的 beforeBodyWrite(返回之前重写)方法,我们可以对所有的接口进行跨域设置,它的具体实现代码如下:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    /**
     * 内容是否需要重写(通过此方法可以选择性部分控制器和方法进行重写)
     * 返回 true 表示重写
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    /**
     * 方法返回之前调用此方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {
        // 设置跨域
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        return body;
    }
}

此实现方式也是全局跨域,它对整个项目中的所有接口有效。

2.Nginx 中解决跨域

在 Nginx 服务器的配置文件中添加以下代码:

server {
    listen       80;
    server_name  your_domain.com;
    location /api {
        # 允许跨域请求的域名,* 表示允许所有域名访问
        add_header 'Access-Control-Allow-Origin' '*';

        # 允许跨域请求的方法
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';

        # 允许跨域请求的自定义 Header
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept';

        # 允许跨域请求的 Credential
        add_header 'Access-Control-Allow-Credentials' 'true';

        # 预检请求的存活时间,即 Options 请求的响应缓存时间
        add_header 'Access-Control-Max-Age' 3600;

        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }
    # 其他配置...
}

上述示例中,location /api 代表配置针对 /api 路径的请求进行跨域设置。可以根据具体需要修改 location 的值和其他相关参数。配置中的 add_header 指令用于设置响应头部,常用的响应头部包括以下这些:

  • Access-Control-Allow-Origin
    :用于指定允许跨域的域名,可以设置为 * 表示允许所有域名访问。
  • Access-Control-Allow-Methods
    :用于指定允许的跨域请求的方法,例如 GET、POST、OPTIONS 等。
  • Access-Control-Allow-Headers
    :用于指定允许的跨域请求的自定义 Header。
  • Access-Control-Allow-Credentials
    :用于指定是否允许跨域请求发送和接收 Cookie。
  • Access-Control-Max-Age
    :用于设置预检请求(OPTIONS 请求)的响应缓存时间。

3.网关中解决跨域

Spring Cloud Gateway 中解决跨域问题可以通过以下两种方式实现:

  1. 通过在配置文件中配置跨域实现。
  2. 通过在框架中添加 CorsWebFilter 来解决跨域问题。

3.1 配置文件中设置跨域

在 application.yml 或 application.properties 中添加以下配置:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 这里的'/**'表示对所有路由生效,可以根据需要调整为特定路径
            allowedOrigins: "*" # 允许所有的源地址,也可以指定具体的域名
            allowedMethods: # 允许的 HTTP 方法类型
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowedHeaders: "*" # 允许所有的请求头,也可以指定具体的请求头
            allowCredentials: true # 是否允许携带凭证(cookies)
            maxAge: 3600 # CORS预检请求的有效期(秒)

其中:

  • allowedOrigins
    : 设置允许访问的来源域名列表,"*" 表示允许任何源。
  • allowedMethods
    : 指定哪些HTTP方法可以被用于跨域请求。
  • allowedHeaders
    : 客户端发送的请求头列表,"*" 表示允许任何请求头。
  • allowCredentials
    : 当设为 true 时,允许浏览器在发起跨域请求时携带认证信息(例如 cookies)。
  • maxAge
    : 预检请求的结果可以在客户端缓存的最大时间。

通过这样的配置,Spring Cloud Gateway 网关将自动处理所有经过它的跨域请求,并添加相应的响应头,从而允许前端应用执行跨域请求。

3.2 添加 CorsWebFilter 来解决跨域问题

在 Spring-Framework 从 5.3 版本之前,使用以下代码可以让 Spring Cloud Gateway 网关允许跨域:

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 这里仅为了说明问题,配置为放行所有域名,生产环境请对此进行修改
        config.addAllowedOrigin("*");
        // 放行的请求头
        config.addAllowedHeader("*");
        // 放行的请求类型,有 GET, POST, PUT, DELETE, OPTIONS
        config.addAllowedMethod("*"); 
        // 暴露头部信息
        config.addExposedHeader("*"); 
        // 是否允许发送 Cookie
        config.setAllowCredentials(true); 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

而 Spring-Framework 5.3 版本之后,关于 CORS 跨域配置类 CorsConfiguration 中将 addAllowedOrigin 方法名修改为 addAllowedOriginPattern,因此配置了变成了以下这样:

@Configuration
public class GlobalCorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 这里仅为了说明问题,配置为放行所有域名,生产环境请对此进行修改
        config.addAllowedOriginPattern("*");
        // 放行的请求头
        config.addAllowedHeader("*");
        // 放行的请求类型,有 GET, POST, PUT, DELETE, OPTIONS
        config.addAllowedMethod("*"); 
        // 暴露头部信息
        config.addExposedHeader("*"); 
        // 是否允许发送 Cookie
        config.setAllowCredentials(true); 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

小结

跨域问题可以在网关层、反向代理层或应用层来解决,而它们的使用优先级是:网关层 > 代理层 > 应用层。因为越靠前覆盖范围就越大,解决跨域问题就越容易。

课后思考

为什么跨域问题竟有 8 种解决方案?这 8 种解决方案有没有相同之处?跨域问题的本质是啥?

欢迎评论区,写出你的答案。点赞过 50,更新下一篇文章,谢谢大家!

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

前一阵子利用Balazor开发了一个NuGet站点,对WebAssembly进行了初步的了解,觉得挺有意思。在接下来的一系列文章中,我们将通过实例演示的方式介绍WebAssembly的一些基本概念和编程模式。首先我们先来说说什么是WebAssembly,它主要帮助我们解决什么问题。

本文演示实例下载:
app1
app2

一、概述
二、WebAssembly程序(app.wat)
三、编译生成app.wasm
四、JavaScript程序
五、跑起来看看
六、调用导出函数

一、概述

WebAssembly可以视为一种采用精简的”二进制格式”的“低等级”、“类汇编”语言。目前主流的浏览器均提供了对WebAssembly的支持,虽然WebAssembly的执行性能(它能够提供near-native的执行性能)是JavaScript无法比拟的,但是在表达能力和灵活性还是不如JavaScript,所以WebAssembly的出现并不是要取代JavaScript,而是作为JavaScript的“助手”,两者配合,各自发挥自身的优势,进而开发出更高质量的Web应用。

与其说WebAssembly像汇编,不如说它更像MSIL,因为它不是面向开发者的高级语言,而是高级语言的“编译目标”。虽然.NET 的运行时CLR不能直接执行C#、VB.NET和F#这样的高级语言编写的代码,而执行执行MSIL指令,但是我们可以将前者编译成后者。与之类似,我们也可以使用C、C++、C#、Rust和AssemblyScript(面向WebAssembly的TypeScript)作为开发语言,然后将代码编译成WebAssembly。

与汇编一样,虽然WebAssembly采用的是单纯的二进制格式,但是可以转换成文本形式。不仅如此,我们还可以按照这样的文本格式来编写程序(在接下来的内容中,我们提供的WebAssembly程序均采用这样的编写方式),并利用相应的工具将其编程成WebAssembly。WebAssembly文本采用一种名为S-expression的树形结构,我们定义的内容都存在与一个具体的“节点”中,每个节点通过小括号包裹起来,子节点直接内嵌于父节点中。由于模块是WebAssembly的基本部署和加载单元,所以module总是它们的根节点,如下所示的就是一个合法的最简单的WebAssembly程序。

(module)

为了让大家对WebAssembly文本的S-expression有一个大致的了解,我们采用这样的形势定义了一个进行整数加法运算的add函数。如下面的代码片段所示,我们定义的函数通过内嵌于module节点的(func)节点表示。它具有一些子节点,其中(export "
add
") 表示将这个函数以名称“add”进行导出,这意味着加载此模块的JavaScript应用可以直接调用此函数。额外三个节点(param $x i32) (param $y i32) (result i32)定义了函数的签名,揭示了此函数包含两个Int32(i32)的输入参数,返回值(结果)的类型也是Int32(i32)。为了提供可读性,同时也方面传参方面,我们将参数进行了命名($x和$y)

(module
  (func (export "add") (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.add)
)

值得一提的是,WebAssembly仅仅支持4种数据类型,分别是32和64位的整数(i32和i64)和浮点数(f32和f64),连我们最常使用的字符串类型都支持。由于字符串本质上就是一组字符序列,而一个字符本质上是采用某种编码的一段字节序列,所以WebAssembly中针对字符串的处理需要自行解决编解码的工作,所以直接以S-expression文本的形式编写WebAssembly程序并不容易。

上面说WebAssembly更像MSIL,还因为执行WebAssembly的VM是一个“堆栈机(Stack Machine)”,意味着它采用“压栈”的方式传递参数。而汇编面向的是真正的机器语言,是“堆栈机(Stack Machine)”和“寄存器机(Register Machine)”的结合,不仅参数传递可以采用堆栈,也可以采用寄存器。这一点可以从add函数的实现看出来,由于最终需要执行i32.add指令,我们需要在这之前调用local.get指令将两个参数压入栈中。i32.add执行后的结果也将入栈,并成为函数返回值。

在对WebAssembly以及基于S-expression的文本形式有了基本了解之后,我们通过一个简单的例子来演示一下一个WebAssembly程序大体上如何编写,我们着重关注JavaScript应用和WebAssembly之间的功能交互。我们创建一个空的目录,并创建两个文本文件app.wat和index.html,前者代表以S-expression文本形式编写的WebAssembly程序,后者是一个空的HTML页面,我们利用它提供的JavaScript程序加载并执行编译后的WebAssembly模块。

二、WebAssembly程序(app.wat)

如下所示的是app.wat的内容,看起来很长,好在我们现在对S-expression有了基本的了解,所以理解起来没有什么问题。模块的第一个节点(func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同样是定义了一个函数,我们将其命名为$print, 子节点(import "js" "print") 表明该函数是从作为宿主的Javascript应用中导入的,具体的路径为js->print。如果导入的Javascript对象通过变量imports表示,意味着imports.js.print表示的就是这个导入的对象。至于其余的四个节点(param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同样是提供了函数的签名:个i32类型的参数,分别表示第一个操作数、第二个操作数、操作符(1,2,3,4分别代表加、减、乘、除)和执行结果。由于这个函数仅仅打印运算表达式,所以没有返回值。

(module

;; import js func print (op1, op2, op, result)

  (func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))

;; int32 add (int32 x, int32 y)
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)

;; int32 sub(int32 x, int32 y)
(func $sub (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.sub)

;; int32 mul(int32 x, int32 y)
(func $mul (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.mul)

;; int32 div(int32 x, int32 y)
(func $div (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.div_u)

(func $main
;; call print(1, 2, add(1,2), 1)
i32.const 1
i32.const 2
i32.const 1

i32.const 1
i32.const 2
call $add
call $print

;; call print(1, 2, sub(1,2), 2)
i32.const 1
i32.const 2
i32.const 2

i32.const 1
i32.const 2
call $sub
call $print

;; call print(1, 2, mul(1,2), 3)
i32.const 1
i32.const 2
i32.const 3

i32.const 1
i32.const 2
call $mul
call $print

;; call print(1, 2, div(1,2), 4)
i32.const 1
i32.const 2
i32.const 4

i32.const 1
i32.const 2
call $div
call $print
)

(start $main)
)

接下来我们定义了四个进行加、减、乘和除运算的函数add、sub、mul和div,它们与上面定义的add函数类似,不过由于缺少了(export “{funcname}”)节点,所以它们仅仅是四个内部函数而已。接下来,我们定义了一个$main函数,它会传入相同的参数(1、2)调用上述4个函数,并调用导入的print方法将包含结果的运算表达式打印出来。虽然命名为$main,但是它也仅仅是一个普通的函数而已,所以我们需要利用(start $main)节点将其作为入口函数,这样它就会在加载的时候自动执行了。

三、编译生成app.wasm

以文本形式编写的WebAssembly程序需要编译成二进制模块才能被加载执行,这里我们使用的wat2wasm这个工具,这个工具可以从
这里
下载。除了将wat文件转化成wasm文件的wat2wasm,下载包里还包含了其他一些有用的工具,比如进行反向操作的wasm2wat。上面编写的app.wat文件利用如下的命令就可以编译生成WebAssembly目标文件app.wasm。

wat2wasm app.wat -o app.wasm

四、JavaScript程序

如下所示的就是index.html文件的内容,我们着重关注其提供的JavaScript代码。我们首先定义被WebAssembly导入的用来输出运行表达式的函数print,然后按照导入路径js.print将其封装到一个导入对象中({"
js
":{"
print
":print}})。我们调用WebAssembly.instantiateStreaming函数以异步方式加载app.wasm模块,并创建对应的实例。具体下载app.wasm模块通过第一个参数提供的Promise(fetch("
app.wasm
"))完成,第二个参数代表导入对象。

<!DOCTYPE html>
<html>
<head></head>
<body>
    <script>
        var url="app.wasm";
        var print = (op1, op2, op, result) => {
            switch (op) {
                case 1:
                    console.log(`${op1} + ${op2} = ${result}`);
                    break;
                case 2:
                    console.log(`${op1} - ${op2} = ${result}`);
                    break;
                case 3:
                    console.log(`${op1} * ${op2} = ${result}`);
                    break;
                case 4:
                    console.log(`${op1} / ${op2} = ${result}`);
                    break;
                default:
                    console.log("invalid operator.");
}
        };
        WebAssembly.instantiateStreaming(fetch("app.wasm"), {"js":{"print":print}});
    </script>
</body>
</html>

五、跑起来看看

我们采用相应的方式将当前目录发布为本地web站点,比如执行Python命令(
python -m http.server
),然后我们利用浏览器访问此站点的默认文件index.html,浏览器的控制台输出就会看到WebAssembly模块初始化输出的四个运算表达式。

image

六、调用导出函数

上面我们演示了WebAssembly模块在初始化的时候调用导入的JavaScript函数,现在我们来演示JavaScript应用如何调用从WebAssembly导出的函数,为此我们将app.wat的代码改写成如下的形式,仅仅保留4个导出的函数add、sub、mul和div。

(module
  (func (export "add") (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.add)

  (func (export "sub")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.sub)

  (func (export "mul")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.mul)

   (func (export "div")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.div_u)
)

index.html中的Javascript代码也做了如下的修改:我们在调用WebAssembly.instantiateStreaming函数成功加载WebAssembly模块并创建对应模块实例后,利用返回结果的instance属性得到这个模块实例。模块导出的成员都保存在该实例的exports属性返回的集合中,为此我们从中提取出导出的四个返回,并利用它们完成对应的运算后,调用console.log函数将包含结果的运算表达式输出到控制台上。由于WebAssembly模块不在需要调用导入的函数,所以调用instantiateStreaming函数的时候不需要在指定导入对象。

<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
var url="app.wasm";
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then(result => {
var exports = result.instance.exports;
var result = exports.add(1,2);
console.log(`1 + 2 = ${result}`);

result = exports.sub(1,2);
console.log(`1 - 2 = ${result}`);

result = exports.mul(1,2);
console.log(`1 * 2 = ${result}`);

result = exports.div(1,2);
console.log(`1 / 2 = ${result}`);
});
</script>
</body>
</html>

程序运行之后,浏览器的控制台上依然会输出相同的结果。

image

物联网浏览器(IoTBrowser)-Web串口自定义开发

工控系统中绝大部分硬件使用串口通讯,不论是原始串口通讯协议还是基于串口的Modbus-RTU协议,在代码成面都是使用System.IO.Ports.SerialPort开发,常用的函数包括打开串口、关闭、接收数据、写入(发送)数据。IoTBrowser封装了一套基础API,前端使用js即可实现打开、关闭、发送数据、接收数据。使用SignlR自动识别客户端环境,使用长连接或WebSocket进行双工实时通讯,以达到实时展示数据的效果。下面示例演示自定义开发模拟串口数据发送和接收。

一、开发

1.创建控制台工程

注意是.Net4.6.2或以下版本。

2.添加引用

引用DDS.IoT.Com.exe

3.新增一个类

a.继承自ComBase,并实现相关接口。

修改定义自定义的驱动类型,后面需要使用到。

b.驱动初始化

c.打开

d.关闭

e.发送数据

发送接口是虚函数,根据需要重写也可以不重写。

f.发送命令接口

public virtual string Command(string name, string data);

用来扩展调用后台代码的函数,与发送接口一样都是虚方法。

二、本机测试

1.编写测试代码

2.查看运行结果

三、编译发布

1.编译工程,并拷贝exe

只需要拷贝工程exe文件,DDS.IoT.Com可不复制。

2.粘贴到插件目录

注意:文件目录app_x64\Plugins\com

3.系统测试

注意修改驱动型号。

以上代码为测试代码,需要根据实际情况操作串口,后续使用真实电子秤开发一个串口交互的示例程序。

示例代码:

https://gitee.com/yizhuqing/IoTBrowser/tree/master/Plugins/MyWebCom