Go 中的 Web 应用

本次作业的任务:

  1. 基于现有 Web 库,编写一个简单 Web 应用。

  2. 使用 curl 工具访问 Web 程序。

  3. 对 Web 执行压力测试。

Web 应用的编写

完整代码可见 GitHub

项目中使用了两个 Web 库:negronimux。其中 negroni 库用于处理中间件,mux 库用于创建路由。

服务器模块提供 NewServer 函数,该函数首先通过 mux 库创建了一个路由,然后通过 HandleFunc 函数将 URL 路径映射到相应的处理函数上。随后,通过 negroni.Classic() 函数创建一个 negroni.Negroni 实例,提供一些默认的中间件,并在最后添加之前创建的路由(因为 Negroni 没有提供路由功能)。

func NewServer() *negroni.Negroni {
    // create router
    router := mux.NewRouter()
    // register routes
    router.HandleFunc("/", sayHello).Methods("GET")
    router.HandleFunc("/login", login).Methods("GET", "POST")
    // add some default middlewares
    nc := negroni.Classic()
    // use router
    nc.UseHandler(router)
    return nc
}

当客户端访问 “/” 路径时,向客户端发送 Hello 回复。

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello!\n")
}

当客户端访问 “/login” 路径时,需要判断 HTTP 的方法:如果 HTTP 方法为 GET,则向客户端返回登陆页面;而如果 HTTP 方法为 POST,则在服务器端输出用户名及密码。

// template for login page
const loginTemplate = `
<!DOCTYPE html>
<html>
    <head>
        <title>Login</title>
    </head>
    <body>
        <form action="/login" method="post">
            Username: <input type="text" name="username">
            Password: <input type="password" name="password">
            <input type="submit" value="Login">
        </form>
    </body>
</html>
`

func login(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Method:", r.Method)
    if r.Method == "GET" {
        t, _ := template.New("webpage").Parse(loginTemplate)
        t.Execute(w, nil)
    } else {
        r.ParseForm()
        fmt.Println("Username:", r.FormValue("username"))
        fmt.Println("Password:", r.FormValue("password"))
    }
}

在主函数中,用变量 port 表示监听端口。首先,需要检查是否存在环境变量 PORT,如果存在则将 port 变量设置为该环境变量的值,否则设置为默认值。然后,通过 pflag 库检查用户是否通过命令行参数设置了端口,如果是,则将 port 变量设置为用户提供的端口。在设置完端口后,调用 Negroni 库的 Run 函数(相当于调用 net/http 中的 ListenAndServe 函数)监听相应的端口。

const defaultPort string = "8888"

func main() {
    // try to retrieve the value of the "PORT" environment variable
    port := os.Getenv("PORT")
    if len(port) == 0 {
        port = defaultPort
    }

    // if the use set the port, then use what user set
    pPort := flag.StringP("port", "p", "", "PORT for httpd listening")
    flag.Parse()
    if len(*pPort) != 0 {
        port = *pPort
    }

    // run server
    server := service.NewServer()
    server.Run(":" + port)
}

访问 Web 程序

通过 go run main.go -p 1234 运行 Web 程序,然后使用 curl 工具访问 Web 程序。

首先访问 “/” 路径:

curl -v http://localhost:1234

结果如下:

接着通过 GET 方法访问 “/login” 路径:

curl -v http://localhost:1234/login

结果如下:

最后通过 POST 方法访问 “/login” 路径:

curl -v -X POST "http://localhost:1234/login?username=rhythm&password=123456"

结果如下:

服务器端的输出如下:

压力测试

最后,使用 macOS 自带的 ab(Apache Bench)对 Web 程序进行压力测试:

touch empty.txt \
&& \
ab \
-n 1000 \
-c 100 \
-T 'application/x-www-form-urlencoded; charset=UTF-8' \
-p empty.txt \
"http://localhost:1234/login?username=rhythm&password=123456" \
&& \
rm empty.txt

其中,-n 参数表示请求数量,-c 参数表示并发请求的数量,-T 参数用于设置 POST 请求时的 Content-Type 字段,-p 设置 POST 的数据文件(此处使用一个临时空文件)。

测试结果如下:

Updated: