HTTP server

Introduction

Setup Server

Using GoLang, it is easy to set up a HTTP server listening at localhost port 8000 by writing.

func main() {
    r := RegisterHandlers()
    mh := NewMiddlewareHandler(r, 20)
    http.ListenAndServe(":8000", mh)
}

Handlers

Each request from the client-side will be forwarded to one of the handlers in "RegisterHandlers" function for processing based on the URI. When all handlers are registered, we wrap the handler with a "middlewareHandler". This middleware handler does nothing special but limits the number of concurrent connections to our file HTTP server. This is usually the case when we build file servers with images and videos, where too many connection request data simultaneously, "ConnLimiter" protects our server's network from jam and failure.

Currently, the connection limit is set to 20. We will increase this limit when we have a better server with a larger bandwidth

func RegisterHandlers() *httprouter.Router {
    router := httprouter.New()
    
    // upload, stream, picture
    router.GET ("/api/v0/testpage", testPageHandler)
    router.POST("/api/v0/upload/:vid-id", postVideoHandler)
    
    router.GET("/api/v0/videos/:vid-id", streamHandler)
    router.GET("/api/v0/images/:iid-id", pictureHandler)

    // paint project
    router.POST("/api/v2/post/:iid-id", postImageHandler)
    router.GET("/api/v2/images/:iid-id", paintHandler)

    // bird project (not used in this course)
    router.GET("/api/v1/billboard", billboardHandler)
    router.GET("/api/v1/dashboard", dashboardHandler)

    return router
}

type middlewareHandler struct {
    r *httprouter.Router
    l *ConnLimiter
}

func NewMiddlewareHandler(r *httprouter.Router, cc int) http.Handler {
    m := middlewareHandler{}
    m.r = r
    m.l = NewConnLimiter(cc)
    return m
}

func (m *middlewareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // check if go beyond our limit 20
    if !m.l.GetConn() {
        sendErrorResponse(w, http.StatusTooManyRequests, "Too many requests")
        return
    }

    m.r.ServeHTTP(w, r)
    defer m.l.ReleaseConn()
}

Implement a ConnLimiter

Below is a simple implementation of "ConnLimiter", basically it builds a channel as a bucket with size cc (20 in our case). If the bucket is not full, add a token 1 to our bucket and return true. Otherwise, return false. Hence, reject connection. "ReleaseConn" function on the other hand will remove a token from bucket, indicating a connection has finished. GoLang prefers CSP model for concurrency, so we use channel.

type ConnLimiter struct {
    concurrentConn int
    bucket         chan int
}

func NewConnLimiter(cc int) *ConnLimiter {
    return &ConnLimiter{
        concurrentConn: cc,
        bucket:         make(chan int, cc),
    }
}

func (cl *ConnLimiter) GetConn() bool {
    if len(cl.bucket) >= cl.concurrentConn {
        glog.Infoln("Reached the rate limitation")
        return false
    }
    cl.bucket <- 1
    return true
}

func (cl *ConnLimiter) ReleaseConn() {
    c := <-cl.bucket
    glog.Infof("A connection finished %d\n", c)
}

Handle Image Post

Images are sent via HTTP Post MultipartForm with the name "file", Unlike WebSocket, where we need to define our own protocol and message types, HTTP already defines those for us. Typically, we Post MultipartForm to send images. Later, we store the image with name provided by "Params(iid-id)" (In our case, it is the UUID generated by Android) on our server and replay to the client "UploadSuccessfully"

func postImageHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
    if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
        sendErrorResponse(w, http.StatusBadRequest, "File is too big") // 400
        return
    }

    file, _, err := r.FormFile("file") // <form name = "file"
    if err != nil {
        sendErrorResponse(w, http.StatusInternalServerError, "Internal Error")
        return
    }

    data, err := ioutil.ReadAll(file)
    if err != nil {
        glog.Errorf("Read file error: %v\n", err)
        sendErrorResponse(w, http.StatusInternalServerError, "Internal Error")
    }

    fn := p.ByName("iid-id") + ".png"
    err = ioutil.WriteFile(IMAGE_DIR + fn, data, 0666)
    if err != nil {
        glog.Errorf("Write file error: %v\n", err)
        sendErrorResponse(w, http.StatusInternalServerError, "Internal Error")
        return
    }

    w.WriteHeader(http.StatusCreated)
    io.WriteString(w, "Uploaded Successfully")
}

func sendErrorResponse(w http.ResponseWriter, sc int, errMsg string) {
    w.WriteHeader(sc)
    io.WriteString(w, errMsg)
}

Handle Image Get

Find the file on server given name "Params(iid-id)", and send it to client via HTTP protocol.

func paintHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {

    iid := p.ByName("iid-id")
    vl := IMAGE_DIR + iid

    image, err := os.Open(vl)

    if err != nil {
        glog.Errorf("Error when open file: %v\n", err)
        sendErrorResponse(w, http.StatusInternalServerError, "Internal Error") // 500
        return
    }

    w.Header().Set("Content-Type", "image/png")
    http.ServeContent(w, r, "", time.Now(), image)

    defer image.Close()
}