top of page

Use gRPC in the browser with gRPC-Web and .NET5

Build High-Performance Services using gRPC and .NET 5


In this article, we will explore Microsoft's gRPC-Web and how it can be used to create real-world gRPC services that can be invoked from the browsers.


(Note: If you are brand new to gRPC then you should check out this article at first. This will walk you through creating your first gRPC server and client using .NET 5).


1 — The Problem

In a previous article, we have seen that unlike REST, the gRPC service can’t be invoked from a browser (as there is simply no browser API that provides enough control over the requests).


The gRPC project template is configured to display the following warning when accessed from a browser:

Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit:https://go.microsoft.com/fwlink/?linkid=2086909

This is because gRPC relies on HTTP/2 trailing response headers (which are also called trailers) which are not supported by the browser APIs.


So let’s understand those trailing response headers by having a high-level overview of HTTP/2.

© HTTP 1.1 and HTTP/2 High-Level Structure


As you can see from the above diagram that HTTP/2 has a framing mechanism — which is an intermediate layer between HTTP/1.1 and the underlying transport protocol.


HTTP/ 2 messages are divided into frames, which are then embedded in a stream. Separating data and header frames opens the door to further optimizations (header compression and Multiplexing).


Using HTTP/2, a server can send multiple responses against one request so the default HTTP status 200/OK wouldn't be sufficient.


To address this issue — trailing headers are used to get meta-data about incoming responses. gRPC-status; for instance, can notify the client if a message in a stream was successfully received.


2 — The Solution

That’s where the gRPC Web comes in — It sits between the browser and gRPC server and acts as a proxy that is compatible with both HTTP 1.1 and HTTP 2.

Source: https://grpc.io/


The technology is not new and it’s based on grpc-web Javascript client. Microsoft released an experimental version earlier this year, and later released an official version which is production-ready and fully supported in the grpc-dot net project.


3 — Getting Started with gRPC-Web for .NET


Step — 1: Configure your gRPC Service Project

Add a new gRPC ASP.NET Core service using “gRPC Service” project template.

Instead of using the default greet.proto let’s add another proto-buf file (stock.proto) as shown below:

syntax = "proto3";

option csharp_namespace = "StockServices.Protos";

package stock;

service Stock 
{  
    rpc GetAllProducts(Empty) returns (stream Product);
    rpc AddProduct (Product) returns (Result);
 }

message Empty {}

message Result
{	
    bool status =1;	
    string msg=2;
}

message Product 
{
    string name = 1;
    uint32 code=2;
    uint32 stock =3;
}

This proto file contains a stock service which has two methods

  1. AddProduct — It adds the product using unary call.

  2. GetAllProducts — it returns a stream of products. To enable streaming we need to add stream prefix.

Then we have our custom messages for Product and Result.


Tip: When you add any protobuf file in the solution. Make sure the ‘build action’ is set to ‘protobuf compiler’ as shown below:

Set build action to Protobuf compiler


3. Add a new class — StockService. cs in Services folder.

You can then inherit your class from {service-name}.{service-name}.Base


using StockServices.Protos;
public class StockService  : Stock.StockBase{
}

Tip: grpc code generation will add .proto namespace automatically so you need to add it’s reference e.g — {your-name-space}.Protos


Define a static product store in our StockService class. This will hold all the product object that will be added by the client.

private static List<Product> _allProducts = new List<Product>();

Override the stub class methods by typing override. You will see all the methods defined in your proto file.

public override Task<Result> AddProduct(Product request, ServerCallContext context){}

We will add the following functionality to this method so it adds the product to our in-memory product store.

if (string.IsNullOrEmpty(request.Name))

return Task.FromResult<Result>(new Result { Msg = "Product Name Can't be nulled", Status = false });

if (_allProducts.FirstOrDefault(f => f.Code == request.Code) != null)

return Task.FromResult<Result>(new Result { Msg = "Product is already Added", Status = false });

_allProducts.Add(request);

return Task.FromResult<Result>(new Result { Msg = "Added. Total Products: "+_allProducts.Count.ToString(), Status = true });
}

Let’s also implement our streaming method which will simply stream all the products that were added in our product store to the client.

public override async Task GetAllProducts(Empty request, IServerStreamWriter<Product> responseStream, ServerCallContext context){
foreach (var each in _allProducts){
await responseStream.WriteAsync(each);
} 
}

Now to make this service callable from the browser, we have to perform the following steps:

  • Install Grpc.AspNetCore package

  • Add the following lines in the configure method of Startup.cs.


app.UseGrpcWeb();
app.UseEndpoints(
endpoints =>{endpoints.MapGrpcService<StockService>().EnableGrpcWeb();
});

This middleware will handle everything about grpc-web calls and our gRPC service doesn’t need to know anything about grpc-web.


