<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hongxi</title>
    <description>The latest articles on DEV Community by Hongxi (@javahongxi).</description>
    <link>https://dev.to/javahongxi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107418%2Fcf8054e5-6a95-40f5-9b25-1e6b24907df6.png</url>
      <title>DEV Community: Hongxi</title>
      <link>https://dev.to/javahongxi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javahongxi"/>
    <language>en</language>
    <item>
      <title>I Dropped the gRPC Dependency and Hand-Wrote the Wire Format. grpcurl Still Works.</title>
      <dc:creator>Hongxi</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:04:57 +0000</pubDate>
      <link>https://dev.to/javahongxi/i-dropped-the-grpc-dependency-and-hand-wrote-the-wire-format-grpcurl-still-works-1i62</link>
      <guid>https://dev.to/javahongxi/i-dropped-the-grpc-dependency-and-hand-wrote-the-wire-format-grpcurl-still-works-1i62</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; My RPC framework's &lt;code&gt;jaws-wire&lt;/code&gt; module speaks the standard gRPC wire format — 5-byte length-prefixed frames over HTTP/2, status in trailers — with zero dependency on grpc-java. A stock grpc-java client and &lt;code&gt;grpcurl&lt;/code&gt; can call it, and it can call a stock gRPC server. The whole module is 4,604 lines, and its only dependencies are the framework core and &lt;code&gt;protobuf-java&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why would anyone do this?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/javahongxi/jaws" rel="noopener noreferrer"&gt;JAWS&lt;/a&gt; (Java Async Wire Service) is a ~23K-line RPC framework built for one purpose: &lt;strong&gt;you can read it end-to-end&lt;/strong&gt;. Read the whole core, understand how an industrial RPC actually works, then go read Dubbo ten times faster.&lt;/p&gt;

&lt;p&gt;That mission has a corollary: every layer must be &lt;em&gt;ours to read&lt;/em&gt;. The moment we wanted gRPC interoperability, the default answer — "add grpc-java, it's right there" — was wrong for this project. grpc-java is a magnificent library, but it would become an opaque wall exactly where the interesting part is: how gRPC actually rides on HTTP/2.&lt;/p&gt;

&lt;p&gt;So the rule became: &lt;strong&gt;speak the protocol, don't marry the library.&lt;/strong&gt; Use protobuf-java for message encoding (no reason to re-derive varint), and hand-write everything above it: the framing, the HTTP/2 mapping, trailers, status codes, deadlines, keepalive, compression.&lt;/p&gt;

&lt;p&gt;What follows is what the wire format actually looks like when you strip the library away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gRPC wire format is smaller than you think
&lt;/h2&gt;

&lt;p&gt;gRPC over HTTP/2 is famously "just" a convention. Famous, yet rarely seen with your own eyes. Here is the entire message framing, from &lt;code&gt;WireFrameCodec&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each gRPC message on the wire is framed as:&lt;/span&gt;
&lt;span class="c1"&gt;//   [1 byte compressed-flag] [4 bytes big-endian length] [payload bytes]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One byte saying whether the payload is compressed, four bytes of length, then raw protobuf bytes. Everything else in the protocol is HTTP/2 itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request&lt;/strong&gt;: &lt;code&gt;HEADERS&lt;/code&gt; frame with pseudo-headers &lt;code&gt;:method POST&lt;/code&gt;, &lt;code&gt;:path /Service/Method&lt;/code&gt;, &lt;code&gt;content-type: application/grpc&lt;/code&gt;, plus optional &lt;code&gt;grpc-encoding&lt;/code&gt;, &lt;code&gt;grpc-timeout&lt;/code&gt;, and your custom metadata — followed by &lt;code&gt;DATA&lt;/code&gt; frames carrying length-prefixed messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt;: &lt;code&gt;HEADERS&lt;/code&gt; with &lt;code&gt;:status 200&lt;/code&gt; and &lt;code&gt;content-type&lt;/code&gt;, then &lt;code&gt;DATA&lt;/code&gt; frames with the reply, then a final &lt;code&gt;HEADERS&lt;/code&gt; frame — the &lt;em&gt;trailers&lt;/em&gt; — carrying &lt;code&gt;grpc-status: 0&lt;/code&gt; and optionally &lt;code&gt;grpc-message&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "status in trailers" trick is the part that surprises people. gRPC does not put its status code in the HTTP status. An RPC that failed with &lt;code&gt;NOT_FOUND&lt;/code&gt; travels over HTTP/2 with &lt;code&gt;:status 200&lt;/code&gt; — the &lt;em&gt;transport&lt;/em&gt; succeeded — and the &lt;em&gt;application&lt;/em&gt; status rides in trailing headers after the last DATA frame. If an error occurs before any message is sent, the server may collapse everything into a single "trailers-only" response: status, content-type, and grpc-status in one HEADERS frame.&lt;/p&gt;

&lt;p&gt;Once you see this shape, the whole protocol stops being magic. It's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HEADERS (method, path, metadata)
DATA    [flag|length|protobuf]
DATA    [flag|length|protobuf]   ← streaming = more frames
HEADERS (grpc-status, grpc-message)  ← trailers, always present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The server: Netty HTTP/2, no stubs
&lt;/h2&gt;

&lt;p&gt;On the server side, &lt;code&gt;WireServer&lt;/code&gt; starts a Netty HTTP/2 server and routes each stream by its &lt;code&gt;:path&lt;/code&gt;. Business logic is registered per method, not code-generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WireHandlerRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WireHandlerRegistry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"interop.Greeter"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SayHello"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WireMethodHandler&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;WireCallContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;HelloRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloRequest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttachment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-trace-id"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from gRPC metadata&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HelloReply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"! (from jaws-wire)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Parser&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getRequestParser&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HelloRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No protoc plugin, no generated &lt;code&gt;GreeterGrpc.GreeterImplBase&lt;/code&gt;, no &lt;code&gt;StreamObserver&lt;/code&gt; boilerplate. You parse what you declare, you return what you build. The handler contract has exactly two jobs.&lt;/p&gt;

&lt;p&gt;For server streaming, the handler returns a &lt;code&gt;Flow.Publisher&amp;lt;Message&amp;gt;&lt;/code&gt; — the JDK 9+ reactive-streams interface — instead of taking a callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"interop.Greeter"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SayHelloStream"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WireMethodHandler&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;MethodType&lt;/span&gt; &lt;span class="nf"&gt;methodType&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;MethodType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SERVER_STREAMING&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Publisher&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;WireCallContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;HelloRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloRequest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subscriber&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;subscriber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSubscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;@Override&lt;/span&gt;
                &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;subscriber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloReply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello #"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"!"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;subscriber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onComplete&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="c1"&gt;// ...&lt;/span&gt;
            &lt;span class="o"&gt;});&lt;/span&gt;
        &lt;span class="o"&gt;};&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose &lt;code&gt;Flow.Publisher&lt;/code&gt; deliberately: it's in the JDK, it composes, and it makes backpressure an explicit part of the contract rather than something a generated stub hides from you.&lt;/p&gt;

&lt;p&gt;One detail I'm fond of: &lt;code&gt;WireServer&lt;/code&gt; automatically registers the standard &lt;code&gt;grpc.health.v1.Health&lt;/code&gt; service, hand-written against the vendor proto without protoc. So Kubernetes gRPC health probes and &lt;code&gt;grpcurl&lt;/code&gt; health checks work out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status codes: where semantics live or die
&lt;/h2&gt;

&lt;p&gt;Here's the part where "protocol-compatible" projects usually quietly fail. If your error mapping is lazy — every error becomes &lt;code&gt;INTERNAL&lt;/code&gt; — standard clients degrade: they won't retry calls they should retry, and they'll misreport deadline misses. The gRPC status code &lt;em&gt;is&lt;/em&gt; the contract.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;WireStatus&lt;/code&gt; maintains a bidirectional, semantically honest mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mapping rules (jaws → gRPC):&lt;/span&gt;
&lt;span class="c1"&gt;//   SERVICE_TIMEOUT (40003)      → DEADLINE_EXCEEDED (4)&lt;/span&gt;
&lt;span class="c1"&gt;//   Connection/transport failure → UNAVAILABLE (14) — retryable&lt;/span&gt;
&lt;span class="c1"&gt;//   Business exceptions          → UNKNOWN&lt;/span&gt;
&lt;span class="c1"&gt;//   Service not found            → NOT_FOUND&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the reverse direction on the client side, so that when JAWS calls a &lt;em&gt;gRPC server&lt;/em&gt; and gets &lt;code&gt;UNAVAILABLE&lt;/code&gt;, it knows that failure is retryable — the failover cluster can pick another node. The timeout mapping carries &lt;code&gt;grpc-timeout&lt;/code&gt; end-to-end — the client's deadline travels in the header, the server applies it to its dispatch future — and I verified it the honest way: give &lt;code&gt;grpcurl&lt;/code&gt; a deadline tighter than the streaming method takes to finish, and it reports the miss exactly as it would against grpc-java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grpcurl &lt;span class="nt"&gt;-plaintext&lt;/span&gt; &lt;span class="nt"&gt;-max-time&lt;/span&gt; 0.15 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    -import-path &amp;lt;proto-dir&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-proto&lt;/span&gt; greeter.proto &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    localhost:50051 greeter.Greeter/SayHelloStream
ERROR:
  Code: DeadlineExceeded
  Message: context deadline exceeded
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;code&gt;grpcurl&lt;/code&gt; — a real gRPC client that knows nothing about my framework — agreeing with my server on deadline semantics. Not a stub echo. A standards tool testifying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof: two directions, both ways
&lt;/h2&gt;

