Samael

web.go源码学习笔记

最近在学习Go, 发现web.go, 很适合我这种初学Go的人去阅读, 代码行数不长, 加注释不到2000行. 这里记录一下学习笔记

web.go是什么

web.go是基于Go语言开发的一个简单的web框架包括了路由匹配, cookie, fastCgi等功能. 是对Go内置的HTTP库的一个抽象封装. 对于编写小的web应用是一个不错的工具,

工作流程

web.go的启动流程十分简单, 增加路由与处理函数就可以:

package main
    
import (
    "github.com/hoisie/web"
)
    
func hello(val string) string { return "hello " + val } 
    
func main() {
    web.Get("/(.*)", hello)
    web.Run("0.0.0.0:9999")
}

不过与Go内置的http不同的是:

  1. 路由匹配正则表达式, 批量进行不同路由的处理
  2. 处理函数的传入参数不同
  3. 处理函数的返回值不同

处理函数也可以传两个参数, 第一个参数就类似http.ResponseWriter

func hello(ctx *web.Context, val string) {
	ctx.WriteString("hello " + val)
}

web.go也对于不同的HTTP的Method进行了分别, 而不是在原有的HandleFunc中进行Method的区分

项目结构

├── LICENSE
├── Readme.md
├── examples    
│   ├── arcchallenge.go
│   ├── cookie.go
│   ├── hello.go
│   ├── logger.go
│   ├── multipart.go
│   ├── multiserver.go
│   ├── params.go
│   ├── secure_cookie.go
│   ├── streaming.go
│   └── tls.go
├── fcgi.go
├── helpers.go
├── scgi.go
├── secure_cookie.go
├── server.go
├── ttycolors.go
├── web.go
└── web_test.go

web.go

包含了框架中的核心结构体Context, 把内置框架中的Request, ResponseWriter, 与Params都封装在了一个结构体内, 并对以上结构体进行了抽象封装

type Context struct {
	Request *http.Request
	Params  map[string]string
	Server  *Server
	http.ResponseWriter
}

在web库调用时内部会自动生成一个mainServer变量

server.go

另一个核心结构体Server, 包含了服务器的一些配置, 如路由, 日志, 当前的环境

type Server struct {
	Config *ServerConfig
	routes []route
	Logger *log.Logger
	Env    map[string]interface{}
	//save the listener so it can be closed
	l       net.Listener
	encKey  []byte
	signKey []byte
}

服务器启动

当服务器配置做好之后, 调用web.Run()(如果想用同时运行两个以上的服务器可以参考示例文件mutliserver.go), 框架会初始化服务器相关的配置

func (s *Server) initServer() {
	if s.Config == nil {
		s.Config = &ServerConfig{}
	}

	if s.Logger == nil {
		s.Logger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
	}

	if len(s.Config.CookieSecret) > 0 {
		s.Logger.Println("Generating cookie encryption keys")
		s.encKey = genKey(s.Config.CookieSecret, "encryption key salt")
		s.signKey = genKey(s.Config.CookieSecret, "signature key salt")
	}
}

func (s *Server) Run(addr string) {
	s.initServer()

	mux := http.NewServeMux()
	if s.Config.Profiler {
		mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
		mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
		mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
		mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
	}
	mux.Handle("/", s)

	l, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("ListenAndServe:", err)
	}

	s.Logger.Printf("web.go serving %s\n", l.Addr())

	s.l = l
	err = http.Serve(s.l, mux)
	s.l.Close()
}

路由添加

type route struct {
	r           string
	cr          *regexp.Regexp
	method      string
	handler     reflect.Value
	httpHandler http.Handler
}
func (s *Server) addRoute(r string, method string, handler interface{}) {
	cr, err := regexp.Compile(r)
	if err != nil {
		s.Logger.Printf("Error in route regex %q\n", r)
		return
	}

	switch handler.(type) {
	case http.Handler:
		s.routes = append(s.routes, route{r: r, cr: cr, method: method, httpHandler: handler.(http.Handler)})
	case reflect.Value:
		fv := handler.(reflect.Value)
		s.routes = append(s.routes, route{r: r, cr: cr, method: method, handler: fv})
	default:
		fv := reflect.ValueOf(handler)
		s.routes = append(s.routes, route{r: r, cr: cr, method: method, handler: fv})
	}
}

当向server添加一个新的路由规则时:

  1. 编译正则表达式, 并缓存
  2. 通过反射匹配对应的Handler函数类型
  3. 增加到[]routes

路由匹配

// ServeHTTP is the interface method for Go's http server package
func (s *Server) ServeHTTP(c http.ResponseWriter, req *http.Request) {
	s.Process(c, req)
}