That’s it — Your gRPC service can now be invoked from the browser.


Step — 2: Create your Web Client using (Blazor,SPA or Javascript)

Let’s add some HTML and Javascript client code to interact with this service.


This HTML file takes two inputs (Product Name and Code) and invokes AddProduct and getAllProducts methods in our gRPC service.


We can now add Add ‘Scripts’ folder in wwwroot to hold our script files e.g index.js

const{ Product, Empty, Result }=require('./stock_pb');
const{ StockClient }=require('./stock_grpc_web_pb');

var client = newStockClient(window.location.origin);

var txtName = document.getElementById('name');
var txtCode = document.getElementById('code');
var btnAddProduct = document.getElementById('addProduct');
var trProducts = document.getElementById('trProducts');

var getAllProducts = document.getElementById('getAllProducts');
var resultText = document.getElementById('result');

getAllProducts.onclick = function()
{
    trProducts.innerHTML="";
    getProductsStream();
};

btnAddProduct.onclick = function(){
    var request = newProduct();
    request.setName(txtName.value);
    request.setCode(txtCode.value);
    client.addProduct(request,{},(err,response)=>
    {
        resultText.innerHTML=htmlEscape(response.getMsg());
   });
};

function getProductsStream(){
    var request = newEmpty();
    streamingCall = client.getAllProducts(request,{});
    streamingCall.on('data', function(response){
    trProducts.innerHTML+="<tr><td>"
        +htmlEscape(response.getName())+"</td><td>"
        +htmlEscape(response.getCode())+"</td></tr>";
    });
    
    streamingCall.on('end', function()
    {
        console.log("Stream ended");
    });
};

function htmlEscape(str){
    return String(str)
        .replace(/&/g,'&amp;')
        .replace(/"/g,'&quot;')
        .replace(/'/g,'&#39;')
        .replace(/</g,'&lt;')
        .replace(/>/g,'&gt;');
 }

You can notice it requires ‘stock_pb’ and ‘stock_grpc_web_pb’ files. These are gRPC-Web JavaScript clients and messages which are generated using ‘protoc’ the gRPC-Web code generator plugin.

You can use the below command from Powershell to generate those files in your script folder.

protoc greet.proto --js_out=import_style=commonjs:CHANGE_TO_SCRIPTS_DIRECTORY --grpc-web_out=import_style=commonjs,mode=grpcwebtext:CHANGE_TO_SCRIPTS_DIRECTORY --plugin=protoc-gen-grpc-web=CHANGE_TO_PROTOC_GEN_GRPC_WEB_EXE_PATH

Note: Make sure ‘protoc’ and ‘Protoc-gen-grpc-web’ should be on your computer and discoverable from your PATH.


Let’s analyze our javascript code:

  1. This will create our gRPC client — we will use this object to invoke service methods.

var client = new StockClient(window.location.origin);

Messages can be initialized as below :

var request = new Product(); 
request.setName(txtName.value); 
request.setCode(txtCode.value);

Finally, we can invoke the gRPC service as

client.addProduct(request, {}, (err, response) => { resultText.innerHTML = htmlEscape(response.getMsg()); 
});

Below is our package.json file which must be present as we are using webpack to


{
    "version": "1.0.0",
    "name": "browser-server",
    "private": true,
    "devDependencies": {"@grpc/proto-loader": "^0.3.0",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.15.0",
    "grpc-web": "^1.0.0",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"  
}
}

©


Note: This example requires node.js and webpack so you need to update build targets.


Let’s build and run the project.


Try adding a new product — you will see your gRPC call with content-type ‘application/grpc-web-text’:

©


Unlike plain-text JSON, the response is base64 encoded (which contains binary bytes).


©


Server Streaming

Our demo app uses the Unary call for ‘Add Product’ feature and the streaming call for ‘Get All Products’. You can try adding a few products and then click ‘Get All Products’. This will result in loading all of the products that were added.



This is being done using server streaming — the client sends a gRPC call and gets a stream to read a sequence of messages back.

var request = new Empty(); 
streamingCall = client.getAllProducts(request, {}); 

streamingCall.on(‘data’, function (response) 
{ 

trProducts.innerHTML += “<tr><td>” + htmlEscape(response.getName()) + “</td><td>” + htmlEscape(response.getCode())+”</td></tr>”; });

If you want to know when the stream was ended— you can subscribe to this function:

streamingCall.on('end', function () 
{ 
       console.log("Stream ended");   
  });


Conclusion

I hope this article gave you a good idea of how to use gRPC-Web to communicate with gRPC Services. You may choose gRPC-Web over REST/JSON where performance is crucial and you need to harness the power of multiplexing or server streaming.


Source: Medium


The Tech Platform

0 comments
bottom of page