HTTP/2 使用 HTTPClient 的 C 客户端 gRPC 的客户端前言字符串丢失或损坏

HTTP/2 client preface string missing or corrupt for C client gRPC using HTTPClient

我收到“HTTP/2 客户端前言字符串丢失或损坏。”

我认为这与 headers 设置不正确有关。很可能是WifiClient/WifiSecureClient的执行。几个星期以来我一直在考虑这个问题,但我被卡住了。有什么建议吗?

[更新:下面的答案]

客户端是使用 nanopb 协议缓冲区编译器生成的:

protoc --plugin=protoc-gen-nanopb=~/grpc/nanopb/generator/protoc-gen-nanopb --nanopb_out=. helloworld.proto

Arduino 客户端:

DHT dht(DHTPIN, DHTTYPE);

WiFiClient client;
//WiFiClientSecure client;

void setup() {
  Serial.setDebugOutput(true);
  Serial.begin(115200);
  delay(10);

  WiFi.begin("<SSID>", "<My Password>");
  delay(3000);

  while (WiFi.status() != WL_CONNECTED) {
    Serial.println("WIFI connection failed, reconnecting...");
    delay(2000);
  }

  Serial.print("WiFi connected, ");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  Serial.println("Starting DHT11 sensor...");
  dht.begin();
}


void loop() {
    Serial.print("connecting to ");
    Serial.println(addr);
    
//    client.setInsecure(); 
  if (!client.connect(addr, port)) {
    Serial.println(addr);
    Serial.println(port);
    Serial.println("connection failed");
    Serial.println("wait 5 sec to reconnect...");
    delay(5000);
    return;
  }

  Serial.println("reading humidity/temp...");
  float hum = dht.readHumidity();
  float tmp = dht.readTemperature(true);

  Serial.println(hum);
  Serial.println(tmp);

  if (isnan(hum) || isnan(tmp)) {
    Serial.println("failed to read sensor data");
    delay(2000);
    return;
  }

  float hiCel = dht.computeHeatIndex(tmp, hum, true);

  helloworld_TempEvent temp = helloworld_TempEvent_init_zero;
  temp.deviceId = 1;
  temp.eventId = 0;
  temp.humidity = hum;
  temp.tempCel = tmp;
  temp.heatIdxCel = hiCel;

  sendTemp(temp);
  delay(1000);
}

void sendTemp(helloworld_TempEvent e) {
  uint8_t buffer[128];
  pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));

  if (!pb_encode(&stream, helloworld_TempEvent_fields, &e)) {
    Serial.println("failed to encode temp proto");
    Serial.println(PB_GET_ERROR(&stream));
    return;
  }
  
  Serial.print("sending temp...   ");
  Serial.println(e.tempCel);
  client.write(buffer, stream.bytes_written);
}

服务器是使用标准 java protocol buffer 编译器生成的。我唯一改变的是添加一个 TempEvent(如下)。

... (helloworld template stuff) ...

// The request message containing temperatures
message TempEvent {
    int32 deviceId = 1;
    int32 eventId = 2;
    float humidity = 3;
    float tempCel = 4;
    float heatIdxCel = 5;
}

示例 java 客户端运行没有任何问题。我的问题出在使用 gRPC 发送数据的 ESP8266-01 wifi 模块上使用 nanopb 的简单客户端。

public class Server {

    // Doesn't work
    public static void main(String[] args) throws IOException, InterruptedException {
        io.grpc.Server server = ServerBuilder
                .forPort(8080)
                .addService(new HelloServiceImpl()).build();
        server.start();
        server.awaitTermination();
    }

    // Works just fine
    public static void main(String[] args) throws IOException, InterruptedException {

        try (ServerSocket server = new ServerSocket(8080)) {
            System.out.println("Server accepting connections on port " + server.getLocalPort());
            TemperatureClient tempClient = new TemperatureClient();
            while(true) {

                Socket client = server.accept();
                System.out.println("Client connected using remote port " + client.getPort());

                final Thread t = new Thread(() -> {
                    try {
                        TempEvent p = TempEvent.parseFrom(client.getInputStream());
                        float i = p.getTempCel();
                        System.out.println("TEMP " + i);
                    } catch (IOException ioe) {
                        ioe.printStackTrace();
                    }
                });
                t.start();
            }
        }
    }

客户端能够访问服务器:

Nov 29, 2021 5:49:30 PM io.grpc.netty.shaded.io.grpc.netty.NettyServerTransport notifyTerminated
INFO: Transport failed
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 080c10641d0000d84125e17aa0422de4459e42
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:108)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:306)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:239)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:438)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:447)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.grpc.netty.shaded.io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor.run(SingleThreadEventExecutor.java:989)
    at io.grpc.netty.shaded.io.netty.util.internal.ThreadExecutorMap.run(ThreadExecutorMap.java:74)
    at io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:831)

为了调试这个,我想先看看我是否可以使用 grpcurl,但我明白了:

localhost@pro ~ % grpcurl -plaintext localhost:50051 list
Failed to list services: server does not support the reflection API

localhost@pro ~ % grpcurl -insecure localhost:50051 list
Failed to dial target host "localhost:50051": tls: first record does not look like a TLS handshake

我开始研究在我的实现代码中使用的 WifiClient.h 的实现,但是有没有人想过一种简单的方法来测试它而不深入研究所有内容?我原以为这应该非常简单……但事实证明,生成一个简单的客户端比我想象的要复杂得多。我觉得我在这里错过了什么。

来自 here 上的其他论坛:“客户端和服务器不一致。通常这是因为一个是纯文本而另一个使用 TLS。但这也可能是由于 HTTP/1与某些环境中的 HTTP/2 相比。"

在查看 Go Lang 实现后,我刚刚尝试使用 WiFiClientSecure client.setInsecure(); // 无效,十六进制转储在下方。

17:36:33.030 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND SETTINGS: ack=false settings={MAX_CONCURRENT_STREAMS=2147483647, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192}
17:36:33.031 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND WINDOW_UPDATE: streamId=0 windowSizeIncrement=983041
17:36:33.063 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND GO_AWAY: lastStreamId=2147483647 errorCode=1 length=126 bytes=485454502f3220636c69656e74207072656661636520737472696e67206d697373696e67206f7220636f72727570742e204865782064756d7020666f72207265...
17:36:33.064 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] Sent GOAWAY: lastStreamId '2147483647', errorCode '1', debugData 'HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100d4010000d00303000000005c2f03aae7147c5f36'. Forcing shutdown of the connection.
Dec 10, 2021 5:36:33 PM io.grpc.netty.shaded.io.grpc.netty.NettyServerTransport notifyTerminated
INFO: Transport failed
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100d4010000d00303000000005c2f03aae7147c5f36
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:108)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:306)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:239)
    at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:438)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:447)
    at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.grpc.netty.shaded.io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
    at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor.run(SingleThreadEventExecutor.java:989)
    at io.grpc.netty.shaded.io.netty.util.internal.ThreadExecutorMap.run(ThreadExecutorMap.java:74)
    at io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:831)
WiFiClient client;
if (!client.connect(addr, port)) {

这形成了一个基本的 TCP 连接。然而,gRPC 是一个基于 HTTP/2 的复杂协议。目前您只是将原始 protobuf 消息写入 TCP 套接字,它可以用于通信,但肯定不是 gRPC 服务器所期望的。

Nanopb 本身不支持 gRPC。有一个third-party project添加它,但目前没有维护。