IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Go HTTP服务器源码浅析 -> 正文阅读

[网络协议]Go HTTP服务器源码浅析

简单服务器 Demo

在 Go 编写一个服务器非常简单:

func main() {
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		writer.Write([]byte("hello, world!"))
	})
	http.ListenAndServe(":8080", nil)
}

之后便可以访问:

~ curl localhost:8080/
hello, world!

也可以用自定义类型实现 http.Handler 接口的方式:

func main() {
	helloStr := "hello, world!"
	http.Handle("/hello", helloHandler(helloStr)) // 用 helloStr 处理 /hello 请求
	http.ListenAndServe(":8080", nil)
}

type helloHandler string

// Implement http.Handler.
func (h helloHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	writer.Write([]byte(h))
}

上述代码中,向 http.Handle 方法传入了请求路径和相应处理请求的 handler,请求到达时服务器会根据请求路径找到相应的 handler 处理请求。

http.ResponseWriter 接口

ResponseWriter 接口用于让 handler 构建一个 http 响应,其方法如下:

// 返回将要被 WriteHeader 发送的响应头。
Header() Header

// 将数据写到 Http 连接中。
// 如果调用该方法时 WriteHeader 还未被调用,Write 将先调用 WriteHeader(http.StatusOK)。
Write([]byte) (int, error)

// 该方法发送一个包含状态码 statusCode 的 HTTP 响应头
WriteHeader(statusCode int)

Header()

Header 代表了 HTTP 首部中的键值对,其实际是一个哈希表:

type Header map[string][]string

Header 包含的方法主要是对响应头键值对的增删改查,以 Add 方法为例:

// 将键值对添加到响应头中。其中键是大小写不敏感的。
func (h Header) Add(key, value string) {
	textproto.MIMEHeader(h).Add(key, value)
}

它将 Header 转为 MIMEHeader 类型,实际调用了 MIMEHeaderAdd 方法:

func (h MIMEHeader) Add(key, value string) {
	// 将 MIME 头部的键值转换为规范格式,如 "accept-encoding" 转换为 "Accept-Encoding"。
	key = CanonicalMIMEHeaderKey(key)
	h[key] = append(h[key], value)
}

Write([]byte)

该方法将数据写到 HTTP 连接中。

WriteHeader(statusCode int)

该方法发送一个包含状态码 statusCode 的 HTTP 响应头。
于 IANA 登记的状态码位于 http/status.go 中,这里仅列出几个常见的状态码:

package http

// HTTP status codes as registered with IANA.
const (
	StatusContinue           = 100 // RFC 7231, 6.2.1
	StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
	StatusProcessing         = 102 // RFC 2518, 10.1
	StatusEarlyHints         = 103 // RFC 8297

	StatusOK                   = 200 // RFC 7231, 6.3.1
	StatusCreated              = 201 // RFC 7231, 6.3.2
	StatusAccepted             = 202 // RFC 7231, 6.3.3
	StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
	StatusNoContent            = 204 // RFC 7231, 6.3.5

	StatusMultipleChoices   = 300 // RFC 7231, 6.4.1
	StatusMovedPermanently  = 301 // RFC 7231, 6.4.2
	StatusFound             = 302 // RFC 7231, 6.4.3
	StatusSeeOther          = 303 // RFC 7231, 6.4.4
	StatusNotModified       = 304 // RFC 7232, 4.1

	StatusBadRequest                   = 400 // RFC 7231, 6.5.1
	StatusUnauthorized                 = 401 // RFC 7235, 3.1
	StatusPaymentRequired              = 402 // RFC 7231, 6.5.2
	StatusForbidden                    = 403 // RFC 7231, 6.5.3
	StatusNotFound                     = 404 // RFC 7231, 6.5.4
	StatusMethodNotAllowed             = 405 // RFC 7231, 6.5.5
	StatusNotAcceptable                = 406 // RFC 7231, 6.5.6
	
	StatusInternalServerError           = 500 // RFC 7231, 6.6.1
	StatusNotImplemented                = 501 // RFC 7231, 6.6.2
	StatusBadGateway                    = 502 // RFC 7231, 6.6.3
	...
)

var statusText = map[int]string{
	StatusContinue:           "Continue",
	StatusSwitchingProtocols: "Switching Protocols",
	StatusProcessing:         "Processing",
	StatusEarlyHints:         "Early Hints",

	StatusOK:                   "OK",
	StatusCreated:              "Created",
	StatusAccepted:             "Accepted",
	StatusNonAuthoritativeInfo: "Non-Authoritative Information",
	StatusNoContent:            "No Content",

	StatusMultipleChoices:   "Multiple Choices",
	StatusMovedPermanently:  "Moved Permanently",
	StatusFound:             "Found",
	StatusSeeOther:          "See Other",
	StatusNotModified:       "Not Modified",

	StatusBadRequest:                   "Bad Request",
	StatusUnauthorized:                 "Unauthorized",
	StatusPaymentRequired:              "Payment Required",
	StatusForbidden:                    "Forbidden",
	StatusNotFound:                     "Not Found",
	StatusMethodNotAllowed:             "Method Not Allowed",
	StatusNotAcceptable:                "Not Acceptable",
	
	StatusInternalServerError:           "Internal Server Error",
	StatusNotImplemented:                "Not Implemented",
	StatusBadGateway:                    "Bad Gateway",
	...
}

// 根据状态码获取相应的消息。
func StatusText(code int) string {
	return statusText[code]
}

response 结构体

在 http 包的服务器实现中,传给 handlerResponseWriter 的实际类型是 http 包下的 response ,换言之,response 实现了 ResponseWriter 接口。其结构如下:

// response 代表服务器的 HTTP 响应.
type Response struct {
    Status     string // 例如"200 OK"
    StatusCode int    // 例如200
    Proto      string // 例如"HTTP/1.0"
    ProtoMajor int    // 例如1
    ProtoMinor int    // 例如0
    
    // Header保管头域的键值对。
    // 如果回复中有多个头的键相同,Header中保存为该键对应用逗号分隔串联起来的这些头的值
    // (参见RFC 2616 Section 4.2)
    // 被本结构体中的其他字段复制保管的头(如ContentLength)会从Header中删掉。
    //
    // Header中的键都是规范化的,参见CanonicalHeaderKey函数
    Header Header
    
    // Body代表回复的主体。
    // Client类型和Transport类型会保证Body字段总是非nil的,即使回复没有主体或主体长度为0。
    // 关闭主体是调用者的责任。
    // 如果服务端采用"chunked"传输编码发送的回复,Body字段会自动进行解码。
    Body io.ReadCloser
    
    // ContentLength记录相关内容的长度。
    // 其值为-1表示长度未知(采用chunked传输编码)
    // 除非对应的Request.Method是"HEAD",其值>=0表示可以从Body读取的字节数
    ContentLength int64
    
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    TransferEncoding []string
    
    // Close记录头域是否指定应在读取完主体后关闭连接。(即Connection头)
    // 该值是给客户端的建议,Response.Write方法的ReadResponse函数都不会关闭连接。
    Close bool
    
    // Trailer字段保存和头域相同格式的trailer键值对,和Header字段相同类型
    Trailer Header
    
    // Request是用来获取此回复的请求
    // Request的Body字段是nil(因为已经被用掉了)
    // 这个字段是被Client类型发出请求并获得回复后填充的
    Request *Request
    
    // TLS包含接收到该回复的TLS连接的信息。 对未加密的回复,本字段为nil。
    // 返回的指针是被(同一TLS连接接收到的)回复共享的,不应被修改。
    TLS *tls.ConnectionState
}

Header()

Header 方法将 handlerHeader 拷贝到 chunkWriterheader 中,然后将 handlerHeader 返回。

func (w *response) Header() Header {
	if w.cw.header == nil && w.wroteHeader && !w.cw.wroteHeader {
		// 在逻辑写和物理写之间访问响应头,需要对逻辑写入的状态进行快照。
		// 即若 w.cw 中的 header 并未设置,则拷贝一份放到 w.cw 中。
		// response 的 WriteHeader 方法会将 handlerHeader 拷贝到 w.cw 中。
		// 由于 w.cw 中的 header 仅包内可见,且没有修改方法,
		// 后续 w.cw.header 将免于再被修改。
		w.cw.header = w.handlerHeader.Clone()
	}
	w.calledHeader = true	// 标记 handler 通过 Header 方法访问了 handlerHeader。
	return w.handlerHeader
}

Write([]byte)

func (w *response) Write(data []byte) (n int, err error) {
	return w.write(len(data), data, "")
}

调用了其 write 方法:

// either dataB or dataS is non-zero.
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
	// 该连接是否已经被劫持
	// 因为被劫持的连接交由相应的 handler 接管,返回 ErrHijacked
	if w.conn.hijacked() {
		if lenData > 0 {
			caller := relevantCaller()
			w.conn.server.logf("http: response.Write on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		}
		return 0, ErrHijacked
	}

	// canWriteContinue 指示是否能将 100 Continue 写到连接中。
	if w.canWriteContinue.isSet() {
		// 请求体 reader 想要写出 100 Continue 但还未写出。
		// 告诉它不要写出。持有 writeContinueMu 是为了确保当前没有在写出数据。
		w.writeContinueMu.Lock()
		w.canWriteContinue.setFalse()
		w.writeContinueMu.Unlock()
	}

	// 如果还没写入响应码,则将 StatusOK 写入。
	if !w.wroteHeader {
		w.WriteHeader(StatusOK)
	}
	if lenData == 0 {
		return 0, nil
	}
	// 根据状态码判断是否允许写响应体。
	// 当状态码为 100~199, 204, 304 时不允许有响应体。
	if !w.bodyAllowed() {
		return 0, ErrBodyNotAllowed
	}

	w.written += int64(lenData) // 即使最终没有写出,仍计入 written
	// 如果写出的数据大于响应头 Content-Length,则不写出。
	if w.contentLength != -1 && w.written > w.contentLength {
		return 0, ErrContentLength
	}
	if dataB != nil {
		return w.w.Write(dataB)	// 调用 response 的 bufio.Writer 写入数据。
	} else {
		return w.w.WriteString(dataS)
	}
}

WriteHeader(statusCode int)

func (w *response) WriteHeader(code int) {
	// 如果连接被劫持,即连接已被一个 handler 接管,则不写入头部。
	if w.conn.hijacked() {
		caller := relevantCaller()
		w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}
	// 如果已经写过头部,则不再重复写入。
	if w.wroteHeader {
		caller := relevantCaller()	// relevantCaller 找到此方法的调用者
		w.conn.server.logf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}
	checkWriteHeaderCode(code) // 100 <= code <= 999?
	w.wroteHeader = true
	w.status = code

	if w.calledHeader && w.cw.header == nil {
		w.cw.header = w.handlerHeader.Clone()
	}
	// 如果设置了 Content-Length,则将该 header 的值放到 response.contentLength 中
	if cl := w.handlerHeader.get("Content-Length"); cl != "" {
		v, err := strconv.ParseInt(cl, 10, 64)
		if err == nil && v >= 0 {
			w.contentLength = v
		} else {
			w.conn.server.logf("http: invalid Content-Length of %q", cl)
			w.handlerHeader.Del("Content-Length")
		}
	}
}

http.Request

与 http.ResponseWriter 不同的是,http.Request 是一个结构体而不是接口。

// Request 代表了一个服务器收到的请求,或一个 client 将要发出的请求。
type Request struct {

    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //	accept-encoding: gzip, deflate
    //	Accept-Language: en-us
    //	Connection: keep-alive
    // 则:
    //	Header = map[string][]string{
    //		"Accept-Encoding": {"gzip, deflate"},
    //		"Accept-Language": {"en-us"},
    //		"Connection": {"keep-alive"},
    //	}
    // HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
    // 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
    Header Header
    
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"host:port"。
    //
    // 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
    // 如过该字段为"",Request.Write方法会使用URL字段的Host。
    Host string
    
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    Form url.Values
    
    // PostForm是解析好的POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    PostForm url.Values
    
    // MultipartForm是解析好的多部件表单,包括上传的文件。
    // 本字段只有在调用ParseMultipartForm后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
    // (客户端会声明哪些trailer会发送)
    // 在处理器从Body读取时,不能使用本字段。
    // 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
    // (如果客户端发送了这些键值对),此时才可以访问本字段。
    //
    // 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
    // ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
    // 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
    // 一旦请求主体返回EOF,调用者就不可再修改Trailer。
    //
    // 很少有HTTP客户端、服务端或代理支持HTTP trailer。
    Trailer Header
    
    // RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
    // 本字段不是ReadRequest函数填写的,也没有定义格式。
    // 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
    // 客户端会忽略请求中的RemoteAddr字段。
    RemoteAddr string
    
    // RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
    // (参见RFC 2616, Section 5.1)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState
}

http.Handle

在声明对一个路径的处理器时,调用了:

http.Handle("/", helloHandler(handler))

其源码如下:

// Handle 函数对给定路径的处理器在 DefaultServeMux 中进行注册。
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

该函数在 DefaultServeMux 中对路径和处理器进行注册,该函数将 patternhandler 传给了 DefaultServeMuxHandle 方法。
ServeMux 是 HTTP 请求的多路转接器(multiplexer)。它会将每一个接收的请求的 URL 与一个注册模式的列表进行匹配,并调用和 URL 最匹配的模式的处理器。
DefaultServeMux.Handle 实际上将模式放到一个哈希表中;如果路径以 '/' 结尾,则额外将其放到一个根据路径长度由长到短排序的数组中。例如,在匹配时如果不存在 /greetings/meet 路径的处理器,则依次尝试查找 /greetings// 路径的处理器。

// Handle 为一个模式注册处理器。
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")
	}
	// m 是由 pattern 到 muxEntry 的映射,若已经存在则 panic。
	// muxEntry 是由处理器 Handler 和 pattern 组成的结构体。
	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
	// 如果路径以 '/' 结尾,则将新的 muxEntry 插入到 es 中
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)	
	}

	if pattern[0] != '/' {
		mux.hosts = true	// hosts:是否有模式指示了主机名
	}
}

例如,以下三个 handler 分别处理三个路径的请求:

func main() {
	rootHandler := "hello"
	http.Handle("/", helloHandler(rootHandler))
	hiHandler := "hi"
	http.Handle("/hi", helloHandler(hiHandler))
	byeHandler := "bye"
	http.Handle("/bye", helloHandler(byeHandler))
	http.ListenAndServe(":8080", nil)
}

得到 mux.m 的内容如下:
请添加图片描述

http.ListenAndServe

地址的绑定和监听

在添加完各 handler 后,通过调用 http.ListenAndServe 函数开启服务器:

http.ListenAndServe(":8080", nil)

其源码如下:

// ListenAndServe 监听给出的 TCP 地址 addr,然后对到来的连接调用 Serve 方法,
// Serve 方法再利用入参 handler 映射来自该连接的请求。
// 入参 handler 用于对路径进行映射,如果设置为 nil,则 DefaultServeMux 将用于对路径进行映射。
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

ListenAndServe 函数利用 TCP 地址 addr 和地址映射 handler 创建了一个 Server,然后调用了其 ListenAndServe 方法。

// ListenAndServe 方法监听 TCP 地址然后调用 Serve 方法处理到来的连接。
// 接受的连接被配置为开启 TCP 保活。
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

net.Listen 源码如下:

// Listen("tcp", ":8080")
func Listen(network, address string) (Listener, error) {
	var lc ListenConfig
	return lc.Listen(context.Background(), network, address)
}

其声明了一个 ListenConfig 后 调用其 Listen 方法:

// Listen announces on the local network address.、
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
	addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil)	// 根据动作、协议、地址等解析返回 addrList 结构
	if err != nil {
		return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err}
	}
	sl := &sysListener{
		ListenConfig: *lc,
		network:      network,
		address:      address,
	}
	var l Listener
	la := addrs.first(isIPv4)	// 找到第一个包含 IPv4 地址的 Addr
	switch la := la.(type) {
	case *TCPAddr:
		l, err = sl.listenTCP(ctx, la)	// 监听 TCP
	case *UnixAddr:
		l, err = sl.listenUnix(ctx, la)
	default:
		return nil, &OpError{Op: "listen", Net: sl.network, Source: nil, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: address}}
	}
	if err != nil {
		return nil, &OpError{Op: "listen", Net: sl.network, Source: nil, Addr: la, Err: err} // l is non-nil interface containing nil pointer
	}
	return l, nil
}

上述方法利用监听配置、网络协议类型、地址声明了一个 sysListener,然后调用了其 listenTCP 方法得到 Listener

// 调用了 internetSocket 创建套接字,将其封装为一个 TCPListener 后返回。
func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
	fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
	if err != nil {
		return nil, err
	}
	return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

该方法调用了 internetSocket 函数创建一个套接字,sotype 即 socket 的类型,其传入的 sotype 参数为 syscall.SOCK_STREAMSOCK_STREAM 提供面向连接的稳定数据传输,即TCP协议。internetSocket 源码如下:

// internetSocket 处理了一些特殊情况然后调用了 socket 创建套接字并返回之。
func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
	// 如果是通配符地址
	if (runtime.GOOS == "aix" || runtime.GOOS == "windows" || runtime.GOOS == "openbsd") && mode == "dial" && raddr.isWildcard() {
		// 将零地址映射为本地系统地址(127.0.0.1 或 ::1)
		raddr = raddr.toLocal(net)
	}
	family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode)	// 返回 syscall.AF_INET6,即 TCP/IP – IPv6 协议簇
	return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn)
}

socket 源码如下:

// socket 函数返回一个网络文件描述符,该描述符可以使用网络轮询器 poller 进行异步 I/O
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
	s, err := sysSocket(family, sotype, proto)	// 返回 s 即描述符的编号
	...
}

socket 调用了 sysSocket 获得了套接字的描述符:

// 包装了 socket 系统调用,并将返回的文件描述符标记为非阻塞的,并在 exec 系统调用时将其关闭。
func sysSocket(family, sotype, proto int) (int, error) {
	syscall.ForkLock.RLock()
	s, err := socketFunc(family, sotype, proto)	// 调用 socketFunc 取得了描述符
	if err == nil {
		syscall.CloseOnExec(s)
	}
	syscall.ForkLock.RUnlock()
	if err != nil {
		return -1, os.NewSyscallError("socket", err)
	}
	if err = syscall.SetNonblock(s, true); err != nil {
		poll.CloseFunc(s)
		return -1, os.NewSyscallError("setnonblock", err)
	}
	return s, nil
}

sysSocket 调用 socketFunc 取得了描述符。socketFunc 其实是 syscall.Socket

var (
	...
	// socket 系统调用的占位符
	socketFunc        func(int, int, int) (int, error)  = syscall.Socket
	...
)

其源码如下:

// 检查是否创建 AF_INET6 协议簇但套接字禁用了 IPv6.
func Socket(domain, typ, proto int) (fd int, err error) {
	if domain == AF_INET6 && SocketDisableIPv6 {
		return -1, EAFNOSUPPORT
	}
	// 调用 socket 创建套接字
	fd, err = socket(domain, typ, proto)
	return
}

socket 函数源码如下,该函数真正进行了 socket 系统调用:

func socket(domain int, typ int, proto int) (fd int, err error) {
	r0, _, e1 := rawSyscall(abi.FuncPCABI0(libc_socket_trampoline), uintptr(domain), uintptr(typ), uintptr(proto))	// libc_socket_trampoline 是真正的 socket 系统调用
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}

socket 函数一路将描述符返回,直至 sock_posix.go 下的 socket 函数:

// socket 函数返回一个网络文件描述符,该描述符可以使用网络轮询器 poller 进行异步 I/O
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
	s, err := sysSocket(family, sotype, proto)	// 返回 s 即描述符的编号
	if err != nil {
		return nil, err
	}
	// 设置默认套接字选项。
	if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
		// 如果设置失败,则调用 close 系统调用关闭文件描述符。
		poll.CloseFunc(s)
		return nil, err
	}
	// 利用套接字编号、协议簇、协议、网络类型创建一个描述符。
	// newFD(s, syscall.AF_INET6, syscall.SOCK_STREAM, "tcp")
	if fd, err = newFD(s, family, sotype, net); err != nil {
		// 如果创建失败,则调用 close 系统调用关闭文件描述符。
		poll.CloseFunc(s)
		return nil, err
	}

	// laddr 和 raddr 意即 local address 和 remote address
	// 当本地地址不为空且远程地址为空时
	if laddr != nil && raddr == nil {
		switch sotype {
		case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
			// listenStream 对描述符进行了设置、将描述符与监听地址进行了绑定、开启对地址的监听。
			// 另外,backlog 是保存客户端请求的队列长度。
			if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
				fd.Close()
				return nil, err
			}
			return fd, nil
		case syscall.SOCK_DGRAM:
			if err := fd.listenDatagram(laddr, ctrlFn); err != nil {
				fd.Close()
				return nil, err
			}
			return fd, nil
		}
	}
	if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
		fd.Close()
		return nil, err
	}
	return fd, nil
}

socket 函数利用调用 sysSocket 获取的文件描述符创建了一个网络文件句柄 fd,之后调用该网络文件句柄的 listenStream 方法开始监听到来的数据流:

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
	var err error
	// 为监听套接字设置默认选项
	if err = setDefaultListenerSockopts(fd.pfd.Sysfd); err != nil {
		return err
	}
	var lsa syscall.Sockaddr
	if lsa, err = laddr.sockaddr(fd.family); err != nil {
		return err
	}
	// 如果传入了 ctrlFn,则先调用一下
	if ctrlFn != nil {
		c, err := newRawConn(fd)
		if err != nil {
			return err
		}
		if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
			return err
		}
	}
	// 将句柄与地址绑定,即 bind 系统调用
	if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
		return os.NewSyscallError("bind", err)
	}
	// 开启监听,listenFunc 实际是 syscall.Listen,即 listen 系统调用
	if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
		return os.NewSyscallError("listen", err)
	}
	// 对句柄进行初始化
	if err = fd.init(); err != nil {
		return err
	}
	// Getsockname 通过 getsockname 系统调用获取一个套接字的名字
	// 其利用套接字描述符获取该套接字的名字
	// 对于 TCP 连接的情况,如果不进行 bind 指定 IP 和端口,那么调用 connect 连接成功后,
	// 使用 getsockname 可以正确获得当前正在通信的 socket 的 IP 和端口地址。
	// 这里通过 Getsockname 和 setAddr 方法设置了 fd 的地址。
	lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
	fd.setAddr(fd.addrFunc()(lsa), nil)
	return nil
}

至此,ListenAndServe 完成了套接字的创建,开始对指定的 TCP 地址进行监听。

连接的接收和请求的映射

回到 ListenAndServe 方法:

// ListenAndServe 方法监听 TCP 地址然后调用 Serve 方法处理到来的连接。
// 接受的连接被配置为开启 TCP keep-alive。
func (srv *Server) ListenAndServe() error {
	...
	ln, err := net.Listen("tcp", addr)	// 由上文可知这里开启了对 TCP 地址的监听,该函数返回了一个用于监听面向流协议的 Listener.
	if err != nil {
		return err
	}
	return srv.Serve(ln)	// Serve 方法接收到来的连接并处理请求
}

在开启了对 TCP 地址的绑定和监听后,调用了 ServerServe 方法,其源码如下:

// Serve 方法从 listener 中接收到来的连接,并为每个连接创建一个 goroutine 处理请求
// 负责服务的 goroutine 读取请求并调用 srv.Handler 回复请求。
func (srv *Server) Serve(l net.Listener) error {
	...
	// 从该连接不断地读取请求
	for {
		rw, err := l.Accept()	// 调用 Accept 方法接收到来的连接,返回的 rw 是 Conn 对象,代表了一个面向流的连接
		if err != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := err.(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", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := srv.newConn(rw)	// 
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)	// 开启新的 goroutine 处理请求
	}
}

ListenerAccept 方法监听并返回了到来的连接。之后调用 src.newConn 封装为 conn (一个 conn 代表服务器侧的连接),设置一下连接的状态、运行一下钩子函数,之后新开启了一个 goroutine 运行该连接的 serve 方法,并将相应的 context 传入。

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()	// 远程地址的 IP 及端口号
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
	defer func() {
		// 从 panic 中恢复,即使具体某个 handler 处理请求时发生 panic,也不会导致整个服务器退出
		if err := recover(); err != nil && err != ErrAbortHandler {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		// 如果连接没有被劫持,则关闭之,并将状态设置为关闭,运行一下钩子函数。
		if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed, runHooks)
		}
	}()

	if tlsConn, ok := c.rwc.(*tls.Conn); ok {
		...
	}

	// HTTP/1.x from here on.
	// 创建了一个子 context
	ctx, cancelCtx := context.WithCancel(ctx)
	c.cancelCtx = cancelCtx
	defer cancelCtx()	// 方法返回前取消任务

	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

	for {
		w, err := c.readRequest(ctx)	// 从连接中读取下一个请求
		if c.r.remain != c.server.initialReadLimitSize() {
			// 将状态设置为活跃。
			c.setState(c.rwc, StateActive, runHooks)
		}
		if err != nil {
			// 错误处理,如请求太大、请求使用了不支持的编码类型等。
			...
		}

		// Expect 100 Continue support
		req := w.req
		if req.expectsContinue() {
			if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
				// 包装请求体的 reader: req.Body
				// 将请求体 req.Body 传给 expectContinueReader,可以让 expectContinueReader 写出 100 Cotinue
				req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
				// 指示可以将 100 Continue 写出。
				// 实际 100 Continue 由 expectContinueReader 写出
				w.canWriteContinue.setTrue()
			}
		} else if req.Header.get("Expect") != "" {
			w.sendExpectationFailed()
			return
		}

		c.curReq.Store(w)

		if requestBodyRemains(req.Body) {
			registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
		} else {
			w.conn.r.startBackgroundRead()
		}
		
		// 尽管 HTTP 管线可以同时处理多个请求,这里并没有实现此功能,而是就在此 goroutine 运行了请求处理器。
		// ServeHTTP 完成了地址到处理器的映射,并调用处理器进行处理。
		serverHandler{c.server}.ServeHTTP(w, w.req)
		// 后续进行了一些收尾工作
		...
	}
}

ServeHTTP 完成了地址到处理器的映射,并调用处理器进行处理。其实现如下:

