开发web服务程序

开发web服务程序

1. 概述

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

任务目标

  1. 熟悉 go 服务器工作原理
  2. 基于现有 web 库,编写一个简单 web 应用类似 cloudgo。
  3. 使用 curl 工具访问 web 程序
  4. 对 web 执行压力测试
2. 任务要求

基本要求

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

扩展要求

  1. 通过源码分析、解释一些关键功能实现
  2. 选择简单的库,如 mux 等,通过源码分析、解释它是如何实现扩展的原理,包括一些 golang 程序设计技巧。
3. web程序设计实现:大多数源代码文件在课件上已经实现,只需要略作修改即可
  1. 首先在cloudgo文件下创建目录asserts,存放静态内容。结构如下:

    assets(静态文件虚拟根目录)
      |-- js
      |-- images
      +-- css
    

    net/http库提供了现成的静态文件服务,直接使用即可。

     mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/"))) 
    //语句就实现了静态文件服务。它的含义是将 path 以 “/” 前缀的 URL 都定位到 webRoot + "/assets/" 为虚拟根目录的文件系统。
    
  2. 实现hello页面:

    func setHello(w http.ResponseWriter, req *http.Request) {
    	w.Write([]byte("Hello, World!"))
    }
    

    w和req分别是服务器相应于客户端请求

  3. 实现登陆界面

    //读取templates文件下的index.html文件即可将对应页面信息打开
    func indexPage(w http.ResponseWriter, req *http.Request) {
    	page := template.Must(template.ParseFiles("templates/index.html"))
    	page.Execute(w, nil)
    }
    
  4. 实现登录信息的保存

    func index2info(w http.ResponseWriter, req *http.Request) {
    	req.ParseForm()
    	usr := req.Form["username"][0]
    	se := req.Form["usersex"][0]
    	pwd := req.Form["password"][0]
    	mail := req.Form["mail"][0]
    
    	page := template.Must(template.ParseFiles("templates/info.html"))
    
    	page.Execute(w, map[string]string{
    		"usr":  usr,
    		"se":   se,
    		"pwd":  pwd,
    		"mail": mail,
    	})
    }
    

    将登陆页面输入的信息保存到一个新页面的表格中,实现表格提交的功能。

  5. 登陆页面和表格页面对应的html文件在templayes文件下

  6. 实现服务器的创建,通过negroni包实现端口的监听功能

    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")
    		} else {
    			webRoot = root
    			//fmt.Println(root)
    		}
    	}
    	
    	mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
        //hello页面
    	mx.HandleFunc("/hello", setHello).Methods("GET")
    	mx.HandleFunc("/", indexPage).Methods("GET")
    	mx.HandleFunc("/", index2info).Methods("POST")
        //static页面
    	mx.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(webRoot+"/asserts/"))))
    	//mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
    }
    
    

包括post、get、cookie、url等信息是用户请求信息的一部分,服务器端需要对请求信息进行解析。本次服务器端的功能主要是借助html文件和标准包的相关功能实现的。

