2021SC@SDUSC
在平时使用各种编程语言的过程中,我们都能接触到map,即key-value的存储方式。java中的map,python中的字典dict等等,都为key-value的存储方式。而在ActiveJ的RPC中,也存在着这一种key-value存储的hash,为RPC提供更高效,更快速的键值存取,使RPC的速度在有ActiveJ的序列化器的加持下变得更快,下面我们就来试着编码看一下与RpcExample不同的key-value模块client和server。
When writing distributed application the common concern is what protocol to use for communication. There are two main options:
编写分布式应用程序时,通常关注的是使用什么协议进行通信。有两个主要选项:
HTTP/REST
RPC
While HTTP is more popular and well-specified, it has some overhead. When performance is a significant aspect of application, you should use something faster. For this purpose ActiveJ RPC was designed based on fast serializers and custom optimized communication protocol that allows to significantly improve application performance.
虽然HTTP更受欢迎且指定明确,但它有一些开销。当性能是应用程序的一个重要方面时,您应该更快地使用某些东西。为此,ActiveJ RPC 基于快速串行器和自定义优化的通信协议进行设计,可显著提高应用程序性能。
可以看出ActiveJ的编写者对于RPC的优化协议的自信。
由于我们有两个基本操作要实现(put and get),让我们从查看用于客户端和服务器之间通信的消息类开始,特别是PutRequest,PutResponse,GetRequest和GetResponse。 这些类的实例由闪电般快速的序列化程序库ActiveJ 序列化程序进行序列化。它需要一些关于这些类的元信息,这些信息由适当的注释提供。基本规则是:
对需要序列化的属性使用@Serialize批注。
在构造函数中使用具有属性名称的@Deserialize批注。
对可以具有空值的属性使用@SerializeNullable。
消息类实现如下所示:
public class PutRequest {
private final String key;
private final String value;
public PutRequest(@Deserialize("key") String key, @Deserialize("value") String value) {
this.key = key;
this.value = value;
}
@Serialize
public String getKey() {
return key;
}
@Serialize
public String getValue() {
return value;
}
}
public class PutResponse {
private final String previousValue;
public PutResponse(@Deserialize("previousValue") String previousValue) {
this.previousValue = previousValue;
}
@Serialize
@SerializeNullable
public String getPreviousValue() {
return previousValue;
}
@Override
public String toString() {
return "{previousValue='" + previousValue + '\'' + '}';
}
}
public class GetRequest {
private final String key;
public GetRequest(@Deserialize("key") String key) {
this.key = key;
}
@Serialize
public String getKey() {
return key;
}
}
public class GetResponse {
private final String value;
public GetResponse(@Deserialize("value") String value) {
this.value = value;
}
@Serialize
@SerializeNullable
public String getValue() {
return value;
}
@Override
public String toString() {
return "{value='" + value + '\'' + '}';
}
}
然后是键值存储(key-value store)的简单实现:
public class KeyValueStore {
private final Map<String, String> store = new HashMap<>();
public String put(String key, String value) {
return store.put(key, value);
}
public String get(String key) {
return store.get(key);
}
}
可以看到,键值存储非常简约。它使用常规Java Map来存储具有相应值的键。
在介绍完基本的键值存储实现后,我们来看一下他是如何应用于客户端和服务器的。
首先来看一下AbstractModule:
public class ServerModule extends AbstractModule {
private static final int RPC_SERVER_PORT = 5353;
@Provides
Eventloop eventloop() {
return Eventloop.create()
.withEventloopFatalErrorHandler(rethrow());
}
@Provides
KeyValueStore keyValueStore() {
return new KeyValueStore();
}
@Provides
RpcServer rpcServer(Eventloop eventloop, KeyValueStore store) {
return RpcServer.create(eventloop)
.withSerializerBuilder(SerializerBuilder.create())
.withMessageTypes(PutRequest.class, PutResponse.class, GetRequest.class, GetResponse.class)
.withHandler(PutRequest.class, req -> Promise.of(new PutResponse(store.put(req.getKey(), req.getValue()))))
.withHandler(GetRequest.class, req -> Promise.of(new GetResponse(store.get(req.getKey()))))
.withListenPort(RPC_SERVER_PORT);
}
}
此时指示了在客户端和服务器之间发送的所有消息类,并为每个请求类指定了适当的RpcRequestHandler。
然后我们再来看一下RPC Server的ServerLauncher类。在这里ServerLauncher用于管理生命周期。
public class ServerLauncher extends Launcher {
@Inject
private RpcServer server;
@Override
protected Module getModule() {
return combine(
ServiceGraphModule.create(),
new ServerModule());
}
@Override
protected void run() throws Exception {
awaitShutdown();
}
public static void main(String[] args) throws Exception {
ServerLauncher launcher = new ServerLauncher();
launcher.launch(args);
}
}
同样地,我们需要重写getModule()和run()方法,重新绑定和修改具体运行方式。
然后我们来看一看客户端,这里再次指出了用于通信的所有消息类,并指定了RPC 策略。所有策略都可以组合,但由于我们只有一台服务器,因此我们使用单服务器策略:
public class ClientModule extends AbstractModule {
private static final int RPC_SERVER_PORT = 5353;
@Provides
Eventloop eventloop() {
return Eventloop.create()
.withEventloopFatalErrorHandler(rethrow())
.withCurrentThread();
}
@Provides
RpcClient rpcClient(Eventloop eventloop) {
return RpcClient.create(eventloop)
.withConnectTimeout(Duration.ofSeconds(1))
.withSerializerBuilder(SerializerBuilder.create())
.withMessageTypes(PutRequest.class, PutResponse.class, GetRequest.class, GetResponse.class)
.withStrategy(RpcStrategies.server(new InetSocketAddress("localhost", RPC_SERVER_PORT)));
}
}
同理,我们来看看RPC Client的ClientLauncher,在run()方法内我们考虑命令行参数并向RpcServer发出适当的请求。
public class ClientLauncher extends Launcher {
private static final int TIMEOUT = 1000;
@Inject
private RpcClient client;
@Inject
Eventloop eventloop;
@Override
protected Module getModule() {
return combine(
ServiceGraphModule.create(),
new ClientModule());
}
@Override
protected void run() throws Exception {
if (args.length < 2) {
System.err.println("Command line args:\n\t--put key value\n\t--get key");
return;
}
switch (args[0]) {
case "--put":
CompletableFuture<PutResponse> future1 = eventloop.submit(() ->
client.sendRequest(new PutRequest(args[1], args[2]), TIMEOUT)
);
PutResponse putResponse = future1.get();
System.out.println("PutResponse: " + putResponse);
break;
case "--get":
CompletableFuture<GetResponse> future2 = eventloop.submit(() ->
client.sendRequest(new GetRequest(args[1]), TIMEOUT)
);
GetResponse getResponse = future2.get();
System.out.println("GetResponse: " + getResponse);
break;
default:
throw new RuntimeException("Unsupported option: " + args[0]);
}
}
public static void main(String[] args) throws Exception {
ClientLauncher launcher = new ClientLauncher();
launcher.launch(args);
}
}
然后我们来测试一下他具体是怎么样进行客户端服务器互连,并且实现相关的远程键值存储的。
首先,我们启动serverLauncher启动server,等待客户端进行连接:
在完成了一系列的start后,服务器因为run()方法,阻塞在这里等待客户端发送信息:
16:25:24.704 [main] INFO ServerLauncher - === RUNNING APPLICATION
然后我们打开客户端命令行,并输入参数“ --put test_key Hello_World!”
服务器端会有对应的输出如下:
16:28:08.900 [eventloop: pool-2-thread-1] INFO io.activej.rpc.server.RpcServer - Client connected on RpcServerConnection{address=/127.0.0.1,active=1, successes=0, failures=0}
16:28:08.922 [eventloop: pool-2-thread-1] INFO io.activej.rpc.server.RpcServer - Client disconnected on RpcServerConnection{address=/127.0.0.1,active=0, successes=1, failures=0}
表示客户端已连接,并在发送消息后断开连接。
而此时客户端会接收到自己完成“ --put test_key Hello_World!”后服务器端返回的消息:
16:28:08.905 [main] INFO ClientLauncher - === RUNNING APPLICATION
PutResponse: {previousValue=‘null’}
然后我们再启动另一个客户端,并在命令行加入参数“–get test_key”,让我们看一下会发生什么:
首先服务器有在此时和客户端连接:
16:31:02.805 [eventloop: pool-2-thread-1] INFO io.activej.rpc.server.RpcServer - Client connected on RpcServerConnection{address=/127.0.0.1,active=1, successes=0, failures=0}
16:31:02.888 [eventloop: pool-2-thread-1] INFO io.activej.rpc.server.RpcServer - Client disconnected on RpcServerConnection{address=/127.0.0.1,active=0, successes=1, failures=0}
然后我们来看看客户端的输出:
16:31:02.879 [main] INFO ClientLauncher - === RUNNING APPLICATION
GetResponse: {value=‘Hello_World!’}
成功了!我们得到了在server中存储的对应key的value,实现了远程键值对调用,且在不使用HTTP的情况下!
相比往前的不断阅读源代码,看到实际通过RPC实现远程键值调用的时候感觉很棒。就如同以往在第一次接触java打印出“Hello World!”的时候,并且自己也已知道内部RPC是如何实际运行的。在下一章,我们将继续RPC的最后一部分,实现一个类Memcached分布式的高速缓存系统的应用程序。