服务计算hw7——开发简单的WEB服务程序

课程任务

开发简单的Web服务程序

基本要求

  1. 编程 web 服务程序 类似 cloudgo 应用。
  2. 支持静态文件服务
  3. 支持简单 js 访问
  4. 提交表单,并输出一个表格(必须使用模板)
  5. 使用 curl 测试,将测试结果写入 README.md
  6. 使用 ab 测试,将测试结果写入 README.md。并解释重要参数。

开发cloudgo

安装Martini框架

Martini 是一个非常新的 Go 语言的 Web 框架,使用 Go 的 net/http 接口开发,类似 Sinatra 或者 Flask 之类的框架,也可使用自己的 DB 层、会话管理和模板。其特性如下:

  • 使用极其简单
  • 无侵入式的设计
  • 很好的与其他的 Go 语言包协同使用
  • 超赞的路径匹配和路由
  • 模块化的设计 - 容易插入功能插件,也容易将其拔出来
  • 已有很多的中间件可以直接使用
  • 框架内已拥有很好的开箱即用的功能支持
  • 完全兼容 http.HandleFunc 接口

使用命令

go get github.com/go-martini/martini
编写代码

创建cloudgo文件夹,在其下新建main.go文件和包含service.go文件的service文件夹
main.go的代码如下:

package main

import (
	"os"

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

const (
	PORT string = "8080" //端口为8080
)

func main() {
	//没有监听到端口则设为默认端口
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = PORT
	}

	//允许用-p来设置端口
	pPort := flag.StringP("port", "p", "PORT", "PORT for httpd listening")
	flag.Parse()
	if len(*pPort) != 0 {
		port = *pPort
	}

	service.NewServer(port) //新建服务
	
}

service.go代码如下:

package service

import "github.com/go-martini/martini"

func NewServer(port string) {
	m := martini.Classic() //创建一个martini实例

	m.Get("/", func(params martini.Params) string {//接受对Get的方法请求
		return "Hello world!"
	})

	m.RunOnAddr(":" + port)
}

运行程序

go run main.go -p8080

运行结果:
运行结果1
运行结果2
打开网页http://localhost:8080,查看显示结果
网页显示

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

使用以下两个命令使得程序可以进行简单的网页渲染

go get github.com/martini-contrib/render 
go get github.com/martini-contrib/binding

将service.go做一些补充修改,加入tmpl文件(等同于html文件),使得网页可以获取并显示当前时间、用户登录信息等内容

package server

import(
	"time"
	"github.com/go-martini/martini"
	"github.com/martini-contrib/render"
	"github.com/martini-contrib/binding"
)

type Post struct {
	Username string `form:"username" binding:"required"`
	Password string `form:"password" binding:"required"`
}

func NewServer(port string) { 

	//默认设置
	m := martini.Classic()

	
	//设置资源文件的路径
	//defaultConfig.Use(martini.Static("assets"))
	//本来有背景图片但图片总是崩所以先将这个注释掉,只显示最基本的文字界面
	//设置渲染html模板的包
	m.Use(render.Renderer())
	
	//GET请求
	m.Get("/", func(r render.Render) {
		//获取当前系统时间
		curTime:=time.Now().Format("2006-01-02 15:04:05")
		//渲染index模板,将时间传过去
        r.HTML(200, "index", map[string]interface{}{"Time":curTime})
	})

	//POST请求
	m.Post("/", binding.Bind(Post{}), func(post Post, r render.Render) {
		curTime:=time.Now().Format("2006-01-02 15:04:05")
		p := Post{Username: post.Username, Password: post.Password}
		//渲染index模板,将用户信息和时间传过去
        r.HTML(200, "info", map[string]interface{}{"post": p, "Time":curTime})
	})

	//开始运行
	m.RunOnAddr(":"+port)
}

再次输入网址
得到如下界面:
界面1
表格信息如下:
界面2

使用 curl 测试

另开终端输入命令:

curl -v http://localhost:8080

测试结果:
curl测试结果

使用 ab 测试
安装 Apache web 压力测试程序:

使用命令:

sudo apt install apache2-utils 
进行ab压力测试

使用命令

ab -n 1000 -c 100 http://localhost:8080/

测试结果:
ab1

ab2
ab3

重要参数解释
基本参数

-n:执行的请求数量
-c:并发请求个数
-t:测试所进行的最大秒数
-p:包含了所需要 POST 的数据的文件
-T:POST 数据所使用的 Content-type 头信息
-k:启用 HTTP KeepAlive 功能,即在一个 HTTP 会话中执行多个请求,默认时,不启用 KeepAlive 功能

测试结果参数

Server Software 服务器软件名称及版本信息
Server Hostname 服务器主机名
Server Port 服务器端口
Document Path 供测试的URL路径
Document Length 供测试的URL返回的文档大小
Concurrency Level 并发数
Time taken for tests 压力测试消耗的总时间
Complete requests 压力测试的总次数
Failed requests 失败的请求数
Write errors 网络连接写入错误数
Total transferred 传输的总数据量
HTML transferred HTML文档的总数据量
Requests per second 平均每秒的请求数
Time per request 所有并发用户请求一次的平均时间
Time per request 单个用户请求一次的平均时间
Transfer rate 传输速率,单位:KB/s

项目地址

项目地址

DefaultServeMux 与 gorilla/mux 对比阅读

基本要求

以 net/http 库 DefaultServeMux 实现为例

  1. 追到类型 ServeMux 。 当然的知道它的任务是将 “用户请求中 path 映射到 Handler”
  • map –> (path/name?, handler)
  • muxEntry:Handler 是接口, pattern?
  1. 关键代码
  • pathMatch 函数,你已经知道了,这就是 path == pattern 的简单匹配
  • 在看看 ServeMux 方法的代码,基本就验证了你的想法

DefaultServeMux

Golang自带的net/http库中包含了DefaultServeMux方法,以此可以搭建一个稳定的高并发的web server。

DefaultServeMux源码分析

golang自带的http.SeverMux路由实现简单,本质是一个map[string]Handler,是请求路径与该路径对应的处理函数的映射关系。

实现简单功能也比较单一:

  • 不支持正则路由, 这个是比较致命的
  • 只支持路径匹配,不支持按照Method,header,host等信息匹配,所以也就没法实现RESTful架构
路由表

路由表是实现路由功能的重要结构。muxEntry是存放具体的路由信息的一个map,key是路径,而value是该路径所对应的处理函数。

  type ServeMux struct {
          mu    sync.RWMutex
          m     map[string]muxEntry
  }
  type muxEntry struct {
          explicit bool
          h        Handler
          pattern  string
  }
路由注册

路由注册就是往map中插入数据,如果注册路径在当前路由表中不存在,则会在路由表中增加这一条路径数据,且处理函数是重定向到该路径,要注意的是注册路径要以/结尾才会添加到路由表中。

  func (mux *ServeMux) Handle(pattern string, handler Handler) {
          mux.mu.Lock()
          defer mux.mu.Unlock()
          mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
          n := len(pattern)
          if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
                  path := pattern
                  fmt.Printf("redirect for :%s to :%s", pattern, path)
                  mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
          }
  }
路由查找

路由查找的过程实际上就是遍历路由表的过程,返回最长匹配请求路径的路由信息,找不到则返回NotFoundHandler。如果路径以xxx/结尾,则只要满足/xxx/* 就符合该路由。

  func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
          mux.mu.RLock()
          defer mux.mu.RUnlock()
          if h == nil {
                  h, pattern = mux.match(path)
          }
          if h == nil {
                  h, pattern = NotFoundHandler(), ""
          }
          return
  }

  func (mux *ServeMux) match(path string) (h Handler, pattern string) {
          var n = 0
          for k, v := range mux.m {
                  if !pathMatch(k, path) {
                  continue
                  }
                  //找出匹配度最长的
                  if h == nil || len(k) > n {
                  n = len(k)
                  h = v.h
                  pattern = v.pattern
                  }
          }
          return
  }

  func pathMatch(pattern, path string) bool {
  n := len(pattern)
          if pattern[n-1] != '/' {
                  return pattern == path
          }
          return len(path) >= n && path[0:n] == pattern
  }

gorilla/mux

gorilla/mux是一个强大的路由,小巧但是稳定高效,不仅可以支持正则路由还可以按照Method,header,host等信息匹配,可以从我们设定的路由表达式中提取出参数方便上层应用,而且完全兼容http.ServerMux

源码分析

在整个mux中,核心类是一个叫Router的类,而另一个同样重要,名字类似的类是Route。从名字上看来,前者是一个路由器类型,后者是某一条具体的路由。也就是说,在一个路由器中,应该会包含多个路由(在代码中,这些Route被存放在了Router中一个名叫routes的数组中)。而一条路由记录将会保存一个处理函数句柄Handler,选择正确的句柄处理到达的请求,就是路由的工作。

Router

路由信息是存放在一个Route类型的数组([]Route)中,数组中的每一个Route对象都表示一条路由信息,其中包含匹配该路由应该满足的所有条件及对应的上层处理Hanlder。当请求到来是Router会遍历Route数组,找到第一个匹配的路由则执行对应的处理函数,如果找不到则执行NotFoundHandler。

type Router struct {
    routes []*Route
}
// Match matches registered routes against the request.
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
    for _, route := range r.routes {
        //Route.Match会检查http.Request是否满足其设定的各种条件(路径,Header,Host..)
        if route.Match(req, match) {
            return true
        }
    }
    return false
}
 
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    var match RouteMatch
    var handler http.Handler
    if r.Match(req, &match) {
        handler = match.Handler
    }
    if handler == nil {
        handler = http.NotFoundHandler()
    }
    handler.ServeHTTP(w, req)
}
Route

每一个Route中包含一个matcher数组,是所有限定条件的集合,matcher是一个返回bool值的接口。当我们添加路由限定条件时,就是往matcher数组中增加一个限定函数。 当请求到来时,Route.Match()会遍历matcher数组,只有数组中所有的元素都返回true时则说明此请求满足该路由的限定条件。

Route保存这三个条件的过程:

  type Route struct {
          // Request handler for the route.
          handler http.Handler
          // List of matchers.
          matchers []matcher
  }
//添加Header限定条件,请求的header中必须含有“Refer”,值为“example.com”
func (r *Route) Headers(pairs ...string) *Route {
    if r.err == nil {
        var headers map[string]string
        //mapFromPairs返回一个map[string][string]{"Refer":"example.com"}
        headers, r.err = mapFromPairs(pairs...)
        return r.addMatcher(headerMatcher(headers))
    }
    return r
}
 
type headerMatcher map[string]string
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
    //matchMap会判断r.Header是否含有“Refer”,并且值为“example.com”
    return matchMap(m, r.Header, true)
}
 
//methodMatcher就是取出r.Method然后判断该方式是否是设定的Method
type methodMatcher []string
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
    return matchInArray(m, r.Method)
}
 
func (r *Route) Methods(methods ...string) *Route {
    for k, v := range methods {
        methods[k] = strings.ToUpper(v)
    }
    return r.addMatcher(methodMatcher(methods))
}
 
//带有正则表达式路径匹配是比较复杂的 tpl就是/user/{userid:[0-9]+}
func (r *Route) Path(tpl string) *Route {
    r.err = r.addRegexpMatcher(tpl, false, false, false)
    return r
}
 
func (r *Route) addRegexpMatcher(tpl string,strictSlash bool) error {
    //braceIndices判断{ }是否成对并且正确出现,idxs是'{' '}'在表达式tpl中的下标数组
    idxs, errBraces := braceIndices(tpl)
     
    template := tpl
    defaultPattern := "[^/]+"
    //保存所需要提取的所有变量名称,此例是userid
    varsN := make([]string, len(idxs)/2)
    var end int //end 此时为0
    pattern := bytes.NewBufferString("")
    for i := 0; i < len(idxs); i += 2 {
        raw := tpl[end:idxs[i]] //raw="/user/"
        end = idxs[i+1]
        parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) //parts=[]{"userid","[0-9]+"}
        name := parts[0]  //name="userid"
        patt := defaultPattern
        if len(parts) == 2 {
            patt = parts[1] //patt="[0-9]+"
        }
        //构造出最终的正则表达式 /usr/([0-9]+)
        fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
        varsN[i/2] = name //将所要提取的参数名userid保存到varsN中
    }//如果有其他正则表达式继续遍历
      raw := tpl[end:]
    pattern.WriteString(regexp.QuoteMeta(raw))
    if strictSlash {
        pattern.WriteString("[/]?")
    }
    //编译最终的正则表达式
    reg, errCompile := regexp.Compile(pattern.String())
     
    rr = &routeRegexp{
        template:    template,
        regexp:      reg,
        varsN:       varsN,
    }
    r.addMatcher(rr)
}
 
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
    return r.regexp.MatchString(getHost(req))
}
context

因为gorilla/mux选择与http.ServerMux的接口保持一致,所以上层应用的处理函数也就变成了固定的 Hanlder

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

为了将正则匹配解析出的参数Vars传递给上层处理函数,gorilla/mux使用了一个第三方模块gorilla/context。当http请求到来时,mux.Router会选择合适的路由,并提取出一些参数信息,将这些参数信息与http.Request对象在gorilla/context中建立映射关系,上层处理函数根据http.Request对象到context中找到该http.Request所对应的参数信息。

  var data  = make(map[*http.Request]map[interface{}]interface{})
  func Set(r *http.Request, key, val interface{}) {
  mutex.Lock()
          if data[r] == nil {
                  data[r] = make(map[interface{}]interface{})
                  datat[r] = time.Now().Unix()
          }
          data[r][key] = val
          mutex.Unlock()
  }
  func Get(r *http.Request, key interface{}) interface{} {
          mutex.RLock()
          if ctx := data[r]; ctx != nil {
                  value := ctx[key]
                  mutex.RUnlock()
                  return value
          }
          mutex.RUnlock()
          return nil
  }

上层处理函数中调用mux.Vars®则可以取出该http.Request所关联的参数信息。val实际上是一个map[string][string],存放该请求对应的变量值集合。
func setVars(r *http.Request, val interface{}) {
context.Set(r, varsKey, val)
}
func Vars(r *http.Request) map[string]string {
if rv := context.Get(r, varsKey); rv != nil {
//类型转换,如果失败直接panic
return rv.(map[string]string)
}
return nil
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值