// Process invokes the routing system for server s
func (s *Server) Process(c http.ResponseWriter, req *http.Request) {
	route := s.routeHandler(req, c)
	if route != nil {
		route.httpHandler.ServeHTTP(c, req)
	}
}

func (s *Server) routeHandler(req *http.Request, w http.ResponseWriter) (unused *route) {
	requestPath := req.URL.Path
	ctx := Context{req, map[string]string{}, s, w}

	//set some default headers
	ctx.SetHeader("Server", "web.go", true)
	tm := time.Now().UTC()

	//ignore errors from ParseForm because it's usually harmless.
	req.ParseForm()
	if len(req.Form) > 0 {
		for k, v := range req.Form {
			ctx.Params[k] = v[0]
		}
	}

	defer s.logRequest(ctx, tm)

	ctx.SetHeader("Date", webTime(tm), true)

	if req.Method == "GET" || req.Method == "HEAD" {
		if s.tryServingFile(requestPath, req, w) {
			return
		}
	}

	for i := 0; i < len(s.routes); i++ {
		route := s.routes[i]
		cr := route.cr
		//if the methods don't match, skip this handler (except HEAD can be used in place of GET)
		if req.Method != route.method && !(req.Method == "HEAD" && route.method == "GET") {
			continue
		}

		if !cr.MatchString(requestPath) {
			continue
		}
		match := cr.FindStringSubmatch(requestPath)

		if len(match[0]) != len(requestPath) {
			continue
		}

		if route.httpHandler != nil {
			unused = &route
			// We can not handle custom http handlers here, give back to the caller.
			return
		}

		// set the default content-type
		ctx.SetHeader("Content-Type", "text/html; charset=utf-8", true)

		var args []reflect.Value
		handlerType := route.handler.Type()
		if requiresContext(handlerType) {
			args = append(args, reflect.ValueOf(&ctx))
		}
		for _, arg := range match[1:] {
			args = append(args, reflect.ValueOf(arg))
		}

		ret, err := s.safelyCall(route.handler, args)
		if err != nil {
			//there was an error or panic while calling the handler
			ctx.Abort(500, "Server Error")
		}
		if len(ret) == 0 {
			return
		}

		sval := ret[0]

		var content []byte

		if sval.Kind() == reflect.String {
			content = []byte(sval.String())
		} else if sval.Kind() == reflect.Slice && sval.Type().Elem().Kind() == reflect.Uint8 {
			content = sval.Interface().([]byte)
		}
		ctx.SetHeader("Content-Length", strconv.Itoa(len(content)), true)
		_, err = ctx.ResponseWriter.Write(content)
		if err != nil {
			ctx.Server.Logger.Println("Error during write: ", err)
		}
		return
	}

	// try serving index.html or index.htm
	if req.Method == "GET" || req.Method == "HEAD" {
		if s.tryServingFile(path.Join(requestPath, "index.html"), req, w) {
			return
		} else if s.tryServingFile(path.Join(requestPath, "index.htm"), req, w) {
			return
		}
	}
	ctx.Abort(404, "Page not found")
	return
}

从客户端来的所有请求都会先进入Process函数, 之后在routerHandler中对路由进行匹配, 并转入相应的Handler中, 具体流程如下:

  1. 创建Context
  2. 写入一些HTTP头信息, 如服务器框架, 请求处理时间
  3. 把Params放入Context的Map中
  4. 判断当前请求是不是静态文件如果是, 而返回静态文件
  5. 对routers中的正则表达式与Method进行匹配
  6. 如果匹配成功, 则判断Handler参数的数量, 并处理Handler
  7. 如果没有对应的路由, 则搜索index.html/index.htm, 没有找到返回404

调用函数(safelyCall)

// safelyCall invokes `function` in recover block
func (s *Server) safelyCall(function reflect.Value, args []reflect.Value) (resp []reflect.Value, e interface{}) {
	defer func() {
		if err := recover(); err != nil {
			if !s.Config.RecoverPanic {
				// go back to panic
				panic(err)
			} else {
				e = err
				resp = nil
				s.Logger.Println("Handler crashed with error", err)
				for i := 1; ; i += 1 {
					_, file, line, ok := runtime.Caller(i)
					if !ok {
						break
					}
					s.Logger.Println(file, line)
				}
			}
		}
	}()
	return function.Call(args), nil
}

在使用反射的方式去调用函数时如果Handler抛出了panic, 则根据Config中的RecoverPanic设置来决定异常panic继续向上抛出,还是输出错误日志

杂项

ttfcolor.go

日志输出颜色配置

helper.go

一些辅助函数, 如当前web时间, 生成新的Cookie, 判断目录是否存在

secure_cookie.go

加密Cookie的一些判断


Share this: