本文介绍常见的异步网络请求编码手法。尽管像golang这些的语言,支持协程,可以使得Programmer以同步的方式编写代码,大大降低编码者的心智负担。但网络编程中,批量又非常常见,这就导致即使在Golang中,也不得不进行协程的切换来满足批量的诉求,在Golang中往往对外以callback的方式暴露接口。

无论是callback、还是返回future、还是返回Mono/Flux,亦或是从channel中读取,这是不同的异步编程范式,编码的时候,可以从项目整体、团队编码风格、个人喜好来依次考虑。本文将以callback为主,但移植到其他异步编码范式,并不困难。

使用callback模式后,对外的方法签名类似:

go

1
func (c *Client) Get(ctx context.Context, req *Request, callback func(resp *Response, err error)) error

java

1
2
3
public interface Client {
void get(Request req, Callback callback);
}

网络编程中的批量

对于网络请求来说,批量可以提高性能。 批量处理是指将多个请求或任务组合在一起,作为单一的工作单元进行处理。批量尽量对用户透明,用户只需要简单地对批量进行配置,而不需要关心批量的实现细节。

常见的批量相关配置

  • batch interval: 批量的时间间隔,比如每隔1s,批量一次
  • batch size: 批量的最大大小,比如每次最多批量100个请求

批量可以通过定时任务实现,也可以做一些优化,比如队列中无请求时,暂停定时任务,有请求时,启动定时任务。

编码细节

整体流程大概如下图所示:

async-network-code

一定要先把请求放到队列/map中

避免网络请求响应过快,导致callback还没注册上,就已经收到响应了。

队列中的消息一定要有超时机制

避免由于丢包等原因,导致请求一直没有响应,而导致队列中的请求越来越多,最终内存溢出。

wait队列生命周期与底层网络client生命周期一致

wait队列中请求一定是依附于client的,一旦client重建,队列也需要重建,并触发callback、future的失败回调。

Apache Ignite简介

Apache Ignite是一个开源分布式的数据库、缓存和计算平台。它的核心是一个内存数据网格,它可以将内存作为分布式的持久化存储,以提供高性能和可扩展性。它还提供了一个分布式的键值存储、SQL数据库、流式数据处理和复杂的事件处理等功能。

Ignite的核心竞争力包括:

  • 兼容Mysql、Oracle语法
  • 性能强大,可以水平扩展
  • 缓存与数据库同源,可通过KV、SQL、JDBC、ODBC等方式访问

同时,为了便于开发,除了jdbc、odbc、restful方式外,Ignite还官方提供了Java、C++、.Net、Python、Node.js、PHP等语言的客户端,可以方便的与Ignite进行交互。

ignite-storage-access

Apache Ignite的问题

频繁创建删除表,导致IGNITE_DISCOVERY_HISTORY_SIZE超过限制

根据Ignite2的拓扑模型,集群的拓扑版本会在创建表/删除表的时候发生变化,该变化版本号递增,且仅会保留最近$IgniteDiscoveryHistorySize条记录,程序某处会写死读取版本为0的数据,读取不到时,ignite集群会重启。默认值为500。
社区issue: https://github.com/apache/ignite/issues/10894
笔者暂时没有时间来修复这个issue,可以通过将IGNITE_DISCOVERY_HISTORY_SIZE设置地比较大,来规避这个问题。

Ignite2客户端易用性问题

Ignite2客户端超时默认值不合理

Ignite2客户端的连接超时、执行sql超时默认都是0,没有精心研究过配置的用户在异常场景下,应用程序可能会hang住。从易用性的角度来说,网络通信的任何操作,默认都应该有超时时间。

Ignite2客户端不支持永远的重试

Ignite通过预先计算出所有需要重连的时间点来实现重连,如果想配置成永远的重连,会因为时间点的计算导致内存溢出。从易用性的角度来说,应该支持永远的重连。

Ignite2客户端在某些异常下无法自愈

当client执行sql的时候,碰到如下异常的时候,无法自愈。可以通过执行SQL对client进行定期检查并重建。

1
Caused by: org.apache.ignite.internal.client.thin.ClientServerError: Ignite failed to process request [47]: 50000: Can not perform the operation because the cluster is inactive. Note, that the cluster is considered inactive by default if Ignite Persistent Store is used to let all the nodes join the cluster. To activate the cluster call Ignite.cluster.state(ClusterState.ACTIVE)

Ignite2 SocketChannel泄露问题

Ignite客户端在连接时,如果对应的Server端没有启动,会导致SocketChannel泄露,已由笔者提交代码修复:https://github.com/apache/ignite/pull/11016/files

通用 GitHub Actions

commit lint

1
2
3
4
5
6
7
8
9
10
11
12
name: commit lint
on:
pull_request:
branches:
- main

jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: wagoid/commitlint-github-action@v5

line lint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: line lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
name: line lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: linelint
uses: fernandrone/linelint@master

Go

golangci-lint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: go ci Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout 3m0s

go mod check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: go mod check

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
go_mod_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Go Mod Check Action
uses: hezhangjian/go-mod-check-action@main
with:
prohibitIndirectDepUpdate: 'true'

go unit tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: go unit test

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
go_unit_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: setup OpenGemini
uses: hezhangjian/setup-opengemini-action@main
- name: Run coverage
run: go test ./... -coverpkg=./padmin/... -race -coverprofile=coverage.out -covermode=atomic

Java GitHub Actions

maven checkstyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: java checkstyle
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
java_checkstyle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Maven Central Repository
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: checkstyle
run: mvn -B clean checkstyle:check

maven spotbugs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: java spotbugs
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
java_spotbugs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Maven Central Repository
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: spotbugs
run: mvn -B -DskipTests clean verify spotbugs:spotbugs

maven unit tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: java unit tests
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
java_unit_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Maven Central Repository
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: unit tests
run: mvn -B clean test

TypeScript GitHub Actions

npm build test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: npm build test
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
npm_buid_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: npm install
- run: npm run build
- name: setup pulsar
uses: hezhangjian/setup-pulsar-action@main
- run: npm run test

prettier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name: prettier
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: npm install --save-dev prettier
- run: npm install --global prettier
- run: prettier --check '**/*.ts'

概述

我们以xyz文件格式为例,来说明文件编解码的代码设计。xyz文件格式内容如下:

  • header部分:文件头,包含文件版本号、文件类型、文件大小等信息
  • body部分:文件主体

通用设计大概如下

classDiagram
    class XyzHeader {
        + byte[] content
    }
    class XyzBody {
        + byte[] content
    }
    class Xyz{
        + XyzHeader header
        + XyzBody body
    }
    class XyzReader {
        + Xyz read(fileName: string)
        + void process(String fileName, XyzProcessor processor)
        - XyzHeader readHeader()
        - XyzBody readBody()
    }
    class XyzProcessor {
        <>
        + void processHeader(XyzHeader header)
        + void processBody(XyzBody body)
    }
    class XyzReadCollectProcessor {
        Xyz getXyz()
    }
    Xyz --> XyzHeader: contains
    Xyz --> XyzBody: contains
    XyzReader --> Xyz: reads
    XyzReader --> XyzProcessor: processes
    XyzReadCollectProcessor --|> XyzProcessor: implements

Java

使用java.io.RandomAccessFilejava.nio.channels.FileChannel来实现文件读取,使用io.netty.buffer.ByteBuf来读写文件。

核心代码举例:

XyzReader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class XyzHeader {
private byte[] content;
}
public class XyzBody {
private byte[] content;
}
public class Xyz {
private XyzHeader header;
private XyzBody body;
}
public interface XyzProcessor {
void processHeader(XyzHeader header);
void processBody(XyzBody body);
}
public class XyzReadCollectProcessor implements XyzProcessor {
private final Xyz xyz = new Xyz();
public Xyz getXyz() {
return xyz;
}
}
public class XyzReader {
public Xyz read(String fileName) throws Exception {
}

private XyzHeader readHeader(FileChannel fileChannel) throws Exception {
}

private XyzBody readBody(FileChannel fileChannel) throws Exception {
}
}

Go

Go标准库

timeout

1
2
3
client := http.Client{
Timeout: timeout,
}

connection timeout

1
2
3
4
5
6
7
client := http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: timeout,
}).Dial,
},
}

Java

标准库(jdk17+)

timeout

1
2
3
4
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://example.com"))
.timeout(Duration.ofSeconds(10))
.build();

connectionTimeout

1
2
3
HttpClient.Builder builder = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.version(HttpClient.Version.HTTP_1_1);

Reactor Netty

timeout

1
HttpClient client = HttpClient.create().responseTimeout(Duration.ofSeconds(10));

connectionTimeout

1
HttpClient client = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

多语言SDK设计的常见问题

日志打印的设计策略

在SDK的关键节点,比如初始化完成、连接建立或者连接断开,都可以打印日志。如果是PerRequest的日志,一般默认不会打印INFO级别的日志。

SDK应该避免仅仅打印错误日志然后忽略异常;相反,它应该提供机制让调用者能够捕获并处理异常信息。这种做法有助于保持错误处理的透明性,并允许调用者根据需要采取适当的响应措施。正如David J. Wheeler所说”Put the control in the hands of those who know how to handle the information, not those who know how to manage the computers, because encapsulated details will eventually leak out.”把控制权放到那些知道如何处理信息的人手中,而不是放在那些知道如何管理计算机的人手中,因为封装的细节最终都会暴露。

