开源 Golang 微服务入门二:RPC 框架 Kitex

news2025/2/26 7:07:01

在这里插入图片描述

前言

前一篇笔记介绍了字节跳动的开源 Golang 微服务 HTTP 框架 Hertz,
如下:
开源 Golang 微服务入门一: HTTP 框架 Hertz

本文将要介绍同样是字节跳动开源的 Golang 微服务 RPC 框架 Kitex。

Kitex 简介

Kitex 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
架构设计:

image.png

框架特点:

  • 高性能:使用自研的高性能网络库 Netpoll,性能相较 go net 具有显著优势。

  • 扩展性:提供了较多的扩展接口以及默认扩展实现,使用者也可以根据需要自行定制扩展。

  • 多消息协议:RPC 消息协议默认支持 Thrift、Kitex Protobuf、gRPC。Thrift 支持 Buffered 和 Framed 二进制协议;Kitex Protobuf 是 Kitex 自定义的 Protobuf 消息协议,协议格式类似 Thrift;gRPC 是对 gRPC 消息协议的支持,可以与 gRPC 互通。除此之外,使用者也可以扩展自己的消息协议。

  • 多传输协议:传输协议封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,用于服务治理,Kitex 支持的传输协议有 TTHeader、HTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 结合使用;HTTP2 目前主要是结合 gRPC 协议使用,后续也会支持 Thrift。

  • 多种消息类型:支持 PingPong、Oneway、双向 Streaming。其中 Oneway 目前只对 Thrift 协议支持,双向 Streaming 只对 gRPC 支持

  • 服务治理:支持服务注册/发现、负载均衡、熔断、限流、重试、监控、链路跟踪、日志、诊断等服务治理模块,大部分均已提供默认扩展,使用者可选择集成。

  • 代码生成:Kitex 内置代码生成工具,可支持生成 Thrift、Protobuf 以及脚手架代码。

Thrift 简介

Thrift 本身是一软件框架(远程过程调用框架),用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引 擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。同时,作为 IDL(接口定义语言 Interface Definition Language),允许你定义一个简单的定义文件中的数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地生成 RPC 客户端和服务器通信的无缝跨编程语言。

Protobuf 简介

Protobuf 全称是 Google Protocol Buffer,是一种高效轻便的结构化数据存储方式,用于数据的通信协议、数据存储等。相对比 XML 来说,其特点:

  • 语言无关,平台无关

  • 高效

  • 扩展性、兼容性更强

准备环境和快速启动

准备环境

推荐环境:

  • 如果您之前未搭建 Golang 开发环境, 可以参考 Golang 安装

  • 推荐使用最新版本的 Golang,我们保证最新三个正式版本的兼容性(现在 >= v1.16)。

  • 确保打开 go mod 支持 (Golang >= 1.15 时,默认开启)

  • kitex 暂时没有针对 Windows 做支持,如果本地开发环境是 Windows 建议使用 WSL2

安装教程:2023年最新无脑安装 Go lang 环境配置并编写、运行、打包第一个 Golang 程序详细步骤,附带图文教程

注意:kitex 暂时没有针对 Windows 做支持,如果本地开发环境是 Windows 建议使用 WSL2

WSL的安装
由于Kitex并不支持Linux,所以需要首先安装WSL2

WSL一句话来说就是微软出的一个虚拟机工具

Win11下安装WSL2的步骤为:

  1. “开始菜单”搜索功能,打开“启动或关闭Window功能”

image.png

  1. 勾选以下功能
  • 适用于Linux的Window子系统
  • 虚拟机平台

image.png
3. 微软商店搜索wsl,点击安装

image.png
4. 安装完打开,安装完之后输入用户的账号密码

image.png
如果安装过程中出现下面这种情况:

image.png
说明内核没有更新,需要更新:
打开一个终端输入以下命令:

wsl --update

5.安装完成之后输入下列命令查看wsl的状态:

wsl -l -v
  1. 启动wsl命令:
wsl

7.安装完成之后我的电脑左下角会出现一个linux盘,为linux子系统的文件系统

image.png

快速启动

基于 IDL 的 KiteX 实践
在 RPC 框架中,我们知道,服务端与客户端通信的前提是远程通信,但这种通信又存在一种关联,那就是通过一套相关的协议(消息、通信、传输等)来规范,但客户端又不用关心底层的技术实现,只要定义好了这种通信方式即可。

在 KiteX 中,也提供了一种生成代码的命令行工具:kitex,目前支持 thrift、protobuf 等 IDL,并且支持生成一个服务端项目的骨架。

  1. 安装 kitex:
go install github.com/cloudwego/kitex/tool/cmd/kitex\@latest
  1. 安装 thriftgo:
go install github.com/cloudwego/thriftgo\@latest
  1. 验证是否安装成功:
  • kitex --version
  • thriftgo --version
  1. 获取示例代码

git clone https://github.com/cloudwego/kitex-examples.git

  1. 运行示例代码

    直接启动:

    • 进入示例仓库的 hello 目录
    cd kitex-examples/hello
    
    • 运行 server
    go run ./server
    
    • 运行 client:另起一个终端后
    go run ./client
    

    使用 Docker 快速启动:

    • 进入示例仓库目录
    cd kitex-examples
    
    • 编译项目
    docker build -t kitex-examples .
    
    • 运行 server
    docker run --network host kitex-examples ./hello-server
    
    • 运行 client:另起一个终端后
    docker run --network host kitex-examples ./hello-client
    

    注:代码生成后的hello/hello.go报错,这是因为github.com/apache/thrift 这个包使用的是v0.17.0版本的,但v0.14.0之后的包中很多函数增加了context上下文参数,所以很多函数由于缺少参数报错。只需要修改go.mod中的require中github.com/apache/thrift v0.17.0为github.com/apache/thrift v0.13.0然后再执行go mod tidy问题就能解决

  2. 现在成功通过 Kitex 发起了 RPC 调用。增加一个新方法

    打开 hello.thrift,为新方法分别定义一个新的请求和响应,AddRequest 和 AddResponse,并在 service Hello 中增加 add 方法

    namespace go api
    
    struct Request {
        1: string message
    }
    
    struct Response {
        1: string message
    }
    
    struct AddRequest {
        1: i64 first
        2: i64 second
    }
    
    struct AddResponse {
        1: i64 sum
    }
    
    service Hello {
        Response echo(1: Request req)
        AddResponse add(1: AddRequest req)
    }
    
  3. 重新生成代码

    运行如下命令后,kitex 工具将根据 hello.thrift 更新代码文件

    kitex -service a.b.c hello.thrift
    

    若当前目录不在 $GOPATH/src 下,需要加上 -module 参数,一般为 go.mod 下的名字

    kitex -module "your\_module\_name" -service a.b.c hello.thrift
    

    执行完上述命令后,kitex 工具将更新下述文件

    • 更新 ./handler.go,在里面增加一个 Add 方法的基本实现

    • 更新 ./kitex_gen,里面有框架运行所必须的代码文件

    • 更新服务端处理逻辑

  4. 上述步骤完成后,./handler.go 中会自动补全一个 Add 方法的基本实现,类似如下代码:

    // Add implements the HelloImpl interface.
    func (s \*HelloImpl) Add(ctx context.Context, req \*api.AddRequest) (resp \*api.AddResponse, err error) {
        // TODO: Your code here...
        return
    }
    

    在里面增加我们所需要的逻辑,类似如下代码:

    // Add implements the HelloImpl interface.
    func (s \*HelloImpl) Add(ctx context.Context, req \*api.AddRequest) (resp \*api.AddResponse, err error) {
        // TODO: Your code here...
        resp = \&api.AddResponse{Sum: req.First + req.Second}
        return
    }
    
  5. 增加客户端调用

    在 ./client/main.go 中你会看到类似如下的 for 循环:

    for {
        req := \&api.Request{Message: "my request"}
        resp, err := client.Echo(context.Background(), req)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(resp)
        time.Sleep(time.Second)
    }
    

    在里面增加 Add 方法的调用:

    for {
        req := \&api.Request{Message: "my request"}
        resp, err := client.Echo(context.Background(), req)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(resp)
        time.Sleep(time.Second)
        addReq := \&api.AddRequest{First: 512, Second: 512}
        addResp, err := client.Add(context.Background(), addReq)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(addResp)
        time.Sleep(time.Second)
    }
    
  6. 重新运行示例代码,可以看到客户端在调用 Add 方法

