This seems more complicated than it needs to be. My suggestion would be the following:
Have a single service, running on a single port, with a single streaming method. The requests and replies are a union of the kinds of commands the client and server want to send to each other. These messages aren't synchronous, but they are in order. The "synchronous" APIs in the client aren't really synchronized at all, it's just the stub that converts it to be that way. In gRPC, every RPC is async.
What does this mean for you? You can fake a sync RPC by just waiting until you get the message type you want. As an example:
service Foo {
rpc Bar(stream BarRequest) returns (stream BarResponse);
}
message BarRequest {
oneof req {
message ClientSyncReq client_sync_req = 1;
message ServerSyncResp server_sync_resp = 2;
}
}
message BarResponse {
oneof resp {
message ClientSyncResp client_sync_resp = 1;
message ServerSyncReq server_sync_req = 2;
}
}
When you client wants to make a call, it issues a BarRequest with the ClientSyncReq field set, and has a switch statement on the response. When a ClientSyncResp message comes back, it will return the result to whoever made the original call. When the server wants to query the client, it sends a ServerSyncReq (note this is in the BarResponse!). The client responds by sending a "request" containing a ServerSyncResponse. Each side can control their own state which allows them to tell if calls have been made in a legal order.
Be aware that the client must initiate this interaction. The server cannot start the RPC, but once it is bootstrapped you can proceed.