GRPC, ProtoBuf, and HTTP


#1

Hi all, I am wondering if anyone has had some experience writing a GRPC client and/or server using the ProtoBuf.jl and HTTP.jl packages (both of which appear to be in good working order for 1.0). I found an old GRPC.jl package, but it relies on HTTP2.jl which doesn’t appear to have been updated in some time.

Protobuf.jl has an example of writing an RPC client and server (using tcp sockets) (https://github.com/JuliaIO/ProtoBuf.jl/blob/master/test/services/testsvc.jl), so I’d like to model what I’m doing off of that (except using HTTP requests instead). The problem is I can’t quite figure out how to get the functionality of creating an HTTP connection session object that I can interact with in HTTP.jl. For example, this is some of the older code in the GRPC.jl library (again relying on HTTP2.jl):

function write_request(channel::gRPCChannel, controller::gRPCController, service::ServiceDescriptor, method::MethodDescriptor, request)
    connection = channel.session
    path = "/" * service.name * "/" * method.name
    headers = Headers(":method" => "POST",
                      ":path" => path,
                      ":scheme" => "http",
                      "content-type" => "application/grpc+proto")

    channel.stream_id = Session.next_free_stream_identifier(connection)
    debug_log(controller, "Stream Id: $(channel.stream_id)")
    Session.put_act!(connection, Session.ActSendHeaders(channel.stream_id, headers, false))
    data_buff = to_delimited_message_bytes(request)
    Session.put_act!(channel.session, Session.ActSendData(channel.stream_id, data_buff, true))

    debug_log(controller, "sent data $(length(data_buff)) bytes")
    nothing
end

It is interacting with this HTTP session object. What would be the best way to fit such functionality in the newer HTTP.jl paradigm?

Thanks!


#2

Well, that’s kind of a fundamental difference between the http 1.x & http 2.0 protocols (i.e. native support for persistent sessions), so no, there’s not really a way to replicate that functionality w/ plain http. Now, you’d probably have better success by replacing the http2 parts w/ WebSockets.jl. websockets use a similar persistent connection to send messages back & forth; I don’t think it’d be too hard to adapt the code to work for this case, but I’m also far from an expert on websockets.


#3

Hi @pazzo83,

I just found your post while searching for HTTP2.
Both HTTP/1.1 and HTTP/2 support persistent bi-directional io (without WebSockets).
I believe the key difference between HTTP/1.1 and HTTP/2 is that the latter adds support for interleaving frames from multiple sessions on a single connection. What I have read about HTTP/2 suggests that while it implements compression and connection sharing and binary framing under the hood, it does not introduce any semantic user visible changes to HTTP/1.1.
If you need multiple simultaneous sessions with HTTP/1.1 you’ll need to open multiple connections. With HTTP/2 your code will look exactly the same, but your HTTP/2 library might quietly reuse an existing connection when it can.

WebSockets is useful if you’re communicating with a web browser, because web browsers have a user friendly javascript API for that protocol. However, HTTP/1.1 has bi-directional message framing built in so if you’re in control of the server and the client there is no need for WebSockets.

Here is a simple example of a bi-directional chat app using HTTP.jl:

using HTTP

function chat(io::HTTP.Stream)
    @async while !eof(io)
        write(stdout, readavailable(io), "\n")
    end
    while isopen(io)
        write(io, readline(stdin))
    end
end

chat_server() = HTTP.listen("127.0.0.1", 8087) do io::HTTP.Stream
    startwrite(io)
    chat(io)
end
    
chat_client() = HTTP.open("POST", "http://127.0.0.1:8087", #= verbose=3 =#) do io::HTTP.Stream
    startread(io)
    chat(io)
end

If you turn the verbose=3 client option on you can see how the framing works…

julia> include("chat.jl"); chat_client()
DEBUG: 2018-10-06T10:46:27.066 4d26 ➡️  "POST / HTTP/1.1\r\n" (write)
DEBUG: 2018-10-06T10:46:27.075 4d26 ➡️  "Host: 127.0.0.1\r\n" (write)
DEBUG: 2018-10-06T10:46:27.075 4d26 ➡️  "Transfer-Encoding: chunked\r\n" (write)
DEBUG: 2018-10-06T10:46:27.075 4d26 ➡️  "\r\n" (write)
DEBUG: 2018-10-06T10:46:28.645 4d26 ⬅️  "HTTP/1.1 200 OK\r\n" (readavailable)
DEBUG:                                 "Transfer-Encoding: chunked\r\n"
DEBUG:                                 "\r\n"
Hello I'm the client!
DEBUG: 2018-10-06T10:46:34.007 4d26 ➡️  "15\r\n" (write)
DEBUG: 2018-10-06T10:46:34.007 4d26 ➡️  "Hello I'm the client!" (unsafe_write)
DEBUG: 2018-10-06T10:46:34.007 4d26 ➡️  "\r\n" (write)
DEBUG: 2018-10-06T10:46:44.323 f3fd ⬅️  "13\r\n" (readavailable)
DEBUG:                                 "Hi, I'm the server.\r\n"
Hi, I'm the server.
Good to meet you.
DEBUG: 2018-10-06T10:46:56.524 4d26 ➡️  "11\r\n" (write)
DEBUG: 2018-10-06T10:46:56.525 4d26 ➡️  "Good to meet you." (unsafe_write)
DEBUG: 2018-10-06T10:46:56.525 4d26 ➡️  "\r\n" (write)
DEBUG: 2018-10-06T10:47:01.957 f3fd ⬅️  "8\r\n" (readavailable)
DEBUG:                                 "You too!\r\n"
You too!

#4

Did you get any further @pazzo83? I am also interested in getting a working gRPC server for use with protos.

I’ve seen two packages:

They don’t appear to be forks of one another.

Also, could someone fill me in on why the HTTP.jl package is important here? As I would have imagined that RPC would bypass HTTP and use TCP connections directly. What am I missing?


#5

I have not looked at the source, but HTTP could be used with grpc-web. However, the native gRPC protocol should be preferred when not restricted to http like browsers.