服务计算学习之路-开发 web 服务程序

本文介绍了如何使用Go的http包和gorilla/mux包开发Web服务程序,包括简单的web服务器示例、http包源码分析、mux包的路由匹配功能及cloudgo程序的实现。通过对http包和mux包的学习,理解了Web服务器的工作原理和Go语言的路由处理机制。

开发 web 服务程序

简介

开发简单 web 服务程序 cloudgo,了解 web 服务器工作原理。

开发环境

  • CentOS7
  • go 1.9.4 linux/amd64

Go的http包

使用http包编写的简单web服务器

下面是一个简单的web服务器,实现在客户端访问http://127.0.0.1:9090/的时候响应内容为Hello World!

package main

import (
    "fmt"
    "net/http"
    "strings"
    "log"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })                                       //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

从上面的代码可以看到,要编写一个Web服务器很简单,首先调用http.HandleFunc()设置路由和响应处理函数,调用http.ListenAndServe()去监听端口,等待客户端访问即可。那http包又为我们做了什么呢,接下来我将分析一下http包的代码执行流程。

http包源码分析
http包有关路由部分

根据上面代码,首先是调用了http.HandleFunc(),它的定义如下,实现了将传入的处理响应函数与对应的path进行匹配。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

所以在Handle()函数中默认的路由是怎样匹配的呢,先看下面的两个struct,它们存放了默认的路由规则

type ServeMux struct {
	mu sync.RWMutex   //锁机制,因为请求会涉及到并发处理
	m  map[string]muxEntry  //路由规则,使用map将string对应mux实体,这里的string是注册的路由表达式
	hosts bool        //是否在任意的规则中带有host信息
}
type muxEntry struct {
	explicit bool     //是否精确匹配
	h        Handler  //这个路由表达式对应的处理响应函数
	pattern  string   //匹配字符串
}

根据http.HandleFunc()中的代码,执行了mux.Handle(),这个函数对传入的path进行解析,然后向ServeMux中添加路由规则

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

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

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
    //增加一个新的匹配规则
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
     
    //根据path的第一个字母判断是否有host
    if pattern[0] != '/' {
        mux.hosts = true
    }
 
	// Helpful behavior:
	// If pattern is /tree/, insert an implicit permanent redirect for /tree.
	// It can be overridden by an explicit registration.
	n := len(pattern)
	if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
		// If pattern contains a host name, strip it and use remaining
		// path for redirect.
		path := pattern
		if pattern[0] != '/' {
			// In pattern, at least the last character is a '/', so
			// strings.Index can't be -1.
			path = pattern[strings.Index(pattern, "/"):]
		}
		url := &url.URL{Path: path}
		mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
	}
}

既然添加了路由规则,那么如果客户端进行访问,是怎样查找到对应的路由规则呢,对过程mux.ServerHTTP->mux.Handler->mux.handler->mux.match进行追踪,找到了路由匹配函数match()。这个函数的实现解释了为什么会匹配最长的最佳匹配,比如传入/user/hh,不是先匹配/user/,而是匹配了/user/hh。

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        //如果匹配到了一个规则,还会继续匹配并且判断path的长度是否最长
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}
http包有关监听与服务部分

接下来就是执行http.ListenAndServe(),可以发现这个函数首先实例化了Server,接着调用了Server.ListenAndServe()

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

ListenAndServe()函数调用了net.Listen("tcp",addr)监听端口,接着调用了srv.Serve()

func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)    //监听端口
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

Server()函数启动一个for循环,然后在循环体中Accept请求,对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()。使用goroutines来处理Conn的读写事件,这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件,这样使Go实现了高并发和高效能。

	for {
		rw, e := l.Accept()
		if e != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve(ctx)
	}

在for循环里面,我们可以看到客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。

在为客户端请求进行服务的c.serve()中,会读取每个请求的内容w, err := c.readRequest(),并且判断handler是否为空,如果没有设置handler则为DefaultServeMux,然后调用handler的ServeHttp()

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    //这里handler为简单web服务器代码中http.ListenAndServe中的第二个参数
    handler := sh.srv.Handler
    if handler == nil {
        //如果handler为空则使用DefaultServeMux进行处理
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    //如果需要使用自定义的mux,就需要实现ServeHTTP方法也就是Handler接口
    handler.ServeHTTP(rw, req)
}

