简单服务器 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
类型,实际调用了 MIMEHeader
的 Add
方法:
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 包的服务器实现中,传给 handler
的 ResponseWriter
的实际类型是 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
拷贝到 chunkWriter
的 header
中,然后将 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
中对路径和处理器进行注册,该函数将 pattern
和 handler
传给了 DefaultServeMux
的 Handle
方法。
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_STREAM
,SOCK_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 地址的绑定和监听后,调用了 Server
的 Serve
方法,其源码如下:
// 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 处理请求
}
}
Listener
的 Accept
方法监听并返回了到来的连接。之后调用 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) // 处理请求
}
我们看 DefaultServeMux
的 Handler
方法是如何分发请求的:
// 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
。