// serverHandler 找到映射请求的 handler 并将请求转发给它。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler	// 该 Handler 负责映射请求。
	// 如果没有设置 handler,则用默认的路由规则。
	if handler == nil {
		handler = DefaultServeMux
	}
	// 如果要处理的是获取选项的请求,则用 globalOptionsHandler 处理请求。
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	
	...

	handler.ServeHTTP(rw, req)	// 利用该 handler 映射请求
}

如果在 main 函数中调用 ListenAndServe 时没有指定路径映射的 handler,则默认使用 DefaultServeMux,其 ServeHTTP 方法如下:

// ServeHTTP 将请求分发到跟请求 URL 最为匹配的模式的处理器
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	// 如果 URI 为 '*',则直接关闭连接并返回。
	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)	// 处理请求
}

我们看 DefaultServeMuxHandler 方法是如何分发请求的:

// Handler 返回给出路径的处理器,它参考了请求的方法、请求主机名、URL。
// 如果 path 不是在标准形式下,返回的将是一个内部生成的 handler,
// 该 handler 将请求重定向到标准形式的路径下。
// 如果主机名包含一个端口号,他在匹配 handler 时将被忽略。
// 如果没有符合请求路径的 handler,将返回一个 `page not found' handler。
// 该 Handler 方法主要对请求路径进行标准化,随后调用其 handler 方法进行映射。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	// CONNECT requests are not canonicalized.
	if r.Method == "CONNECT" {
		// If r.URL.Path is /tree and its handler is not registered,
		// the /tree -> /tree/ redirect applies to CONNECT requests
		// but the path canonicalization does not.
		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 := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	// 如果路径为 /tree 但没有相应的 handler,重定向至 /tree/.
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
		return RedirectHandler(u.String(), StatusMovedPermanently), pattern
	}

	return mux.handler(host, r.URL.Path)	// 匹配
}
// 该方法返回了给定路径请求的处理器。
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// 特定于主机的模式优先于通用模式
	// 本地测试下 host 为 localhost
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}

match 真正包含了路径的匹配规则:

// 从保存 handler 的哈希表中找出一个符合该路径的 handler。
// 最具体的模式(最长)将被返回。
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// 先查找完全符合的 handler。
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// 查找最长的匹配路径。mux.es 中包含了所有以 '/' 结尾的模式。
	// 比如在注册时加入 '/hello/' 和 '/hi/',
	// 则 mux.es 中将保存为 ['/hello/', '/hi/']。
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

至此,可以知道默认的路由方法为:

  • 如果路径为 /tree 但没有相应的 handler,重定向至 /tree/
  • 如果能精确匹配,则利用此 handler 处理;
  • 如果不能精确匹配,则匹配以 '/' 结尾的最长路径。

验证 DefaultServeMux 路由规则

建立如下路径处理器:

func main() {
	http.Handle("/greetings/", helloHandler("greetings"))
	http.Handle("/greetings/hello", helloHandler("hello"))
	http.Handle("/greetings/hi", helloHandler("hi"))
	http.Handle("/farewell/bye", helloHandler("bye"))
	http.Handle("/farewell/good_night", helloHandler("good night"))
	http.Handle("/farewell/", helloHandler("farewell"))
	http.ListenAndServe(":8080", nil)
}

type helloHandler string

func (h helloHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	writer.Write([]byte(h))
}
  • 如果路径为 /tree 但没有相应的 handler,重定向至 /tree/
  • 当访问 /greetings//greetings/hello 时,能返回相应的字符串 "greetings" "hello"
  • 当访问 /greetings/bye 时,由于不能精确匹配该路径,DefaultServeMux 将调用 /greetings/ 处理请求;
  • 当访问 /hello 时,由于不能精确匹配该路径,DefaultServeMux 将调用 ’/‘ 处理请求,由于没有注册 '/' 下的处理器,返回 404 page not found
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-02-28 15:59:55  更:2022-02-28 16:01:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 7:52:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码