这里需要注意的是Handler是一个接口,如一开始的web服务器代码中,虽然我们并没有实现ServeHTTP(),但是在http包里面还定义了一个类型HandlerFunc,这个类型默认就实现了ServeHTTP(),在调用http.HandleFunc()的时候已经将自定义的handler处理函数强制转为HandlerFunc类型

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

所以以上就是http包的整个的代码执行过程

gorilla/mux包

mux包实现了注册路由以及对传入的请求匹配到各自的处理程序的功能,实现了http.Handler接口与http.ServeMux兼容,并且可以支持正则路由还可以按照Method,header,host等信息匹配。

使用mux包编写的简单web服务器

下面是一个使用了mux包的简单的web服务器,实现在客户端访问http://127.0.0.1:9090/的时候响应内容为Hello World!

package main

import (
    "fmt"
    "net/http"
    "github.com/gorilla/mux"
    "log"
)
func HomeHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World!\n"))
}
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Category: %v\n", vars["category"])
}
func main() {
    r := mux.NewRouter()
    r.Path("/").HandlerFunc(HomeHandler)
    //允许category参数存在
    r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)

    log.Fatal(http.ListenAndServe(":9090", r))
}
mux包源码分析

NewRouter()方法开始,它实例化了Router,并且默认的KeepContext设置为flase。

// NewRouter returns a new router instance.
func NewRouter() *Router {
	return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
}

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

	// Parent route, if this is a subrouter.
	parent parentRoute
	// Routes to be matched, in order.
	routes []*Route
	// Routes by name for URL building.
	namedRoutes map[string]*Route
	// See Router.StrictSlash(). This defines the flag for new routes.
	strictSlash bool
	// See Router.SkipClean(). This defines the flag for new routes.
	skipClean bool
	// If true, do not clear the request context after handling the request.
	// This has no effect when go1.7+ is used, since the context is stored
	// on the request itself.
	KeepContext bool
	// see Router.UseEncodedPath(). This defines a flag for all routes.
	useEncodedPath bool
	// Slice of middlewares to be called after a match is found
	middlewares []middleware
}
mux包创建路由部分

接下来调用了Path()方法,该函数使用NewRoute()Router创建一个新路由,并调用新路由Route对象的Path()方法。也可以直接使用r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)所完成的工作和r.Path("/").HandlerFunc(HomeHandler)一样

func (r *Router) Path(tpl string) *Route {
    return r.NewRoute().Path(tpl)
}

func (r *Router) NewRoute() *Route {
    //会从Router中继承某些属性
    route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath}
    //将新路由添加到Router中
    r.routes = append(r.routes, route)
    return route
}

func (r *Route) Path(tpl string) *Route {
    r.err = r.addRegexpMatcher(tpl, false, false, false)
    return r
}

func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
	*http.Request)) *Route {
	return r.NewRoute().Path(path).HandlerFunc(f)
}

Path中调用的addRegexpMatcher()方法,实现了根据传入的tpl创建正则表达式的匹配以及调用newRouteRegexp()方法。newRouteRegexp()解析一个路由模板并返回一个routeRegexp,用于匹配主机、路径或查询字符串

// addRegexpMatcher adds a host or path matcher and builder to a route.
func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
	//此处省略一些代码.....
	rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
		strictSlash:    r.strictSlash,
		useEncodedPath: r.useEncodedPath,
	})
	//此处省略一些代码.....
	r.addMatcher(rr)
	return nil
}

func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) {
	// braceIndices判断{ }是否成对并且正确出现,idxs是'{' '}'在表达式tpl中的下标数组
	idxs, errBraces := braceIndices(tpl)
	if errBraces != nil {
		return nil, errBraces
	}

	template := tpl
	defaultPattern := "[^/]+"
	if typ == regexpTypeQuery {
		defaultPattern = ".*"
	} else if typ == regexpTypeHost {
		defaultPattern = "[^.]+"
	}
	//如果没有匹配斜杠
	if typ != regexpTypePath {
		options.strictSlash = false
	}
	endSlash := false
	if options.strictSlash && strings.HasSuffix(tpl, "/") {
		tpl = tpl[:len(tpl)-1]
		endSlash = true
	}
	//保存所需要提取的所有变量名称
	varsN := make([]string, len(idxs)/2)
	varsR := make([]*regexp.Regexp, len(idxs)/2)
	pattern := bytes.NewBufferString("")
	pattern.WriteByte('^')
	reverse := bytes.NewBufferString("")
	var end int
	var err error
	//构造正则表达式
	for i := 0; i < len(idxs); i += 2 {
		raw := tpl[end:idxs[i]]
		end = idxs[i+1]
		//parts=[]{"路径中设置的变量","匹配变量的正则表达式"}
		parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
		//name="路径中设置的变量"
		name := parts[0]
		patt := defaultPattern
		if len(parts) == 2 {
			 //patt="匹配变量的正则表达式"
			patt = parts[1]
		}
		if name == "" || patt == "" {
			return nil, fmt.Errorf("mux: missing name or pattern in %q",
				tpl[idxs[i]:end])
		}
	    //构造出最终的正则表达式
		fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)

		fmt.Fprintf(reverse, "%s%%s", raw)
		varsN[i/2] = name
		varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
		if err != nil {
			return nil, err
		}
	}
	raw := tpl[end:]
	pattern.WriteString(regexp.QuoteMeta(raw))
	if options.strictSlash {
		pattern.WriteString("[/]?")
	}
	if typ == regexpTypeQuery {
		if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
			pattern.WriteString(defaultPattern)
		}
	}
	if typ != regexpTypePrefix {
		pattern.WriteByte('$')
	}
	reverse.WriteString(raw)
	if endSlash {
		reverse.WriteByte('/')
	}
	//编译最终的正则表达式
	reg, errCompile := regexp.Compile(pattern.String())
	if errCompile != nil {
		return nil, errCompile
	}
	if reg.NumSubexp() != len(idxs)/2 {
		panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
			"Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
	}
	//返回routeRegexp
	return &routeRegexp{
		template:   template,
		regexpType: typ,
		options:    options,
		regexp:     reg,
		reverse:    reverse.String(),
		varsN:      varsN,
		varsR:      varsR,
	}, nil
}
mux包实现路由匹配部分

跟踪HandlerFunc()方法的实现,该函数完成了给特定的路由匹配一个处理响应函数。调用了 Handler()函数,Handler()函数将处理响应函数赋值给该Routehandler

func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
    return r.Handler(http.HandlerFunc(f))
}
// Handler sets a handler for the route.
func (r *Route) Handler(handler http.Handler) *Route {
	if r.err == nil {
		r.handler = handler
	}
	return r
}

到现在已经可以处理客户端的请求,之后的步骤与之前只是用http包步骤类似。但是Router实现了Handler 接口,定义了自己的ServeHTTP()方法。该ServeHTTP()方法使用Match()函数对请求进行匹配,然后执行对应的响应函数。

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if !r.skipClean {
		path := req.URL.Path
		if r.useEncodedPath {
			path = req.URL.EscapedPath()
		}
		// Clean path to canonical form and redirect.
		if p := cleanPath(path); p != path {
			url := *req.URL
			url.Path = p
			p = url.String()

			w.Header().Set("Location", p)
			w.WriteHeader(http.StatusMovedPermanently)
			return
		}
	}
	var match RouteMatch
	var handler http.Handler
	//对请求的路由进行匹配
	if r.Match(req, &match) {
		handler = match.Handler
		req = setVars(req, match.Vars)
		req = setCurrentRoute(req, match.Route)
	}

	if handler == nil && match.MatchErr == ErrMethodMismatch {
		handler = methodNotAllowedHandler()
	}

	if handler == nil {
		handler = http.NotFoundHandler()
	}

	if !r.KeepContext {
		defer contextClear(req)
	}

	handler.ServeHTTP(w, req)
}

Match()方法首先会遍历Router中的所有路由routeMatch方法,如果有匹配到,则直接返回,否则返回NotFoundHandler


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
}

在最上层调用mux.Vars()则可以取出该http.Request所有相关联的变量的信息。val实际上是一个map[string][string],存放该请求对应的变量值集合。


func setVars(r *http.Request, val interface{}) {
	//设置参数时候,val实际上时一个map[string][string],存放该请求对应的变量值集合
    context.Set(r, varsKey, val)
}

func Vars(r *http.Request) map[string]string {
    if rv := context.Get(r, varsKey); rv != nil {
        return rv.(map[string]string)
    }
    return nil
}

以上只是mux包部分功能的实现,更多的功能解读见mux包的github地址

使用第三方包开发cloudgo

下面的cloudgo程序使用了negronimuxrender包,实现了用-p参数可以指定监听的端口号,并且对特定路由/hello/{name}进行处理。

main.go

package main

import (
	flag "github.com/spf13/pflag"
	"github.com/cyulei/cloudgo/service"
	"os"
)
const (
	PORT string = "8000"  //默认端口8000
)

func main() {
	var port string
	//得到环境变量PORT
	port = os.Getenv("PORT")
	if (len(port) <= 0) {
		port = PORT
	}
	//对命令参数进行绑定
	flag.StringVarP(&port, "port", "p", "8000", "define server port")
	flag.Parse()
	//绑定路由
	server := service.NewServer()
	//监听端口,negroni中的Run函数
	server.Run(":" + port)
}

service.go

package service
import (
	"net/http"
	"github.com/urfave/negroni"
	"github.com/gorilla/mux"
	"github.com/unrolled/render"
)
//初始化路由,并绑定请求方式GET以及响应回调函数
func initRouter(r *mux.Router, formatter *render.Render) {
	r.HandleFunc("/hello/{name}", helloHandler(formatter)).Methods("GET")
}

func NewServer() *negroni.Negroni {
	formatter := render.New(render.Options {
	    IndentJSON: true,
	})
	
	r := mux.NewRouter()
	//negroni包中初始化服务
	server := negroni.Classic()
	initRouter(r, formatter)
	server.UseHandler(r)
	return server
}

func helloHandler(formatter *render.Render) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		//获取请求的参数信息
		vars := mux.Vars(r)
		name := vars["name"]
		//使用render包中JSON函数进行解析
		formatter.JSON(w, http.StatusOK, map[string]string{"name": name})
	}
}

最后程序执行结果如下
执行结果

小结

这次通过阅读http包源码了解了一个web服务器是如何实现路由匹配和请求响应的,而且也看到了一些go编程技巧比如闭包,接口之类的。阅读mux源码后知道了如果要对原始的路由处理要修改应该怎样处理,mux这样的第三方包也是简化了编程使go的web服务程序开发更加高效和灵活。

参考资料

HTTP 协议 与 golang web 应用服务
mux包的github地址
render包的github地址
negroni包的github地址
Go的http包详解
mux源码解读

从1990年代开始,IT的快速发展为传统服务业带来了的巨大的革新并逐步形成了知识经济为主体的现代服务业。同第一产业的农业和第二产业的工业一样,服务业的快速发展也需要相应的理论体系和工程技术加以支持。IBM公司于2004年提出的"服务、管理与工程(Service Sciences, Management and Engineering, SSME)",试图将传统的服务相关科的知识整合起来形成一个称为"服务"的独立科,吸引术界、教育界和工业界共同关注"服务"的研究与实践, 进而提高服务产业的水平。"服务计算"正是关注服务中基础理论、技术体系和工程实践的科门类,高等校培养的面向现代服务业的科技型人才必须具备该科的相关知识及应用能力。作为现代服务的奠基石,服务计算已成为一项桥接商业服务与信息技术服务的跨科的科技术。IEEE认为服务计算已成为面向现代服务业的一门新的基础科。服务计算已经成为新兴的系统构造和 企业管理模型,产业界迫切需要掌握服务计算相关理论和技术的软件工程师和管理人员。本课程面对这一需求,涵盖了服务计算方向的主要知识点,主要内容包括服务计算概要、面向服务的体系结构(方法)、服务计算技术(技术观)、Web服务基础(实现式)、实时服务计算(航空航天特色)和服务计算的基础理论(理论点)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值