4. 代码阅读:[Mux](gorilla/mux: A powerful HTTP router and URL matcher for building Go web servers with 🦍 (github.com))
  1. 功能:实现了一个简单的路由器结构以及对应的调度程序,可以将传入的请求和处理程序匹配。

  2. 定义路由器和路由器间链路的数据结构

    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
    }
    

    从上面路由器的数据结构可以看出,router []*Route说明对于每个路由器,都可以有多个链路与其相连,namedRoutes map[string]*Route定义了路由器的路由表,每个端口都对应特定地址的转发端口。之后我们就需要了解链路的数据结构。

    // Route stores information to match a request and build URLs.
    type Route struct {
    	// Request handler for the route.
    	handler http.Handler
    	// If true, this route never matches: it is only used to build URLs.
    	buildOnly bool
    	// The name used to build URLs.
    	name string
    	// Error resulted from building a route.
    	err error
    	// "global" reference to all named routes
    	namedRoutes map[string]*Route
    	// config possibly passed in from `Router`
    	routeConf
    }
    

    可以看到每条链路和特定的路由器请求有关,通过判断与传入地址的URL是否匹配而确定路由器是否要使用该条链路进行转发。

  3. 之后就需要了解路由器及链路是如何与地址进行匹配的,需要查看对应的match方法。

    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
    }
    

    Match方法尝试将传入路由器的地址和当前路由器的转发表中的信息进行匹配,如果路由器或者该路由器的子路由器匹配成功,返回true,否则返回false并将地址填充到MatchErr字段。

  4. 了解地址如何在路由器中进行匹配之后,路由器需要将请求通过特定的链路将请求转发到下一跳地址,这就是Serve HTTP函数的功能

    // ServeHTTP dispatches the handler registered in the matched route.
    //
    // When there is a match, the route variables can be retrieved calling
    // mux.Vars(request).
    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 {
    
    			// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
    			// This matches with fix in go 1.2 r.c. 4 for same problem.  Go Issue:
    			// http://code.google.com/p/go/issues/detail?id=5252
    			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 = requestWithVars(req, match.Vars)
    		req = requestWithRoute(req, match.Route)
    	}
    
    	if handler == nil && match.MatchErr == ErrMethodMismatch {
    		handler = methodNotAllowedHandler()
    	}
    
    	if handler == nil {
    		handler = http.NotFoundHandler()
    	}
    
    	handler.ServeHTTP(w, req)
    }
    
  5. 可以看到如果转发表没有对应的转发信息,路由器就需要找到一个空链路进行转发,用到下面两种方法。

    func requestWithVars(r *http.Request, vars map[string]string) *http.Request {
    	ctx := context.WithValue(r.Context(), varsKey, vars)
    	return r.WithContext(ctx)
    }
    
    func requestWithRoute(r *http.Request, route *Route) *http.Request {
    	ctx := context.WithValue(r.Context(), routeKey, route)
    	return r.WithContext(ctx)
    }
    
  6. 链路每次收到一个请求,都会通过一个Walk函数查看当前的路由器和链路,同时还有请求之前已经经过的链路。

    func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
    	for _, t := range r.routes {
    		err := walkFn(t, r, ancestors)
    		if err == SkipRouter {
    			continue
    		}
    		if err != nil {
    			return err
    		}
    		for _, sr := range t.matchers {
    			if h, ok := sr.(*Router); ok {
    				ancestors = append(ancestors, t)
    				err := h.walk(walkFn, ancestors)
    				if err != nil {
    					return err
    				}
    				ancestors = ancestors[:len(ancestors)-1]
    			}
    		}
    		if h, ok := t.handler.(*Router); ok {
    			ancestors = append(ancestors, t)
    			err := h.walk(walkFn, ancestors)
    			if err != nil {
    				return err
    			}
    			ancestors = ancestors[:len(ancestors)-1]
    		}
    	}
    	return nil
    }
    
    
  7. 路由器和链路之间还定义了一个共享配置信息结构

    type routeConf struct {
    	// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
    	useEncodedPath bool
    	// If true, when the path pattern is "/path/", accessing "/path" will
    	// redirect to the former and vice versa.
    	strictSlash bool
    	// If true, when the path pattern is "/path//to", accessing "/path//to"
    	// will not redirect
    	skipClean bool
    	// Manager for the variables from host and path.
    	regexp routeRegexpGroup
    	// List of matchers.
    	matchers []matcher
    	// The scheme used when building URLs.
    	buildScheme string
    	buildVarsFunc BuildVarsFunc
    }
    
  8. 多路路由器Mux可以将传入的请求和已经注册的路由表相匹配,并提供对应的调度程序。程序根据URL 的host, path, path prefix, schemes, header and query values等参数信息来匹配请求。路由表可以被构建和维护,提高了对网络的利用率。路由器之间也可以并行或者嵌套使用,可以用来实现局域路由网络。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值