&lt;p&gt;Interop claims need heterogeneity. Testing my client against my server proves nothing — reflection hides naming mismatches from the same codebase. So the &lt;code&gt;jaws-sample-wire-interop&lt;/code&gt; module demonstrates &lt;strong&gt;both directions against real gRPC&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction 1 — grpc-java client → JAWS server.&lt;/strong&gt; A stock &lt;code&gt;ManagedChannel&lt;/code&gt; with &lt;code&gt;MetadataUtils&lt;/code&gt; attaches &lt;code&gt;x-trace-id&lt;/code&gt;; the server reads it from &lt;code&gt;WireCallContext&lt;/code&gt; and echoes it back. Unary and server-streaming both work, metadata flows end-to-end through HTTP/2 headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction 2 — JAWS client → grpc-java server.&lt;/strong&gt; &lt;code&gt;WireClient&lt;/code&gt; connects to a standard gRPC server (started with the actual grpc-java API) and calls its methods with full filter-chain support — load balancing, auth, metrics all apply, because &lt;code&gt;jaws-wire&lt;/code&gt; is a first-class protocol inside the framework, not a bolt-on bridge.&lt;/p&gt;

&lt;p&gt;Run the interop demos yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;./mvnw -q compile exec:java -pl jaws-samples/jaws-sample-wire-interop -am \
    -Dexec.mainClass="org.hongxi.jaws.sample.wire.interop.GrpcCallWireDemo"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or start the plain wire sample and poke it with grpcurl (no server reflection — you point grpcurl at the proto file, just like with any gRPC server that doesn't enable reflection):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;./run-sample.sh wire        #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;starts a JAWS server on :50051 speaking gRPC wire format
&lt;span class="gp"&gt;grpcurl -plaintext -import-path &amp;lt;proto-dir&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-proto&lt;/span&gt; greeter.proto &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -d '{"name": "grpcurl"}' localhost:50051 greeter.Greeter/SayHello
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the protocol taught me
&lt;/h2&gt;

&lt;p&gt;Hand-writing the wire format converted gRPC from "a library I use" into "a protocol I understand." A few things that only became real when I had to write them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trailers force async thinking.&lt;/strong&gt; You cannot write &lt;code&gt;grpc-status&lt;/code&gt; until the RPC's fate is known, which means your response path must be structured around completion — a whenComplete, not a return. This shaped the entire async dispatch pipeline in the framework core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata is just headers, and headers need rules.&lt;/strong&gt; gRPC metadata maps to HTTP/2 headers, but only some of them: custom keys must be lowercase and (for ASCII values) &lt;code&gt;[a-z0-9_-.]&lt;/code&gt;, and the &lt;code&gt;-bin&lt;/code&gt; suffix has a special meaning for binary values. Getting the mapping bidirectionally right — including which headers to strip on the way in and which to add on the way out — is fiddly, protocol-grade work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The features you thought were "gRPC" are mostly policies.&lt;/strong&gt; Keepalive PING strategy, &lt;code&gt;too_many_pings&lt;/code&gt; GOAWAY, gzip content coding, &lt;code&gt;RST_STREAM&lt;/code&gt; as cancellation, max message size limits — none of these are in the framing. They're HTTP/2 mechanics applied with gRPC's chosen policies. Implementing each one was a focused lesson in a different corner of the HTTP/2 spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger
&lt;/h2&gt;

&lt;p&gt;What does the whole thing cost? The &lt;code&gt;jaws-wire&lt;/code&gt; module is &lt;strong&gt;4,604 lines&lt;/strong&gt; — frame codec, HTTP/2 server and client handlers, status mapping, compression, health service, streaming glue — with exactly two dependencies: the framework core and &lt;code&gt;protobuf-java&lt;/code&gt;. Compare that to grpc-java's core alone, and remember that this includes no code generation step for your services.&lt;/p&gt;

&lt;p&gt;I won't pretend this replaces grpc-java for production use — it doesn't aim to. There is no proxy support, no load-balancing delegation to an external name resolver, none of the hardening that comes from a decade of production fires. What it replaces is &lt;em&gt;ignorance&lt;/em&gt;. Every one of those 4,604 lines is readable in an afternoon, and together they form a complete, working skeleton of how gRPC actually works.&lt;/p&gt;

&lt;p&gt;That was the goal. The framework is called "the RPC skeleton you can read end-to-end," and now the gRPC wire format is part of what you get to read.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://github.com/javahongxi/jaws" rel="noopener noreferrer"&gt;github.com/javahongxi/jaws&lt;/a&gt; — &lt;code&gt;./run-sample.sh wire&lt;/code&gt; is one command away from a gRPC-compatible server on your machine. The interop module shows both directions against stock gRPC. Stars and issues welcome, in any language.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover image: the JAWS poster — a shark swimming through circuitry. Because "JAWS".&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>netty</category>
      <category>grpc</category>
      <category>rpc</category>
    </item>
  </channel>
</rss>
