Our final web server is capable of serving static, template-rendered, and dynamic content well within the confines of the goal of 10,000 concurrent connections on even the most modest of hardware.
The code—much like the code in this book—can be considered a jumping-off point and will need refinement if put into production. This server lacks anything in the form of error handling but can ably serve valid requests without any issue. Let's take a look at the following server's code:
package main import ( "net/http" "html/template" "time" "regexp" "fmt" "io/ioutil" "database/sql" "log" "runtime" _ "github.com/go-sql-driver/mysql" )
Most of our imports here are fairly standard, but note the MySQL line that is called solely for its side effects as a database/SQL driver:
const staticPath string = "static/"
The relative static/
path is where we'll look for any file requests—as mentioned earlier, this does no additional error handling, but the net/http
package itself will deliver 404 errors should a request to a nonexistent file hit it:
type WebPage struct { Title string Contents string Connection *sql.DB }
Our WebPage
type represents the final output page before template rendering. It can be filled with static content or populated by data source, as shown in the following code:
type customRouter struct { } func serveDynamic() { } func serveRendered() { } func serveStatic() { }
Use these if you choose to extend the web app—this makes the code cleaner and removes a lot of the cruft in the ServeHTTP
section, as shown in the following code:
func (customRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { path := r.URL.Path; staticPatternString := "static/(.*)" templatePatternString := "template/(.*)" dynamicPatternString := "dynamic/(.*)" staticPattern := regexp.MustCompile(staticPatternString) templatePattern := regexp.MustCompile(templatePatternString) dynamicDBPattern := regexp.MustCompile(dynamicPatternString) if staticPattern.MatchString(path) { serveStatic() page := staticPath + staticPattern.ReplaceAllString(path, "${1}") + ".html" http.ServeFile(rw, r, page) }else if templatePattern.MatchString(path) { serveRendered() urlVar := templatePattern.ReplaceAllString(path, "${1}") page.Title = "This is our URL: " + urlVar customTemplate.Execute(rw,page) }else if dynamicDBPattern.MatchString(path) { serveDynamic() page = getArticle(1) customTemplate.Execute(rw,page) } }
All of our routing here is based on regular expression pattern matching. There are a lot of ways you can do this, but regexp
gives us a lot of flexibility. The only time you may consider simplifying this is if you have so many potential patterns that it could cause a performance hit—and this means thousands. The popular web servers, Nginx and Apache, handle a lot of their configurable routing through regular expressions, so it's fairly safe territory:
func gobble(s []byte) { }
Go is notoriously cranky about unused variables, and while this isn't always the best practice, you will end up, at some point, with a function that does nothing specific with data but keeps the compiler happy. For production, this is not the way you'd want to handle such data.
var customHTML string var customTemplate template.Template var page WebPage var templateSet bool var Database sql.DB func getArticle(id int) WebPage { Database,err := sql.Open("mysql", "test:test@/master") if err != nil { fmt.Println("DB error!") } var articleTitle string sqlQ := Database.QueryRow("SELECT article_title from articles WHERE article_id=? LIMIT 1", id).Scan(&articleTitle) switch { case sqlQ == sql.ErrNoRows: fmt.Printf("No rows!") case sqlQ != nil: fmt.Println(sqlQ) default: } wp := WebPage{} wp.Title = articleTitle return wp }
Our getArticle
function demonstrates how you can interact with the database/sql
package at a very basic level. Here, we open a connection and query a single row with the QueryRow()
function. There also exists the Query
command, which is also usually a SELECT
command but one that could return more than a single row.
func main() { runtime.GOMAXPROCS(4) var cr customRouter; fileName := staticPath + "template.html" cH,_ := ioutil.ReadFile(fileName) customHTML = string(cH[:]) page := WebPage{ Title: "This is our URL: ", Contents: "Enjoy our content" } cT,_ := template.New("Hey").Parse(customHTML) customTemplate = *cT gobble(cH) log.Println(page) fmt.Println(customTemplate) server := &http.Server { Addr: ":9000", Handler:cr, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } server.ListenAndServe() }
Our main function sets up the server, builds a default WebPage
and customRouter
, and starts listening on port 9000
.
One thing we did not focus on in our server is the notion of lingering connection mitigation. The reason we didn't worry much about it is because we were able to hit 10,000 concurrent connections in all three approaches without too much issue, strictly by utilizing Go's powerful built-in concurrency features.
Particularly when working with third-party or external applications and services, it's important to know that we can and should be prepared to call it quits on a connection (if our application design permits it).
Note the custom server implementation and two notes-specific properties: ReadTimeout
and WriteTimeout
. These allow us to handle this use case precisely.
In our example, this is set to an absurdly high 10 seconds. For a request to be received, processed, and sent, up to 20 seconds can transpire. This is an eternity in the Web world and has the potential to cripple our application. So, what does our C10K look like with 1 second on each end? Let's take a look at the following graph:
Here, we've saved nearly 5 seconds off the tail end of our highest volume of concurrent requests, almost certainly at the expense of complete responses to each.
It's up to you to decide how long it's acceptable to keep slow-running connections, but it's another tool in the arsenal to keep your server swift and responsive.
There will always be a tradeoff when you decide to kill a connection—too early and you'll have a bevy of complaints about a nonresponsive or error-prone server; too late and you'll be unable to cope with the connection volume programmatically. This is one of those considerations that will require QA and hard data.