Как я могу полностью перехватить унарный вызов Java gRPC на клиенте и на сервере? - PullRequest
0 голосов
/ 16 января 2019

Я перемещаю кодовую базу распределенных систем из SOAP (JAX-WS) в gRPC-java. Мы используем эту кодовую базу для обучения удаленным вызовам, отказоустойчивости, реализации безопасности.

В архитектуре JAX-WS существует класс-перехватчик (называемый обработчиком SOAP), который может перехватывать сообщения SOAP. Вы можете настроить обработчики на клиенте и на сервере.

Для справки, это полная последовательность для удаленного вызова на JAX-WS:

  • Клиент - создать порт (заглушку) и вызвать удаленный метод
  • Stub - конвертировать объекты Java в сообщение SOAP (XML)
  • ClientHandler - перехватывает исходящее SOAP-сообщение и может читать / записывать его
  • Сеть - передано сообщение запроса SOAP
  • ServerHandler - перехватывает входящее SOAP-сообщение, может читать / писать
  • Tie - преобразовать SOAP-сообщение в объекты Java
  • Сервер - выполнить метод, ответить
  • ServerHandler - перехватывает исходящий SOAP-ответ, может читать / писать
  • Сеть - передано ответное сообщение SOAP
  • Клиент - создать порт (заглушку) и вызвать удаленный метод
  • Stub - преобразовать объекты Java в сообщение SOAP (XML)
  • ClientHandler - перехватывает входящее SOAP-сообщение
  • Клиент - получает ответ

При таком подходе мы можем создавать обработчики для регистрации сообщений SOAP и для обеспечения безопасности, такой как цифровая подпись или шифрование.

Я пытаюсь использовать аналогичные возможности с gRPC на Java (v1.17.2).

Я основал свой код gRPC в этом уроке google , простом привет мире с унарным методом.

На основании этих примеров я написал ClientInterceptor :

package example.grpc.client;
import java.util.Set;
import io.grpc.*;

public class HelloClientInterceptor implements ClientInterceptor {

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor,
        CallOptions callOptions, Channel channel) {
    return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
            channel.newCall(methodDescriptor, callOptions)) {

        @Override
        public void sendMessage(ReqT message) {
            System.out.printf("Sending method '%s' message '%s'%n", methodDescriptor.getFullMethodName(),
                    message.toString());
            super.sendMessage(message);
        }

        @Override
        public void start(Listener<RespT> responseListener, Metadata headers) {
            System.out.println(HelloClientInterceptor.class.getSimpleName());

            ClientCall.Listener<RespT> listener = new ForwardingClientCallListener<RespT>() {
                @Override
                protected Listener<RespT> delegate() {
                    return responseListener;
                }

                @Override
                public void onMessage(RespT message) {
                    System.out.printf("Received message '%s'%n", message.toString());
                    super.onMessage(message);
                }
            };

            super.start(listener, headers);
        }
    };
}

}

Я создал ServerInterceptor :

package example.grpc.server;

import java.util.Set;

import io.grpc.*;

public class HelloServerInterceptor implements ServerInterceptor {

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata,
        ServerCallHandler<ReqT, RespT> serverCallHandler) {
    // print class name
    System.out.println(HelloServerInterceptor.class.getSimpleName());

    return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
}

}

Вот (наконец) мои вопросы:

  1. Как сервер-перехватчик может увидеть сообщение до и после выполнения метода?
  2. Как сервер-перехватчик может изменить сообщение?
  3. Как клиент-перехватчик может изменить сообщение?

Конечная цель состоит в том, чтобы иметь возможность написать CipherClientHandler и CipherServerHandler, которые бы шифровали байты сообщения по проводам. Я знаю, что TLS - верный способ сделать это на практике, но я хочу, чтобы студенты делали индивидуальную реализацию.

Спасибо за любые указатели в правильном направлении!

1 Ответ

0 голосов
/ 17 января 2019
  1. Под «выполнением метода» я предполагаю, что вы имеете в виду «Сервер - выполнить метод, ответить» ранее. Точное время вызова серверного метода не является частью API перехвата и не должно зависеть от него. Сегодня с обработчиками асинхронных серверов случается, что метод сервера вызывается при вызове serverListener.halfClose(). Но опять же, это не должно зависеть от. Непонятно, зачем это нужно.

  2. Сервер-перехватчик получает ReqT message для запроса и RespT message для ответа. Чтобы изменить сообщения, просто измените эти сообщения перед вызовом super.

  3. Клиент-перехватчик может делать то же самое, что и сервер-перехватчик; измените сообщение перед его передачей.

Обратите внимание, что когда я говорю "изменить сообщение", оно обычно реализуется как "сделать копию сообщения с соответствующими изменениями".

Но если вы захотите зашифровать / расшифровать сообщения, это не так легко вытекает из API, потому что вы полностью меняете их тип. Вам дают ReqT, и вы превратите это в байты. Для этого вы должны изменить MethodDescriptor s.

На стороне клиента это можно сделать в течение start() и предоставить свои Marshaller s для MethodDescriptor.Builder. У вас есть доступ к исходному приложению MethodDescriptor, поэтому вы можете использовать его для сериализации в байты.

Marshaller ENCRYPTING_MARSHALLER = new Marshaller<InputStream>() {
  @Override
  public InputStream parse(InputStream stream) {
    return decrypt(stream);
  }

  @Override
  public InputStream stream(InputStream stream) {
    return encrypt(stream);
  }
};

public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
        MethodDescriptor<ReqT, RespT> methodDescriptor,
        CallOptions callOptions, Channel channel) {
  ClientCall<InputStream, InputStream> call = channel.newCall(
      methodDescriptor.toBuilder(
        ENCRYPTING_MARSHALLER, ENCRYPTING_MARSHALLER),
      callOptions);
  // Can't use Forwarding* because the generics would break.
  // Note that all of this is basically boilerplate; the marshaller is
  // doing the work.
  return new ClientCall<ReqT, RespT>() {
    @Override
    public void halfClose() {
      call.halfClose();
    }
    // ... ditto for _all_ the other methods on ClientCall

    @Override
    public void sendMessage(ReqT message) {
      call.sendMessage(methodDescriptor.streamRequest(message));
    }

    @Override
    public void start(Listener<RespT> listener, Metadata headers) {
      call.start(new Listener<InputStream>() {
        @Override
        public void onHalfClose() {
          listener.onHalfClose();
        }
        // ... ditto for _all_ the other methods on Listener

        @Override
        public void onMessage(InputStream message) {
          listener.onMessage(methodDescriptor.parseResponse(message));
        }
      }, headers);
    }
  };
}

На стороне сервера, как правило, будет похоже, но немного сложнее, поскольку вам нужно будет перестроить ServerServiceDefinition, что нельзя сделать обычным перехватчиком. Но, оказывается, есть утилита, которая делает шаблон:

ssd = ServerInterceptors.useMarshalledMessages(ssd, ENCRYPTING_MARSHALLER);
...