A HTTP/2 multiplexing demo with Spring WebFlux Netty and React
By: Kwong Hung Yip
Tags: Spring, Http2, Iex Cloud, Mongodb, React

The HTTP/2 multiplexing feature overcomes the head of line blocking limitation inherited from HTTP/1.1, which greatly improves the TCP connection throughput between the browser and backend server.

To demonstrate the capacity gained from this feature, this post presents an example, the Stock Quote Dashboard, which is a single React page that plots the intraday price chart and promps the real-time quote of 25 stocks, and updates it for every second, the page generates numerous requests which flood into the HTTP/2 channel between the browser and the Spring WebFlux Netty server. HTTP/1.1 is hardly to hande such loading, which makes it be a good example to demonstrate the HTTP/2 multiplexing feature.

There are two GitHub repo for the Stock Quote Dashboard example, one is for the React frontend, and another one is for the Spring Boot backend. Market Data showed in the example and this postr was collected from IEX Cloud which for demo only, stock quotes in this post does not reflect the price in market.

1. Technical highlights:

2. Application architecture

To focus on HTTP/2 multiplexing feature, and keep the architecture simple and clear, this example setup is for running on local environment. You will need a Node.js server to run the React frontend, also a Java Runtime to run the Spring Boot embedded Netty server. The Netty server supports HTTP/2 over HTTPS (h2) only, therefore I provided a keystore with a self-signed certificate to enable TLS configuration.

code#1) the server.http2.* and server.ssl.* properties in application.yml in the stock-dashboard-backend module to enable h2 for Netty Server:

...

server:

http2:

enabled: true

ssl:

key-store: classpath:my-service.p12

key-store-type: PKCS12

key-store-password: <your-keystore-password>

key-alias: my-cert

...

3. Fetch quotes from IEX and cache it in MongoDB

Figure#2 The data flow diagram of Stock Quote Dashboard application

The data flow started from calling the IEX cloud API with the WebFlux WebClient to collect the market data. Other than IEX cloud, I had checked out other market data providers, but I found that the data coverage of IEX are more comprehensive, they provide unlimited access for the UAT sandbox, and their free-tier is more generous. Even IEX offers 50,000 free calls per month, but because of the they have the rate limiting protection for free-tier account, therefore, I cannot serve the React frontend requests with IEX cloud API directly, instead of it, the requests query the cached market data which stored in the MongoDB Altas.

MongoDB Altas is the cloud-based MongoDB database service which also provides the free-tier, it has no rate limiting restriction which has sufficient bandwidth to support our numerous calls from React frontent. The cloud-based service also saved my time to set up the local MongoDB instance.

code#2) an example of using WebFlux WebClient as wrapper to call a IEX cloud function, which gets the historical price data of a stock within a time range.

