使用go搭建简单的http服务及源码解析

199 阅读7分钟

创建一个简单的http服务

使用go语言搭建一个http服务事是件很简单的事情,如果代码逻辑很简单的话甚至不用框架就行。话不多说,直接看代码吧。

package main

import (
	"fmt"
	"net/http"
)

func echo(rw http.ResponseWriter, r *http.Request) {
	var rspText string
	params := r.URL.Query() // 获取查询参数
	values, ok := params["name"]
	if ok && len(values) > 0 {
		rspText = fmt.Sprintf("hello, %s!", values[0])
	} else {
		rspText = "hello, boy!"
	}
	rw.Header().Add("Content-Type", "text/plain") // 添加header,值得注意的是,header必须在Write之前调用,否则不会生效
	rw.WriteHeader(http.StatusOK)                 // 这里可以省略
	rw.Write([]byte(rspText))
}

func main() {
	http.HandleFunc("/echo", echo) // 注册路由以及回调函数
	// 监听ip和端口,第二个参数为nil,则使用默认的DefaultServeMux来处理请求
	if err := http.ListenAndServe("127.0.0.1:10001", nil); err != nil {
		panic(err)
	}
}

运行以及返回结果

~ http :10001/echo name==coolboy -v
GET /echo?name=coolboy HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:10001
User-Agent: HTTPie/2.0.0


HTTP/1.1 200 OK
Content-Length: 15
Content-Type: text/plain
Date: Wed, 19 Jan 2022 08:00:36 GMT

hello, coolboy!

可看出go语言创建http服务最简单只需要两步

  1. 使用http.HandleFunc绑定路由以及路由对应的回调函数
  2. 使用http.ListenAndServe指定监听端口,并启动服务

下面我们第一步的去分析这两个函数。

了解http.HandleFunc

首先我们先看下源码,go语言的源码注释还是可以的,借助源码我们可以方便正确的了解该函数的作用

// file: net/http/server.go  
var defaultServeMux ServeMux

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
// 这里可以看出,实际上是调用了DefaultServeMux.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

首先看参数类型,第一个是路由地址,没啥好说的。不过注释提示了,如果想进一步了解路由的匹配规则,可以查看ServeMux的文档。第二个参数也限定了回调函数的类型。下面我们具体查看下回调函数的这两个参数类型。

首先是ResponseWriter,它其实是一个接口类型。看起来也是简单且清晰的,就是三件事三个接口

  1. 写入http response status状态码
  2. 写入http response header
  3. 写入http response body(这里使用byte类型,即支持任意数据类型)
type Header map[string][]string

// 注意,源码注释比较详细,但是字数太多就不放这里了,推荐去看看的
type ResponseWriter interface {
	// 返回一个header字典,调用WriteHeader函数时,将会返回该map数据。但是对header的
    // 修改必须在调用WriteHeader前,否则不会生效
	Header() Header
	// 写数据到http响应里的body中
    // 调用该函数之前,如果没调用WriteHeader则会先调用一次
	Write([]byte) (int, error)
	// 先http相应写入statusCode以及header内容
	WriteHeader(statusCode int)
}

下面再来看看Request,它是一个结构体,而不像ResponseWriter是一个接口类型。

