Armeria - 基于 Armeria 框架构建 gRPC服务

基础设施


依赖插件配置

Note:JDK 需要 11 及以上,Protobuf3.

import com.google.protobuf.gradle.id

plugins {
    application
    kotlin("jvm") version "1.9.24"
    id("com.google.protobuf") version "0.9.3"
}

group = "org.cyk"
version = "1.0-SNAPSHOT"

repositories {
    mavenLocal()
    mavenCentral()
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.1"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.64.0"
        }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.plugins {
                id("grpc")
            }
        }
    }
}

dependencies {
    implementation ("com.linecorp.armeria:armeria:1.30.1")
    implementation ("com.linecorp.armeria:armeria-grpc:1.30.1")
    compileOnly("javax.annotation:javax.annotation-api:1.3.2")
    runtimeOnly ("ch.qos.logback:logback-classic:1.4.14")
    testImplementation ("org.junit.jupiter:junit-jupiter:5.10.3")
    testImplementation ("com.linecorp.armeria:armeria-junit5:1.30.1")
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

tasks.withType<JavaCompile> {
    sourceCompatibility = "17"
    targetCompatibility = "17"
    options.encoding = "UTF-8"
    options.isDebug = true
    options.compilerArgs.add("-parameters")
}

application {
    mainClass.set("MainKt")
}

编写 proto 文件

{project_root}/src/main/proto 下创建 blog.proto 文件,如下:

syntax = "proto3";

package org.cyk.armeria.grpc.blog;
option java_package = "org.cyk.armeria.grpc.blog";
option java_multiple_files = true;


import "google/protobuf/empty.proto";

service BlogService {
  rpc CreateBlog (CreateBlogReq) returns (CreateBlogResp) {}
  rpc QueryBlogById (QueryBlogByIdReq) returns (QueryBlogByIdResp) {}
  rpc QueryBlogByIds (QueryBlogByIdsReq) returns (QueryBlogByIdsResp) {}
  rpc UpdateBlogById (UpdateBlogByIdReq) returns (UpdateBlogResp) {}
  rpc DeleteBlogById (DeleteBlogByIdReq) returns (google.protobuf.Empty) {}
}

message CreateBlogReq {
  string title = 1;
  string content = 2;
}
message CreateBlogResp {
  Blog blog = 1;
  optional string errorMsg = 2;
}

message QueryBlogByIdReq {    // For retrieving a single post
  int32 id = 1;
}
message QueryBlogByIdResp {
  optional Blog blog = 1;
}

message QueryBlogByIdsReq {  // For retrieving multiple posts
  repeated int32 ids = 1;
}
message QueryBlogByIdsResp {
  repeated Blog blogs = 1;
}

message UpdateBlogByIdReq {
  int32 id = 1;
  string title = 2;
  string content = 3;
}
message UpdateBlogResp {
  Blog blog = 1;
  optional string errorMsg = 2;
}

message DeleteBlogByIdReq {
  int32 id = 1;
}

message Blog {
  int32 id = 1;
  string title = 2;
  string content = 3;
  int64 createdAt = 4;
  int64 modifiedAt = 5;
}

编译 proto 文件

Note:protobuf-gradle-plugin -> https://github.com/google/protobuf-gradle-plugin

gradlew 跳过测试并构建,protobuf-gradle-plugin 插件也会随之编译 proto 文件,如下命令:

gradlew build -x test

之后就可以在如下位置看到编译得到的文件:

  • { project_root }/build/generated/source/proto/main/grpc
  • { project_root }/build/generated/source/proto/main/java

Armeria 集成 gRPC,启动服务

1)创建 BlogServiceGrpcFacade 类,继承 BlogServiceImplBase,表示将来需要远程调用的对象,如下:

import example.armeria.blog.grpc.BlogServiceGrpc.BlogServiceImplBase

class BlogServiceGrpcFacade: BlogServiceGrpc.BlogServiceImplBase()

