Go 开发 web 服务程序

Go 开发 web 服务程序

一、任务要求
基本要求
  1. 编程 web 服务程序 类似 cloudgo 应用。
    支持静态文件服务
    支持简单 js 访问
    提交表单,并输出一个表格(必须使用模板)
  2. 使用 curl 测试,将测试结果写入 README.md
  3. 使用 ab 测试,将测试结果写入 README.md。并解释重要参数。
扩展要求
  1. 通过源码分析、解释一些关键功能实现
  2. 选择简单的库,如 mux 等,通过源码分析、解释它是如何实现扩展的原理,包括一些 golang 程序设计技巧。
二、代码实现
支持静态文件服务

net/http 库以提供了现成的支持,仅使用一个函数 func FileServer(root FileSystem) Handler 就让你使用上文件服务。

net/http包提供了静态文件的服务,需要在服务器上创建目录,以存放静态内容。例如:
在这里插入图片描述

编写main.go文件如下:
启动服务器时默认监听8080端口。

package main

import (
	"os"

	"github.com/github-user/cloudgo/service"
	flag "github.com/spf13/pflag"
)

const (
	PORT string = "8080"
)

func main() {
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = PORT
	}

	pPort := flag.StringP("port", "p", PORT, "PORT for httpd listening")
	flag.Parse()
	if len(*pPort) != 0 {
		port = *pPort
	}

	server := service.NewServer()
	server.Run(":" + port)
}
  • server.go
    该文件通过构建formatter,指定了模板的目录,模板文件的扩展名。
    建立目录后,通过初始化mux,并使用如下语句即可定位到静态文件虚拟根目录。
mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
package service

import (
	"net/http"
	"os"

	"github.com/codegangsta/negroni"
	"github.com/gorilla/mux"
	"github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

	formatter := render.New(render.Options{
		Directory:  "templates",
		Extensions: []string{".html"},
		IndentJSON: true,
	})

	n := negroni.Classic()
	mx := mux.NewRouter()

	initRoutes(mx, formatter)

	n.UseHandler(mx)
	return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
	webRoot := os.Getenv("WEBROOT")
	if len(webRoot) == 0 {
		if root, err := os.Getwd(); err != nil {
			panic("Could not retrive working directory")
		} else {
			webRoot = root
			//fmt.Println(root)
		}
	}
	//mx.HandleFunc("/api/test", apiTestHandler(formatter))
	//mx.HandleFunc("/form", login(formatter))
	//mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
	mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
}

此时,通过运行

go run main.go

并在服务器中访问8080端口,可以看到如下页面。显示的是assets文件夹下的目录。
在这里插入图片描述
任意点击一个文件夹下的文件,可以看到相应结果。
在这里插入图片描述

支持简单 js 访问

编写一个html文件

  • index.html
<html>
<head>
  <link rel="stylesheet" href="css/main.css"/>
  <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
  <script src="js/hello.js"></script>
</head>
<body>
  
  <div class="border">
        <img src="images/cng.jpg"/>
        <p>Sample Go Web Application!!</p>
        <p class="greeting-id">The ID is </p>
        <p class="greeting-content">The content is </p>
  </div>
  
</body>
</html>
  • hello.js
    通过hello.js向服务器请求数据,将从服务器中获得的数据追加到index.html中。
$(document).ready(function() {
    $.ajax({
        url: "/api/test"
    }).then(function(data) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
    });
});
  • apitest.go
    返回一个json对象
package service

import (
	"net/http"

	"github.com/unrolled/render"
)

func apiTestHandler(formatter *render.Render) http.HandlerFunc {

	return func(w http.ResponseWriter, req *http.Request) {
		formatter.JSON(w, http.StatusOK, struct {
			ID      string `json:"id"`
			Content string `json:"content"`
		}{ID: "8675309", Content: "Hello from Go!"})
	}
}

将server.go中的部分注释去掉,

mx.HandleFunc("/api/test", apiTestHandler(formatter))
//mx.HandleFunc("/form", login(formatter))
mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
  • home.go
    加载index.html。
package service

import (
	"net/http"

	"github.com/unrolled/render"
)

func homeHandler(formatter *render.Render) http.HandlerFunc {

	return func(w http.ResponseWriter, req *http.Request) {
		formatter.HTML(w, http.StatusOK, "index", struct {
			
		}{})
	}
}

通过以上步骤,可以看到,hello.js通过向服务器请求"/api/test",服务器向其返回一个json类型的数据,hello.js拿到json对象后,将data.id和data.content加到html文件里。
再次运行

go run main.go

由于这次新增了html文件,可以看到如下界面:
在这里插入图片描述
可以看到,通过访问js,index.html的ID和Content都获取到了js中传输的数据。

提交表单,并输出一个表格(必须使用模板)

通过学习处理表单的输入
得知递交表单到服务器的/login,当用户输入信息点击登录之后,会跳转到服务器的路由login里面。并且可以通过

r.Method

来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑。

因此往index.html文件中新增部分代码如下:

<div class="border">
       <img src="images/cng.jpg"/>
       <p>Sample Go Web Application!!</p>
       <p class="greeting-id">The ID is </p>
       <p class="greeting-content">The content is </p>
       <form action="/form" method="login">
         学号 :<input type="text" name="id"><br>
         电话 :<input type="text" name="phonenum"><br>
         用户名:<input type="text" name="username"><br>
         密码 :<input type="password" name="password"><br>
         <input type="submit" value="登录">
       </form>
 </div>

还需要往服务器中新增路由

mx.HandleFunc("/form", login(formatter))

