Go as a Web Server
Although I am primarily an embedded systems developer, creating web interfaces is one of my main activities. My simplest informational sites like the site you are now visiting are written in plain HTML code or use the off-the-shelf framework WordPress. Standalone sites which need more unique processing features like my Cross Stitch Pattern Generator Pic2Pat are custom built in PHP. But I also add web interfaces to many of my embedded projects. A web interface is a relatively easy way to get access to a remote device over a network. Some time ago I created LibHTTP as a fork of the open source package CivetWeb for exactly this purpose but to be honest, my experience has been mixed.
C is not the language to build elegant applications which can handle lots of concurrent connections. The LibHTTP implementation uses a fixed amount of ,threads which causes either a large memory footprint even when few clients have to be served, or runs in connection limit problems during high network loads. It is clearly not the best solution for having a simple scalable web interface to an embedded application. This is the main reason I started looking into the functionality of Go, because Go was explicitly designed for this goal. Go can be compiled together with existing C code which opens the possibility to enhance exiting C applications with a Go web-server.
In a number of example programs I will cover all the steps from a simple standalone Go web server which only performs trivial tasks, to a Go web-server which serves happily over HTTPS connections and on the fly exchanges data with a linked C program. The first example below is a simple site serving HTTP requests. On the homepage, a string can be entered which is sent to the web server with a POST request. If the processing of the POST request succeeds, the data is stored in a global variable and the homepage is reloaded with the entered string on the page.
The data-flow in the application is different from how PHP and other server-side scripts normally processes web requests. A server-side script is started when a request is accepted by the web-server software (often Apache or Nginx) and ended when all processing is completed for that single request. All data of that processing is then discarded. This is basically a state-less approach and any visitor specific state information must be stored in either a database or session files.
Go doesn’t stop between requests. The web server and the request handling code are one piece of software and data remains available in between requests inside the program. This opens possibilities as well as possible vulnerabilities. For a real-life web application the state of different visitors must be carefully separated to prevent data leakage. I won’t go into the details to do this in this example application because there is no preserved state for each individual visitor. But you have to understand that if you visit this example application from two different computers or browsers, the second request will show you the information entered by the first.
package main
// Copyright (C) 2021 - Lammert Bies
// Distributed under terms of the MIT license.
import (
"fmt"
"sync"
"net/http"
)
const (
port = 8080
)
/*
* We store data entered through the web interface in one
* global variable. Because access to the application can
* happen from multiple sources at once, the access to the
* variable is protected with a mutex.
*/
var last_entered_name string = ""
var mux sync.Mutex
/*
* func IndexHandler()
*
* The IndexHandler() function outputs the HTML code for the
* base URL of the site. This page contains a text field in
* which string data can be submitted. If previously string
* data was recorded, it will be printed as a text line.
*/
func IndexHandler( w http.ResponseWriter, r *http.Request ) {
fmt.Fprint( w,
`<!DOCTYPE html>
<html>
<head>
<title>Simple Go Webserver</title>
</head>
<body>
<h1>Go Website!</h1>
<p>Example website running on Go</p>` )
mux.Lock()
if last_entered_name != "" {
fmt.Fprintf( w, "<p><strong>%s</strong> was here before!</p>\n", last_entered_name )
}
mux.Unlock()
fmt.Fprintf( w,
` <form method="post" action="/process-post">
Enter your name: <input type="text" name="yourname" value"">
<input type="submit" name="sa" value="Submit">
</form>
</body>
</html>` )
} /* IndexHandler */
/*
* func Posthandler()
*
* The PostHandler() function receives the request to the POST
* handler. If the request is valid, the newly entered data
* is stored in the global string variable.
*
* If an error occurs, the error is sent to the browser and the
* function exits. At success a 301 redirect is done to the
* base URL of the site where the visitor can enter a new string
* value.
*/
func PostHandler( w http.ResponseWriter, r *http.Request ) {
switch r.Method {
case "POST" :
err := r.ParseForm()
if err != nil {
fmt.Fprintf( w, "Error parsing input: %v", err )
break
}
mux.Lock()
last_entered_name = r.FormValue( "yourname" )
mux.Unlock()
new_destination := fmt.Sprintf( "http://%s/", r.Host )
http.Redirect( w, r, new_destination, 301 )
break
default :
fmt.Fprintf( w, "Invalid method: \"%s\"", r.Method )
break
}
} /* PostHandler */
/*
* func RequestHandler()
*
* The RequestHandler() function processes all requests to
* the webserver and sends the requests to other functions
* based on the URL of the request.
*/
func RequestHandler( w http.ResponseWriter, r *http.Request ) {
switch r.URL.Path {
case "/" :
IndexHandler( w, r )
break
case "/process-post" :
PostHandler( w, r )
break
default :
w.WriteHeader( 404 )
break
}
} /* HandleIndex */
/*
* func main()
*
* The function main() is the main entry point of the application.
* It does the necessary initialization to start the webserver
* and then starts serving requests.
*/
func main() {
listen_address := fmt.Sprintf( ":%d", port )
fmt.Printf( "Starting webserver at port %d\n", port )
http.HandleFunc( "/", RequestHandler )
fmt.Printf( "Problem encountered:\n%v\n", http.ListenAndServe( listen_address, nil ) )
} /* main */
The use of the net/http package makes creating this web-server relatively easy. The total initialization takes one line to create the string which tells on which IP address and port the server should listen, and one line to define the handler function for all requests. The spawning of new processes and keeping track of opened and closed connections is completely transparent for the application. Other than by OS limits like the maximum amount of open network connections, and memory the application is not limited in how many clients it can serve.
What is also a nice feature is that one handler function can handle all requests for all URLs and request methods for the whole application. To make the code resilient to high loads I have added a mutex around the manipulation of the only globally shared variable, but when running this application as test, that is probably overkill.
Backticks for Literal Strings
A functionality of the Go language which I haven’t mentioned in other example programs yet is the use of back-ticks to enclose strings. Strings enclosed in double quotes will be parsed by the Go runtime. Escape characters like \n for a newline or for Unicode characters are replaced on the fly. Strings enclosed in back-ticks on the other hand are used literally without any processing. Even double quotes and newlines inside the string are unaltered. Therefore back-ticks are ideally pass large chunks of static text to functions. I used them in the call to fmt.Fprint() in the IndexHandler() function to send large blocks of static HTML code directly to the client.
A 60-day warranty guarantees that the product will self-destruct on the 61st day.
SINETETO'S FIRST LAW OF CONSUMERISM
|