2)配置 Armeria-gRPC 服务(这里没有使用 SpringBoot,默认构建 Bean)

import org.slf4j.LoggerFactory
import com.linecorp.armeria.server.Server
import com.linecorp.armeria.server.grpc.GrpcService
import service.BlogServiceGrpcFacade

object ArmeriaGrpcBean {
    fun newServer(port: Int): Server {
        return Server.builder()
            .http(port) // 1.配置端口号
            .service(
                GrpcService.builder()
                    .addService(BlogServiceGrpcFacade()) // 2.添加服务示例
                    .build()
            )
            .build()
    }
}

fun main(args: Array<String>) {
    val log = LoggerFactory.getLogger("MainLogger")

    val server = ArmeriaGrpcBean.newServer(9000)
    server.closeOnJvmShutdown().thenRun {
        log.info("Server is closed ...")
    }

    server.start().join()
}

如果看到如下日志,表明服务正在运行:

22:27:51.746 [armeria-boss-http-*:9000] INFO com.linecorp.armeria.server.Server -- Serving HTTP at /[0:0:0:0:0:0:0:0]:9000 - http://127.0.0.1:9000/

开发

基础设施

1)客户端

    companion object {

        private lateinit var stub: BlogServiceBlockingStub
        private lateinit var server: Server
        @JvmStatic
        @BeforeAll
        fun beforeAll() {
            server = ArmeriaGrpcBean.newServer(9000)
            server.start()
            //这里启动不是异步的,所以不用 Thread.sleep 等待
            stub = GrpcClients.newClient(
                "http://127.0.0.1:9000/",
                BlogServiceBlockingStub::class.java,
            )
        }
    }

2)服务端
这里为了专注 Armeria-gRPC 的处理,使用 map 来替代数据库

class BlogServiceGrpcFacade: BlogServiceGrpc.BlogServiceImplBase() {

    // ID 生成器
    private val idGenerator = AtomicInteger()
    // 数据库
    private val blogRepo = ConcurrentHashMap<Int, Blog>()
    
}

创建操作

1)服务端

    override fun createBlog(
        request: CreateBlogReq,
        responseObserver: StreamObserver<CreateBlogResp>
    ) {
        val id = idGenerator.getAndIncrement()
        val now = Instant.now()

        val blog = Blog.newBuilder()
            .setId(id)
            .setTitle(request.title)
            .setContent(request.content)
            .setModifiedAt(now.toEpochMilli())
            .setCreatedAt(now.toEpochMilli())
            .build()
        blogRepo[id] = blog

        val resp = CreateBlogResp.newBuilder()
            .setBlog(blog)
            .build()
        responseObserver.onNext(resp)
        responseObserver.onCompleted()
    }

2)客户端

    @Test
    fun createBlogTest() {
        val req = CreateBlogReq.newBuilder()
            .setTitle("我的博客1")
            .setContent("今天天气真不错~")
            .build()

        println("================= req send ... =================")
        val resp = stub.createBlog(req)
        println(resp.blog.title)
        println(resp.blog.content)
        println("================= resp received ... =================")
    }

3)效果如下:

================= req send ... =================
22:14:03.812 [Test worker] INFO com.linecorp.armeria.internal.common.JavaVersionSpecific -- Using the APIs optimized for: Java 12+
22:14:03.923 [armeria-common-worker-nio-3-3] DEBUG com.linecorp.armeria.server.HttpServerHandler -- [id: 0xd94afbdc, L:/127.0.0.1:9000 - R:/127.0.0.1:55237] HTTP/2 settings: {ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192}
22:14:03.929 [armeria-common-worker-nio-3-2] DEBUG com.linecorp.armeria.client.Http2ResponseDecoder -- [id: 0xfd4c7207, L:/127.0.0.1:55237 - R:/127.0.0.1:9000] HTTP/2 settings: {MAX_CONCURRENT_STREAMS=2147483647, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192}
我的博客1
今天天气真不错~
================= resp received ... =================