type URL struct {
	Scheme      string
	Opaque      string    // encoded opaque data
	User        *Userinfo // username and password information
	Host        string    // host or host:port
	Path        string    // path (relative paths may omit leading slash)
	RawPath     string    // encoded path hint (see EscapedPath method)
	ForceQuery  bool      // append a query ('?') even if RawQuery is empty
	RawQuery    string    // encoded query values, without '?'
	Fragment    string    // fragment for references, without '#'
	RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

type Request struct {
	// http请求方法:如GET, POST, PUT
	Method string

	// 如上面URL所示,记录了请求url以及url后面的查询参数信息
	URL *url.URL

	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0

	// If a server received a request with header lines,
	//
	//	Host: example.com
	//	accept-encoding: gzip, deflate
	//	Accept-Language: en-us
	//	fOO: Bar
	//	foo: two
	//
	// then
	//
	//	Header = map[string][]string{
	//		"Accept-Encoding": {"gzip, deflate"},
	//		"Accept-Language": {"en-us"},
	//		"Foo": {"Bar", "two"},
	//	}
	Header Header

	// Body is the request's body.
	//
	// For client requests, a nil body means the request has no
	// body, such as a GET request. The HTTP Client's Transport
	// is responsible for calling the Close method.
    // 这里由提示,如果由使用Body,则要记得调用Close方法关闭Body
	Body io.ReadCloser

	GetBody func() (io.ReadCloser, error)

	// ContentLength records the length of the associated content.
	// The value -1 indicates that the length is unknown.
	// Values >= 0 indicate that the given number of bytes may
	// be read from Body.
	//
	// For client requests, a value of 0 with a non-nil Body is
	// also treated as unknown.
	ContentLength int64

	// TransferEncoding lists the transfer encodings from outermost to
	// innermost. An empty list denotes the "identity" encoding.
	// TransferEncoding can usually be ignored; chunked encoding is
	// automatically added and removed as necessary when sending and
	// receiving requests.
	TransferEncoding []string

	Close bool
	Host string

	// Form contains the parsed form data, including both the URL
	// field's query parameters and the PATCH, POST, or PUT form data.
	// This field is only available after ParseForm is called.
	// The HTTP client ignores Form and uses Body instead.
    // 注意的是,这里的值包括url和body里的两部分
	Form url.Values

	// PostForm contains the parsed form data from PATCH, POST
	// or PUT body parameters.
	// This field is only available after ParseForm is called.
	// The HTTP client ignores PostForm and uses Body instead.
	PostForm url.Values

	MultipartForm *multipart.Form
	Trailer Header
	RemoteAddr string
	RequestURI string
	TLS *tls.ConnectionState
	Cancel <-chan struct{}
    
	// Response is the redirect response which caused this request
	// to be created. This field is only populated during client
	// redirects.
	Response *Response
	ctx context.Context
}

可以看出,Request结构包含了http请求的信息并做了简单的处理,同时也内置了一些基础的函数,大家可以通过IDE的智能提示或者源码看看。

了解ListenAndServe

ListenAndServe函数主要是指定监听的ip和端口,并指定一个Handler处理监听的请求。

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

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

Handler也是一个接口类型,但是Handler里的ServeHTTP(ResponseWriter, *Request)的参数类型是不是很熟悉,跟我们上面路由回调函数一模一样,这两者之间是不是有啥不可告人的事呢?

Server涉及到http服务器的处理细节,我们先不细究,主要还是看看Handler

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}

上面是Server处理http请求的一个关键代码,可以看到如果handler为空,则会使用DefaultServeMux来代替,那么我们下面就来仔细探究DefaultServeMux。请看代码:

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

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
}

// ServeMux的一些关键函数
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

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
	}
}


// 这个函数的作用,就是请求url(路由)匹配一个handler(路由回调函数)
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	// 不支持 http.method == CONNECT
	if r.Method == "CONNECT" {
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}
		return mux.handler(r.Host, r.URL.Path)
	}

	// 获取host和path
    // 比如url="https://www.baidu.com:8080/hello?a=b
    // 则 host = www.baidu.com
    // path = /hello
	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

    // 打个比方,如过请求 https://baidu.com/aaa 没有匹配成功
    // 则重定向到 https://baidu.com/aaa/
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

    // 这里的path跟URL里的path不一致,则重定向到URL里的path去
	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}

	return mux.handler(host, r.URL.Path)
}

// 其实这个才是真正的路由匹配函数,注意它跟Handler首字母大小写不同
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	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) {
	// 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, ""
}

// 这里就是上面Server部分所说,当一个http请求进来时,默认使用DefaultServeMux
// 从而进入到这个函数来。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
    // 根据路由规则获取对应路由回调函数
	h, _ := mux.Handler(r)
    // 执行路由回调函数
	h.ServeHTTP(w, r)
}

另一种写法

上面里的main函数可以换成下面写法,两者是等价的

func main() {
	server := http.ServeMux{}
	server.HandleFunc("/echo", echo) // 注册路由以及回调函数
	// 监听ip和端口,第二个参数为nil,则使用默认的DefaultServeMux来处理请求
	if err := http.ListenAndServe("127.0.0.1:10001", &server); err != nil {
		panic(err)
	}
}

进一步思考

通过上面的源码,我可以可以发现go语言更多的是定义http处理相关接口,比如上面的ServeMux,它负责将路由映射到处理函数中。实际上我们完成可以自己写一个,只要实现相关的接口就ok。

这里是不是可以看出go语言的一些哲学呢?比如提供接口,让用户去决定具体实现?(原谅我知识面不够,不知道怎么说...)

最后我们想一想,利用这些接口,我们是不是可以开发一个简单的go框架呢,比如说最简单的MVC模型框架?我倒是挺想试试的!