新增一个form.html,显示登陆后的页面,根据要求,需要使用模板。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>form</title>
</head>
<body>
	<table >
		<thead>
			<tr>
				<th>User</th>
				<th>ID</th>
				<th>PhoneNum</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>{{.Username}}</td>
				<td>{{.ID}}</td>
				<td>{{.PhoneNum}}</td>
				
			</tr>
		</tbody>
	</table>
</body>
</html>
  • login.go
    当请求方法是GET时,解析url传递的参数并且加载form.html,直接将传递的参数数据注入模板,并输出到浏览器。
package service

import (
	"net/http"

	"github.com/unrolled/render"
)

func login(formatter *render.Render) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		if req.Method == "GET" {
			req.ParseForm()
			formatter.HTML(w, http.StatusOK, "form", struct {
				Username string  
				ID string  
				PhoneNum string
			}{Username: req.Form["username"][0], ID: req.Form["id"][0], PhoneNum: req.Form["phonenum"][0]})
		}
	}
}

通过以上步骤,当点击登录按钮时,会递交表单到服务器的/login,服务器通过login函数将表单中的数据解析并通过模板加载到form.html中。
在这里插入图片描述
在这里插入图片描述

使用 curl 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用 ab 测试,并解释重要参数。

-n 执行的请求数量
-c 并发请求个数

对主页面进行1000次请求,10个并发请求。

ab -n 1000 -c 100 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        769 bytes       #文件大小

Concurrency Level:      100             #并发个数 
Time taken for tests:   0.429 seconds   #总请求时间
Complete requests:      1000            #总请求数
Failed requests:        0               #失败的请求数
Total transferred:      886000 bytes    #总共传输数据量
HTML transferred:       769000 bytes    #HTML文件总共传输数据量
Requests per second:    2330.86 [#/sec] (mean) #平均每秒的请求数
Time per request:       42.903 [ms] (mean)     #平均每个请求消耗的时间
Time per request:       0.429 [ms] (mean, across all concurrent requests)   #上面的请求除以并发数
Transfer rate:          2016.74 [Kbytes/sec] received  #传输速率

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   5.4      0      27
Processing:     0   37  30.0     32     146
Waiting:        0   36  30.0     32     146
Total:          0   38  29.4     32     146

Percentage of the requests served within a certain time (ms)
  50%     32
  66%     33
  75%     36
  80%     41
  90%     52
  95%    131
  98%    140
  99%    142
 100%    146 (longest request)

扩展要求-源代码阅读
DefaultServeMux 与 gorilla/mux 对比阅读

DefaultServeMux
查看Go语言默认路由表DefaultServeMux的结构:
可以看到,其路由表本质是一个map,通过map来处理路径的映射关系,将path 映射到 Handler。

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {
	h       Handler
	pattern string
}

路由注册,查看Handle,如果路由表中没有所要注册的路径,往路由表中增加数据。并且注册路径要以‘/’结尾。

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}
func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
	n := len(es)
	i := sort.Search(n, func(i int) bool {
		return len(es[i].pattern) < len(e.pattern)
	})
	if i == n {
		return append(es, e)
	}
	// we now know that i points at where we want to insert
	es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
	copy(es[i+1:], es[i:])      // Move shorter entries down
	es[i] = e
	return es
}

路由查找,查看match函数,如果路径str前面包含路由表中某个路径,则返回该路径。

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}
func HasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}

通过阅读DefaultServeMux源代码,发现其实现比较简单,其不支持正则匹配,路由查找时需要从头遍历路由表中的路径,并且只能对路径进行匹配,不支持其它信息的匹配。

gorilla/mux
gorilla/mux路由信息实现,区别于DefaultServeMux利用map存储路由信息,其路由信息利用Route类型的数组存储。

type Router struct {
	// Configurable Handler to be used when no route matches.
	NotFoundHandler http.Handler

	// Configurable Handler to be used when the request method does not match the route.
	MethodNotAllowedHandler http.Handler

	// Routes to be matched, in order.
	routes []*Route

	// Routes by name for URL building.
	namedRoutes map[string]*Route

	// If true, do not clear the request context after handling the request.
	//
	// Deprecated: No effect, since the context is stored on the request itself.
	KeepContext bool

	// Slice of middlewares to be called after a match is found
	middlewares []middleware

	// configuration shared with `Route`
	routeConf
}

路由查找,查看match函数,与DefaultServeMux一样,gorilla/mux也需要遍历所有数组,区别是gorilla/mux的match函数还接收一个RouteMatch结构,通过它来检查各种限制条件是否满足,只有所有限制条件都满足才会返回对应的Handler。

type RouteMatch struct {
	Route   *Route
	Handler http.Handler
	Vars    map[string]string

	// MatchErr is set to appropriate matching error
	// It is set to ErrMethodMismatch if there is a mismatch in
	// the request method and route method
	MatchErr error
}
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
	for _, route := range r.routes {
		if route.Match(req, match) {
			// Build middleware chain if no error was found
			if match.MatchErr == nil {
				for i := len(r.middlewares) - 1; i >= 0; i-- {
					match.Handler = r.middlewares[i].Middleware(match.Handler)
				}
			}
			return true
		}
	}

	if match.MatchErr == ErrMethodMismatch {
		if r.MethodNotAllowedHandler != nil {
			match.Handler = r.MethodNotAllowedHandler
			return true
		}

		return false
	}

	// Closest match for a router (includes sub-routers)
	if r.NotFoundHandler != nil {
		match.Handler = r.NotFoundHandler
		match.MatchErr = ErrNotFound
		return true
	}

	match.MatchErr = ErrNotFound
	return false
}

代码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值