基础教程

Kitex 是一个 RPC 框架,既然是 RPC,底层就需要两大功能:

  • Serialization 序列化

  • Transport 传输

Kitex 框架及命令行工具,默认支持 thrift 和 proto3 两种 IDL,对应的 Kitex 支持 thrift 和 protobuf 两种序列化协议。 传输上 Kitex 使用扩展的 thrift 作为底层的传输协议(注:thrift 既是 IDL 格式,同时也是序列化协议和传输协议)。IDL 全称是 Interface Definition Language,接口定义语言。

创建项目目录

在开始后续的步骤之前,先创建一个项目目录用于后续的教程。

mkdir example

然后进入项目目录

 cd example

编写 IDL

首先创建一个名为 echo.thrift 的 thrift IDL 文件。

然后在里面定义服务

namespace go api

struct Request {
1: string message
}

struct Response {
1: string message
}

service Echo {
Response echo(1: Request req)
}

生成 echo 服务代码

有了 IDL 以后便可以通过 kitex 工具生成项目代码了,执行如下命令:

$ kitex -module example -service example echo.thrift
上述命令中,-module 表示生成的该项目的 go module 名,-service 表明要生成一个服务端项目,后面紧跟的 example 为该服务的名字。最后一个参数则为该服务的 IDL 文件。

