Hi everyone,

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!
Author:

Software Developer, Codemio Admin

Disqus Comments Loading..