Recently, I've been looking into building a streaming HTTP server that supports the new HTTP/2 protocol, but there seems to be a lack of information on the internet around this, so I decided to write this post. Before we start, I am assuming that you are familiar with Go programming language and interested in building a streaming HTTP/2 server-client pair.
For those who would like to go get the code and leave, feel free to visit: https://github.com/herrberk/go-http2-streaming
HTTP/2
"HTTP/2 will make our applications faster, simpler, and more robust — a rare combination — by allowing us to undo many of the HTTP/1.1 workarounds previously done within our applications and address these concerns within the transport layer itself. Even better, it also opens up a number of entirely new opportunities to optimize our applications and improve performance!"https://developers.google.com/web/fundamentals/performance/http2/
The HTTP/2 by default works on highly secured connections. It uses high quality ciphers. So it can only run on HTTPS connections. Moreover, to make HTTPS connections, you also need to have your SSL enabled and have the required certificates installed.
Feel free to create your own certificate-key pair using this one-liner:
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt
Let's install the dependencies first:
$ go get golang.org/x/net/http2
$ go get github.com/julienschmidt/httprouter
Server
Here is the server code that accepts only HTTP/2 requests:
package http2
import (
"io"
"log"
"net"
"net/http"
httpRouter "github.com/julienschmidt/httprouter"
)
type Server struct {
router *httpRouter.Router
}
func (s *Server) Initialize() error {
s.router = httpRouter.New()
s.router.POST("/", s.handler)
//Creates the http server
server := &http.Server{
Handler: s.router,
}
listener, err := net.Listen("tcp", ":10000")
if err != nil {
return err
}
log.Println("HTTP server is listening..")
return server.ServeTLS(listener, "./http2/certs/key.crt", "./http2/certs/key.key")
}
func (s *Server) handler(w http.ResponseWriter, req *http.Request, _ httpRouter.Params) {
// We only accept HTTP/2!
// (Normally it's quite common to accept HTTP/1.- and HTTP/2 together.)
if req.ProtoMajor != 2 {
log.Println("Not a HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError)
return
}
buf := make([]byte, 4*1024)
for {
n, err := req.Body.Read(buf)
if n > 0 {
w.Write(buf[:n])
}
if err != nil {
if err == io.EOF {
w.Header().Set("Status", "200 OK")
req.Body.Close()
}
break
}
}
}
Client
And, this is the client code:
package http2
import (
"bufio"
"bytes"
"crypto/tls"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"golang.org/x/net/http2"
)
type Client struct {
client *http.Client
}
func (c *Client) Dial() {
// Adds TLS cert-key pair
certs, err := tls.LoadX509KeyPair("./http2/certs/key.crt", "./http2/certs/key.key")
if err != nil {
log.Fatal(err)
}
t := &http2.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{certs},
InsecureSkipVerify: true,
},
}
c.client = &http.Client{Transport: t}
}
func (c *Client) Post(data []byte) {
req := &http.Request{
Method: "POST",
URL: &url.URL{
Scheme: "https",
Host: "localhost:10000",
Path: "/",
},
Header: http.Header{},
Body: ioutil.NopCloser(bytes.NewReader(data)),
}
// Sends the request
resp, err := c.client.Do(req)
if err != nil {
log.Println(err)
return
}
if resp.StatusCode == 500 {
return
}
defer resp.Body.Close()
bufferedReader := bufio.NewReader(resp.Body)
buffer := make([]byte, 4*1024)
var totalBytesReceived int
// Reads the response
for {
len, err := bufferedReader.Read(buffer)
if len > 0 {
totalBytesReceived += len
log.Println(len, "bytes received")
// Prints received data
// log.Println(string(buffer[:len]))
}
if err != nil {
if err == io.EOF {
// Last chunk received
// log.Println(err)
}
break
}
}
log.Println("Total Bytes Sent:", len(data))
log.Println("Total Bytes Received:", totalBytesReceived)
}
Let's put these two together and run to see them in action:
package main
import (
"go-http2-streaming/http2"
"io/ioutil"
"log"
)
func main() {
// Waitc is used to hold the main function
// from returning before the client connects to the server.
waitc := make(chan bool)
// Reads a file into memory
data, err := ioutil.ReadFile("./test.json")
if err != nil {
log.Println(err)
return
}
// HTTP2 Client
go func() {
client := new(http2.Client)
client.Dial()
client.Post(data)
}()
// HTTP2 Server
server := new(http2.Server)
err = server.Initialize()
if err != nil {
log.Println(err)
return
}
// Waits forever
<-waitc
}
The code should be pretty self explanatory and I added some comments to make it more clear, but if you have any questions or comments please drop a line below. Thank you!
Disqus Comments Loading..