生成后的项目结构如下:

    .
    |-- build.sh
    |-- echo.thrift
    |-- handler.go
    |-- kitex_gen
    |   `-- api
    |       |-- echo
    |       |   |-- client.go
    |       |   |-- echo.go
    |       |   |-- invoker.go
    |       |   `-- server.go
    |       |-- echo.go
    |       `-- k-echo.go
    |-- main.go
    `-- script
        |-- bootstrap.sh
        `-- settings.py

获取最新的 Kitex 框架

由于 kitex 要求使用 go mod 进行依赖管理,所以要升级 kitex 框架会很容易,只需要执行以下命令即可:

go get github.com/cloudwego/kitex@latest$ go mod tidy

编写 echo 服务逻辑

需要编写的服务端逻辑都在 handler.go 这个文件中,现在这个文件应该如下所示:

package main

import (
    "context"
    "example/kitex\_gen/api"
)

// EchoImpl implements the last service interface defined in the IDL.
type EchoImpl struct{}

// Echo implements the EchoImpl interface.
func (s \*EchoImpl) Echo(ctx context.Context, req \*api.Request) (resp \*api.Response, err error) {
    // TODO: Your code here...
    return
}

这里的 Echo 函数就对应了之前在 IDL 中定义的 echo 方法。

修改 Echo 函数为下述代码:

func (s \*EchoImpl) Echo(ctx context.Context, req \*api.Request) (resp \*api.Response, err error) {
    return \&api.Response{Message: req.Message}, nil
}

编译运行

kitex 工具已经生成好了编译和运行所需的脚本:

  • 编译:执行以下命令后,会生成一个 output 目录,里面含有编译产物。
sh build.sh
  • 运行:执行以下命令后,Echo 服务就开始运行啦!
sh output/bootstrap.sh

编写客户端

有了服务端后,接下来编写一个客户端用于调用刚刚运行起来的服务端。

  • 创建一个目录用于存放客户端代码:
mkdir client
  • 进入目录:
cd client
  • 创建 client,准备编写客户端代码
import "example/kitex\_gen/api/echo"
import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
    log.Fatal(err)
}

上述代码中,echo.NewClient 用于创建 client,其第一个参数为调用的服务名,要与生成代码中的service_name保持一致,第二个参数为 options,用于传入参数, 此处的 client.WithHostPorts 用于指定服务端的地址(server默认占用8888端口)。

  • 发起调用
import "example/kitex\_gen/api"
...
req := \&api.Request{Message: "my request"}
resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3\*time.Second))
if err != nil {
    log.Fatal(err)
}
log.Println(resp)

上述代码中,首先创建了一个请求 req , 然后通过 c.Echo 发起了调用。

  • 第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为。

  • 第二个参数为本次调用的请求。

  • 第三个参数为本次调用的 options ,Kitex 提供了一种 callopt 机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)

在编写完一个简单的客户端后,通过下述命令发起调用:

go run main.go

如果不出意外,可以看到类似如下输出:

2023/01/30 18:48:07 Response({Message:my request})

至此已经成功编写了一个 Kitex 的服务端和客户端,并完成了一次调用!

补充:链路追踪

在分布式系统或者微服务架构中,一次请求往往需要调动内部的多个模块,多个中间件,多台机器相互协调才能完成。这些调用过程是较为复杂的,有的是串行调用的,有的是并行调用的。这种情况下,如果确定整个请求当中调用了哪些应用,哪些节点,哪些模块,以及他们的先后顺序和各部分的性能,这就是链路追踪。链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如,各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等。

Kitex提供了对OpenTelemetry和OpenTracing的支持,也支持用户自定义链路追踪

OpenTelemetry链路追踪

import (
...
    "github.com/kitex-contrib/obs-opentelemetry/provider"
    "github.com/kitex-contrib/obs-opentelemetry/tracing"
)

func main(){
    serviceName := "echo-client"

    //接入OpenTelemetry
    p := provider.NewOpenTelemetryProvider(
        provider.WithServiceName(serviceName),
        provider.WithExportEndpoint("localhost:4317"),
        provider.WithInsecure(),
    )
    defer p.Shutdown(context.Background())

}

svr := userservice.NewServer(
    new(UserServiceImpl),
    //注入trace到server实例中
    server.WithSuite(tracing.NewServerSuite()),
    server.WithServerBasicInfo(\&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}),
)

服务注册与发现

Kitex服务注册与发现已经对接了主流的服务注册与发现中心
下面以注册ETCD为例

server:

import (
    api "example/kitex\_gen/api/echo"
    "fmt"
    "github.com/cloudwego/kitex/pkg/rpcinfo"
    "github.com/cloudwego/kitex/server"
    etcd "github.com/kitex-contrib/registry-etcd"
    "log"
)

func main() {
    r, err := etcd.NewEtcdRegistry(\[]string{"127.0.0.1:2379"})
    //创建一个etcd的注册,ectd占127.0.0.1的2379端口
        if err != nil {
            fmt.Println(err)
        }
        svr := api.NewServer(new(EchoImpl),
        server.WithRegistry(r), //将server注册到etcd中
        server.WithServerBasicInfo(\&rpcinfo.EndpointBasicInfo{
        ServiceName: "example",
    }),
    )

    err = svr.Run()

    if err != nil {
        log.Println(err.Error())
    }
}

client

import (
    "context"
    "example/kitex\_gen/api"
    "example/kitex\_gen/api/echo"
    "fmt"
    client "github.com/cloudwego/kitex/client"
    etcd "github.com/kitex-contrib/registry-etcd"
    "time"
)

func main() {
    //根据127.0.0.1:2379地址创建一个etcd组件实例
    r, err := etcd.NewEtcdResolver(\[]string{"127.0.0.1:2379"})
    if err != nil {
        fmt.Println(err)
    }
    c := echo.MustNewClient("example",
    client.WithResolver(r)//client.WithResolver,我们能看到最后把一个服务发现实例放到了 client 的 options 结构体中:
    )
    if err != nil {
        fmt.Println(err)
    }
    for {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second\*3)
        resp, err := c.Echo(ctx, \&api.Request{Message: "hello world!"})
        cancel()
        if err != nil {
            fmt.Println(err)
        }
        time.Sleep(time.Second)
        fmt.Println(resp)
    }
}

总结

本文主要介绍了Golang 微服务 RPC 框架 Kitex 的相关内容,并进行了一个简单的示例演示,本文介绍的都是最基础的入门知识,想更深入学习了解 Kitex 还是需要仔细研究官方文档。

引用

  • Kitex 官方文档 Kitex | CloudWeGo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nacos高级

一、什么是配置中心 在微服务架构中,当系统从一个单体应用被拆分成分布式系统上的一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了。不仅配置会分散,分散中还会包含着冗余。 配置中心将…

Linux进程、用户、权限命令

进程管理命令 进程和程序的区别 1 程序是静态概念,本身作为一种软件资源长期保存;而进程是程序的执行过程,它是动态概念,有一定的生命期,是动态产生和消亡的。 2 程序和进程无一一对应关系。一个进程在活动中可有顺序…

顺序表和链表的比较

本文主要内容:从存取方式、逻辑/物理结构、查找/插入/删除操作和空间分配的角度比较顺序表和链表,并从存储、运算、环境的角度对比应如何选取存储结构。 目录 一、顺序表和链表的比较1、存取(读写)方式2、逻辑/物理结构3、查找、…

AI人工智能领域精美绘图模板分享

1 人工智能的发展历程 如今人工智能的应用渗透了我们生活的方方面面,我们都知道人工智能的前景十分光明,在未来对于推进人类发展进程也是非常重要的,但其实人工智能的发展道路是极其曲折的,下面就将人工智能的发展历程分为如下六…

arcgis for javascript TileLayer 自定义高德地图图层

效果如图: 一、创建自定义切片层 要创建自定义图块层,您必须调用BaseTileLayer类的createSubclass()方法。命名自定义层为TintLayer 由于这一层需要知道在哪里访问预定义的图块,我们将创建一个属性。应用程序将为图层提供值,图…

全球首发 《NGINX 完全指南》中文版

原文作者:Derek DeJonghe of F5 原文链接:全球首发 | 《NGINX 完全指南》中文版 转载来源:NGINX 开源社区 NGINX唯一中文官方社区 ,尽在nginx.org.cn 在社区小伙伴们的催促下,我们很高兴地与大家分享这个好消息&#…

关于阵列发射端的波束形成(相控阵)研究与仿真实践

说明 相控阵是一个很大的话题,相控阵说得直白一点就是通过控制天线阵列中各个天线的相位来使得波束指向我们想要的方向。现阶段相控阵雷达用得更多的还是军事领域,不过随着技术的进步、成本的下降以及小型化,相控阵雷达也逐渐被用于民用领域了…

Python之Gradio简单使用

目录 安装Gradio示例用法应用界面1. gr.Interface2. gr.Blocks Gradio的输入和输出组件输入组件(Inputs)输出组件(Outputs) 其他 Gradio是一个Python库,用于构建快速的Web界面,以便于使用机器学习模型进行实…

Vue.js 中的 watch 属性详解

Vue.js 中的 watch 属性详解 在 Vue.js 中,watch 属性是一种非常重要的属性,它可以监听 Vue 实例中指定的数据变化,并在数据发生变化时执行相应的操作。本文将对 Vue.js 中的 watch 属性进行详细的介绍,并附上相关的代码示例。 什…

一文看穿 TypeScript 的庐山真面目

导语: 在了解 TypeScript 之前,我们需要了解 什么是强类型语言和什么是弱类型语言,以及什么是静态类型,什么又是动态类型。 强类型不允许任意的隐式类型转换,而 弱类型 允许静态类型:一个变量声明时它的类型…

基于Python的大数据舆情分析,舆论情感分析可视化系统

运行效果图 基于Python的微博大数据舆情分析,舆论情感分析可视化系统 系统介绍 微博舆情分析系统,项目后端分爬虫模块、数据分析模块、数据存储模块、业务逻辑模块组成。 先后进行了数据获取和筛选存储,对存储后的数据库数据进行提取分析处…

sqlmap -os-shell 使用方法

一、burp suite抓包。 如上图所示,红框处很明显是一个传参点,我们就在这个页面抓包。 抓到包之后将内容保存到桌面的1000.txt文件下。 二、sqlmap跑包。 打开sqlmap跑包。 python sqlmap.py -r C:\Users\16434\Desktop\1000.txt -dbmsmysql --os-shell…

Oracle-catalog影响归档量统计

有个12.2 rac环境报警备份异常,登录检查备份,发现报错日志 piece handle/backup/orcl/archbackup/ARCHBAK_ORCL_20230607_738_1 tagARCH_BAK commentNONE channel d1: backup set complete, elapsed time: 00:01:55 released channel: d1 RMAN-00571: …

从Vuex过渡到pinia

Vuex过渡到Pinia 众所周知,Vuex是一个状态管理库,它方便了我们任何组件不用考虑关系就可以共享一个全局的状态。😃但是 Vuex也有它一定的缺陷。主要缺点,我总结如下: mutations里面不能写异步函数,否则就…

Simulink仿真模块 - Waveform Generator

Waveform Generator模块的功能是使用信号符号输出波形。它所在的库为: Simulink / Sources 如图所示: 双击模型弹出如下对话框,如图所示: Waveform Generator 模块根据您在波形定义表中输入的信号符号输出波形。 此模块支持下列用于信号符号的语法: 函数…

STM32单片机OLED语音识别路灯台灯控制系统人检测亮度调节

实践制作DIY- GC0143-OLED语音识别路灯台灯控制系统 基于STM32单片机设计---OLED语音识别路灯台灯控制系统 二、功能介绍: 电路:STM32F103C系列最小系统串口语音识别模块LED灯板1个红外传感器OLED显示器1个手动自动模式键1个开关按键 1.有两个模式1个手…

速卖通,国际站,temu测评,补单策略:安全与效能并重,提高账号存活率

测评能够帮助卖家让亚马逊平台更喜欢自己的产品,给予更好排名的同时也让后续进入店铺的买家更容易认可自己的产品。这些真实评价在亚马逊卖家管理系统中被称为Review Feedback。这是进行真实交易后形成的评价,而不是通过机器软件生成,形成虚拟…

SpringData进阶篇-下

SpringData进阶篇 一:故事背景二:自定义操作2.1 JPQL和SQL2.1.1 接口内定义2.1.2 调用2.2.3 SQL 方式查询 2.2 规定方法名2.2.1 普通查询规则2.2.2 修饰查询 2.3 Query By Example2.3.1 Repository继承QueryByExampleExecutor2.3.2 具体使用2.3.2 Exampl…

Aspose.Words功能演示:使用 C# 从 Word 文档中读取宏

Aspose.Words 是一种高级Word文档处理API,用于执行各种文档管理和操作任务。API支持生成,修改,转换,呈现和打印文档,而无需在跨平台应用程序中直接使用Microsoft Word。此外, Aspose API支持流行文件格式处…

java-不借用三方程序情况下,使用java自动工具将可执行jar转成exe

一、先新建一个javafx项目 二、配置Artifacts 点击Artifacts,会看到新建的项目会自动创建好了 如果没有的话,可以手动创建如下操作 三、配置 按下图逐一配置即可 四、builde 5、使用java的打包命令打包 PS C:\Users\xx\Downloads\cc\exeex\out\artifacts\JavaFXApp>…