是否需要使用显式的start/connect方法?

像go这样的语言,一般来说不太在意特定的时间内,某个协程是否处于阻塞等待连接的状态。而在java这样的语言,特别是在采用响应式编程模型的场景下,通常需要通过异步操作来管理连接的建立。这可以通过显式的start/connect方法来或者是异步的工厂方法来实现。

背景

TLS(Transport Layer Security)是一种安全协议,用于在两个通信应用程序之间提供保密性和数据完整性。TLS是SSL(Secure Sockets Layer)的继任者。

不同的编程语言处理TLS配置的方式各有千秋, 本文针对TLS配置参数的设计进行探讨。

代码配置中,建议使用反映状态的参数名。

通用参数

  • tlsEnable: 是否启用TLS

Go

推荐使用方式一

方式一:

  • tlsConfig *tls.Config: Go标准库的内置TLS结构体

方式二:

由于Go不支持加密的私钥文件,推荐使用文件内容,而不是文件路径,避免敏感信息泄露。

  • tlsCertContent []byte: 证书文件内容
  • tlsPrivateKeyContent []byte: 私钥文件内容
  • tlsMinVersion uint16: TLS最低版本
  • tlsMaxVersion uint16: TLS最高版本
  • tlsCipherSuites []uint16: TLS加密套件列表

Java

Java的TLS参数基本上都是基于keystore和truststore来配置的。一般常见设计如下参数:

  • keyStorePath: keystore文件路径
  • keyStorePassword: keystore密码
  • trustStorePath: truststore文件路径
  • trustStorePassword: truststore密码
  • tlsVerificationDisabled: 是否禁用TLS校验
  • tlsHostnameVerificationDisabled: 是否禁用TLS主机名校验,仅部分框架支持。
  • tlsVersions: TLS版本列表
  • tlsCipherSuites: TLS加密套件列表

JavaScript

JavaScript可以使用标准库里的tls.SecureContextOptions

Kotlin

kotlin的Tls与Java相同:

  • keyStorePath: keystore文件路径
  • keyStorePassword: keystore密码
  • trustStorePath: truststore文件路径
  • trustStorePassword: truststore密码
  • tlsVerificationDisabled: 是否禁用TLS校验
  • tlsHostnameVerificationDisabled: 是否禁用TLS主机名校验,仅部分框架支持。
  • tlsVersions: TLS版本列表
  • tlsCipherSuites: TLS加密套件列表

Python

推荐使用方式一

方式一

  • ssl.SSLContext: Python标准库的内置TLS结构体

方式二

Python可以使用文件路径以及加密的私钥文件。

  • tlsCertPath: 证书文件路径
  • tlsPrivateKeyPath: 私钥文件路径
  • tlsPrivateKeyPassword: 私钥密码
  • tlsMinVersion: TLS最低版本
  • tlsMaxVersion: TLS最高版本
  • tlsCipherSuites: TLS加密套件列表

Rust

由于常见的Rust TLS实现不支持加密的私钥文件,推荐使用文件内容,而不是文件路径,避免敏感信息泄露。 一般常见如下设计参数:

  • tls_cert_content Vec: 证书内容
  • tsl_private_key_content Vec: 私钥内容
  • tls_versions: TLS版本列表
  • tls_cipher_suites: TLS加密套件列表
  • tls_verification_disabled: 是否禁用TLS校验

根据Python项目的需求和特性,可以为Python的Http SDK项目选择以下命名方式:

  • xxx-client-python:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。
  • xxx-http-client-python:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-python:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

由于Python的调用方式通常是模块名.类名.方法名

TypeScript的调用方式通常是

1
2
import { ClassName } from 'moduleName';
const object = new ClassName();

根据TypeScript项目的需求和特性,可以为TypeScript的Http SDK项目选择以下命名方式:

  • xxx-client-ts:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。在npm可以注册为”xxx”。
  • xxx-http-client-ts:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-ts:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

根据Go项目的需求和特性,可以为Go的Http SDK项目选择以下命名方式:

  • xxx-client-go:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。
  • xxx-http-client-go:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-go:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

由于Go语言的调用方式是包名.结构体名.方法名,所以在设计SDK时,需要考虑包名、结构体名、方法名的设计。

以xxx业务为例,假设业务名为xxx,推荐包名也为xxx,结构体名为Client

目录布局可以是这样子的:

1
2
3
xxx-client-go/
|-- xxx/
| |-- client.go
0%