public IEXHistoricalPrice[] historicalPrice(String symbol,LocalDate exactDate) {

MultiValueMap<String, String> params =

new LinkedMultiValueMap<String,String>();

params.add("token", apiToken);

params.add("chartByDay", "true");

params.add("exactDate",

exactDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")));



Mono<IEXHistoricalPrice[]> body =

webclient

.get()

.uri((urlBuilder) -> {

return urlBuilder

.path("/stock/{symbol}/chart")

.queryParams(params)

.build(symbol);

})

.retrieve()

.onStatus(HttpStatus::is4xxClientError, clientResponse -> {

log.error("Error when get delayedQuote",clientResponse);

return Mono.error(

new Exception(clientResponse.toString())

);

})

.bodyToMono(IEXHistoricalPrice[].class);

return body.block(); }

4. Fire requests with React Hooks

After the cached market data is ready in MongoDB Altas, the data flow goes to the React frontend. In the Stock Quote Dashboard page, each stock is presented by a StockCard component, at top of the component, it shows the simple stock profile, and then the real-time quote in the middle, and then the intraday price chart at the bottom, which is plotted by the rechart Area Chart.

Figure #3 The StockCard component.

The real-time quote and intraday price chart get updated for every second, it triggered by a setInterval() function, within function, it calls a async-await-fetch function to get the latest quote from the Netty backend. The JSON responses then are passed into a reducer function, which has been registered with the useReducer() for maintaining the state of the component, once latest quoted get reflected in the state, the real-time quote and the chart are rerendered and get updated.

The setInterval() function is called by the useEffect() functions which is triggered after the StockCard has been rendered.

code#3) one of the useEffect() function in the StockCard component, it is fired once the state variable “previous” has been set, the setInterval function within calls another async function “fetchIntradayDelta” to fetch intraday quotes for every second.

useEffect(() => {

if ( previous !== null && refreshInterval > 0 ) {

const timerId = setInterval(() => {

let since;

if (intradayRef.current.length > 0) {

since = intradayRef.current[intradayRef

.current.length - 1].timestamp;

} else {

since = `${moment(config.tradeDate)

.format('YYYY-MM-DD')}T09:30:00`;

}

fetchIntradayDelta(symbol,since,apiHost,dispatch);

},refreshInterval*1000 + randomDelay(20)); return () => clearTimeout(timerId);

}

},[previous,refreshInterval])

code#4) the “fetchIntradayDelta” is an async function to call the Netty backend, dispatch the response as the payload into the reducer fucntion.

async function fetchIntradayDelta(symbol,since,apiHost,dispatch) {

try {

const response = await fetch(`https://${apiHost}/

intraday-delta-demo?symbol=${symbol}&since=${since}`);

const json = await response.json();

dispatch({

type:"fetch/intraday-delta",

payload:json,since:since

});

} catch (err) {

console.log("fetchIntradayDelta error:"+err);

}

}

code#5) the reducer function, which has been registered with useReducer(), responses to update the component state, upon changing the state, the component UI will be rerender.

const reducer = (state, action) => {

let newState = { ...state };

if (action.type === "fetch/profile") {

newState = { ...state, profile: action.payload };

} else if (action.type === "fetch/quote") {

newState = { ...state, quote: action.payload };

} else if (action.type === "fetch/intraday-init") {

newState = {

...state,

previous: action.payload.previous,

intraday: action.payload.quote

};

} else if (action.type === "fetch/intraday-delta") {

if (Array.isArray(action.payload)) {

if (action.payload.length > 0) {

newState = {

...state,

intraday: [ ...state.intraday, ...action.payload]

};

} else if (action.since === `${moment(config.tradeDate)

.format('YYYY-MM-DD')}T15:59:00`) {

newState = { ...state, intraday: [] };

}

}

}

return newState;

}; const [ state, dispatch ] = useReducer(reducer,initState);

5. Handle requests with Webflux backend

Finally, the requests is sent to the Netty server backend through using the HTTP/2 protocol. Since the example implemented the WebFlux Functional Endpoint to handle the requests, therefore, the requests first go to the rounter function (code#6), and based on the request URI, the requests are dispatched to different handler function (code#7) to query different market data from Altas and return back to the React frontend.

code#6) the router function in StockQuoteRouter class registers the URIs and corresponding handler function served by the backend. The “/intraday-delta-demo” URI was called by previous React example.

@Bean

public RouterFunction<ServerResponse> route() {

return RouterFunctions.route()

.GET("/company", handler::company)

.GET("/previous", handler::previous)

.GET("/quote-demo", handler::realTimeQuoteDemo)

.GET("/intraday-init-demo", handler::intradayInitDemo)

.GET("/intraday-delta-demo", handler::intradayDeltaDemo)

.build();

}

code #7) the hander function “intradayDeltaDemo” in StockQuoteHandler class uses the mongoTemplate to fire a query to MongoDB Altas, and return the response to React frontend.

@Autowired

private MongoTemplate mongoTemplate; ... public Mono<ServerResponse>

intradayDeltaDemo(ServerRequest request) {



Optional<String> optSymbol = request.queryParam("symbol");

Optional<String> optSince = request.queryParam("since"); if (optSymbol.isEmpty()) {

return ServerResponse

.status(HttpStatus.BAD_REQUEST)

.bodyValue("parameter symbol is missing");

} LocalDateTime since = null;

if (optSince.isEmpty()) {

return ServerResponse

.status(HttpStatus.BAD_REQUEST)

.bodyValue("since parameter should be provided");

} else {

try {

since = LocalDateTime.parse(

optSince.get(),DateTimeFormatter.ISO_DATE_TIME);

} catch (DateTimeParseException e) {

return ServerResponse

.status(HttpStatus.BAD_REQUEST)

.bodyValue("invalid format for since parameter '"

+ optSince.get() + "', should be in

yyyy-mm-ddThh:mm:ss format");

}

} String symbol = optSymbol.get().toLowerCase();

LocalDateTime from = LocalDateTime.of(

since.toLocalDate(), LocalTime.of(9,30)); LocalTime now = LocalTime.now();

int interval = (now.getMinute()*60 + now.getSecond())%600; long mins = (long)Math.ceil((390d/600d)*interval);

LocalDateTime to = from.plusMinutes(mins); from = since;

//log.info("{} : {} - {}",symbol,from,to); Query query = query(

where("symbol")

.is(symbol)

.and("timestamp")

.gt(from)

.lte(to))

.with(Sort.by(Order.asc("timestamp"))); List<IntradayPrice> intraday = mongoTemplate.find(query,

IntradayPrice.class, "intraday-prices"); return ServerResponse.ok()

.contentType(MediaType.APPLICATION_JSON)

.bodyValue(intraday);

}

6. Conclusion

The Stock Quote Dashboard example illustrated how the HTTP/2 protocol boosts the network throughput, with other HTTP/2 features such as Server-Sent-Event (SSE), which shape the new landscape for the web application design.