读取操作

单个读取:
1)服务端

    override fun queryBlogById(
        request: QueryBlogByIdReq,
        responseObserver: StreamObserver<QueryBlogByIdResp>
    ) {
        val resp = QueryBlogByIdResp.newBuilder()

        blogRepo[request.id]?.let {
            //这里的 it 不能为 null (proto 编译出的文件,只要 set,就不能为 null,除非你不 set)
            resp.setBlog(it)
        }

//        如果不习惯, 可以对可能为空的字段滞后处理
//        val blog = blogRepo[request.id]
//        val resp = QueryBlogByIdResp.newBuilder().apply {
//            blog?.let { setBlog(it) }
//        }

        responseObserver.onNext(resp.build())
        responseObserver.onCompleted()
    }

2)客户端

    @Test
    @Order(1)
    fun createBlogTest() {
		//...
    }

    @Test
    @Order(2)
    fun queryBlogByIdTest() {
        val req = QueryBlogByIdReq.newBuilder()
            .setId(0)
            .build()
        println("================= req send ... =================")
        val resp = stub.queryBlogById(req)
        if (resp.hasBlog()) {
            println(resp.blog.title)
            println(resp.blog.content)
        }
        println("================= req received ... =================")
    }

3)效果如下:

================= req send ... =================
我的博客1
今天天气真不错~
================= req received ... =================

多个读取
1)服务端

    override fun queryBlogByIds(
        request: QueryBlogByIdsReq,
        responseObserver: StreamObserver<QueryBlogByIdsResp>,
    ) {
        val blogs = blogRepo.filter {
            return@filter request.idsList.contains(it.key)
        }.map { it.value }

        val resp = QueryBlogByIdsResp.newBuilder()
            .addAllBlogs(blogs)
            .build()
        responseObserver.onNext(resp)
        responseObserver.onCompleted()
    }

2)客户端

    @Test
    fun queryBlogByIdsTest() {
        // init start
        val q1 = CreateBlogReq.newBuilder().setTitle("blog 1").setContent("balabala").build()
        stub.createBlog(q1)
        val q2 = CreateBlogReq.newBuilder().setTitle("blog 2").setContent("balabala").build()
        stub.createBlog(q2)
        val q3 = CreateBlogReq.newBuilder().setTitle("blog 3").setContent("balabala").build()
        stub.createBlog(q3)
        // init end

        val req = QueryBlogByIdsReq.newBuilder()
            .addAllIds(listOf(0,1,2))
            .build()
        println("================= req send ... =================")
        val resp = stub.queryBlogByIds(req)
        resp.blogsList.forEach {
            println(it.title)
        }
        println("================= req received ... =================")
    }

3)效果如下:

================= req send ... =================
blog 1
blog 2
blog 3
================= req received ... =================

修改操作

1)服务端

    override fun updateBlogById(
        request: UpdateBlogByIdReq,
        responseObserver: StreamObserver<UpdateBlogResp>
    ) {
        //这里的校验一般不再这一层做(还会有 Handler 读写分离类)
        val (errorMsg, beforeBlog) = checkAndGetPair(request)
        if (errorMsg != null) {
            responseObserver.onNext(
                UpdateBlogResp.newBuilder()
                .setErrorMsg(errorMsg)
                .build()
            )
            responseObserver.onCompleted()
            return
        }

        val afterBlog = Blog.newBuilder().apply {
            id = beforeBlog!!.id
            title = request.title
            content = request.content
        }.build()
        blogRepo[afterBlog.id] = afterBlog

        val resp = UpdateBlogResp.newBuilder()
            .setBlog(afterBlog)
            .build()
        responseObserver.onNext(resp)
        responseObserver.onCompleted()
    }

    private fun checkAndGetPair(req: UpdateBlogByIdReq): Pair<String?, Blog?> {
        val blog = blogRepo[req.id]
            ?: return "文章不存在" to null
        // 如果还需要其他校验
        // ...
        return null to blog
    }

2)客户端

    @Test
    fun updateBlogTest() {
        // init start
        val q1 = CreateBlogReq.newBuilder().setTitle("blog 1").setContent("balabala").build()
        val blogBefore = stub.createBlog(q1)
        // init end

        println("update before =========================>")
        println("title: " + blogBefore.blog.title)

        println("update after =========================>")
        val updateReq = UpdateBlogByIdReq.newBuilder()
            .setId(0)
            .setTitle(q1.title + " update...")
            .setContent(q1.content + " update...")
            .build()
        val blogAfter = stub.updateBlogById(updateReq)
        println("title: " + blogAfter.blog.title)
    }

3)效果如下:

update before =========================>
title: blog 1
update after =========================>
title: blog 1 update...

删除操作

1)服务端

    override fun deleteBlogById(
        request: DeleteBlogByIdReq,
        responseObserver: StreamObserver<Empty>
    ) {
        blogRepo.remove(request.id)
        responseObserver.onNext(Empty.getDefaultInstance())
        responseObserver.onCompleted()
    }

2)客户端

    @Test
    fun deleteByIdTest() {
        val cq = CreateBlogReq.newBuilder()
            .setTitle("blog")
            .setContent("balabala ...")
            .build()
        stub.createBlog(cq)

        val qq = QueryBlogByIdReq.newBuilder()
            .setId(0)
            .build()
        stub.queryBlogById(qq).also {
            assertTrue { it.hasBlog() }
        }

        val dq = DeleteBlogByIdReq.newBuilder()
            .setId(0)
            .build()
        stub.deleteBlogById(dq)

        stub.queryBlogById(qq).also {
            assertTrue { !it.hasBlog() }
        }
    }

要在Windows Server上将Tomcat服务器接入SkyWalking APM(Application Performance Monitoring)代理,首先需要安装SkyWalking Agent,并配置它以监控Tomcat应用程序。以下是基本步骤: 1. **下载并安装SkyWalking Agent**: - 访问SkyWalking官网下载适合Windows的SkyWalking Agent JAR包:https://github.com/apache/skywalking/releases - 将下载的JAR文件复制到Tomcat的`lib`目录下或者放在一个易于访问的系统级目录。 2. **配置SkyWalking Agent**: - 创建一个名为`application.yml`(或者`.yaml`)的配置文件,通常放在`<skywalking-agent-installation-root>\conf`目录下。对于Tomcat,添加类似下面的内容: ```yml service: name: tomcat # 指定服务名称 port: 8080 # 如果你的Tomcat监听的是这个端口 discovery: addressZk: "localhost:2181" # 如果使用Zookeeper作为发现服务,这里填Zookeeper地址 receivers: jaeger: # SkyWalking支持Jaeger协议收集数据 endpoint: localhost:6831 # Jaeger服务器的端口 reporters: log: # 将日志发送到本地文件或者其他地方 file: path: logs/app.log ``` 3. **启动SkyWalking Agent**: - 确保你的环境中有运行Zookeeper的服务(如果配置了),然后通过命令行启动Agent,例如: ``` java -jar skywalking-agent.jar @conf/application.yml ``` 4. **配置Tomcat**: - 在Tomcat的`server.xml`中添加SkyWalking的过滤器。查找`<filter>`标签,添加以下内容: ```xml <filter> <filter-name>skywalking</filter-name> <filter-class>com.linecorp.armeria.server.trace.SkyWalkingTracingFilter</filter-class> </filter> <filter-mapping> <filter-name>skywalking</filter-name> <url-pattern>/*</url- 关闭Tomcat,然后按照上述配置修改后的文件重启Tomcat服务。 现在,SkyWalking Agent应该已经开始收集Tomcat应用的性能指标,并将其发送到指定的监控服务。如果你有任何疑问,可以参考SkyWalking官方文档:https://skywalking.apache.org/docs/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值