小结:

1、HandleFunc 只是一个适配器

go http 服务器编程(1) - 云+社区 - 腾讯云 https://cloud.tencent.com/developer/article/1071189

1. 初识

http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。

golang 的标准库 net/http 提供了 http 编程有关的接口,封装了内部TCP连接和报文解析的复杂琐碎的细节,使用者只需要和 http.requesthttp.ResponseWriter 两个对象交互就行。也就是说,我们只要写一个 handler,请求会通过参数传递进来,而它要做的就是根据请求的数据做处理,把结果写到 Response 中。废话不多说,来看看 hello world 程序有多简单吧!

  1. package main
  2.  
  3. import (
  4. "io"
  5. "net/http"
  6. )
  7.  
  8. type helloHandler struct{}
  9.  
  10. func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  11. w.Write([]byte("Hello, world!"))
  12. }
  13.  
  14. func main() {
  15. http.Handle("/", &helloHandler{})
  16. http.ListenAndServe(":12345", nil)
  17. }

运行 go run hello_server.go,我们的服务器就会监听在本地的 12345 端口,对所有的请求都会返回 hello, world!

正如上面程序展示的那样,我们只要实现的一个 Handler,它的接口原型是(也就是说只要实现了 ServeHTTP 方法的对象都可以作为 Handler):

  1. type Handler interface {
  2. ServeHTTP(ResponseWriter, *Request)
  3. }

然后,注册到对应的路由路径上就 OK 了。

http.HandleFunc接受两个参数:第一个参数是字符串表示的 url 路径,第二个参数是该 url 实际的处理对象。

http.ListenAndServe 监听在某个端口,启动服务,准备接受客户端的请求(第二个参数这里设置为 nil,这里也不要纠结什么意思,后面会有讲解)。每次客户端有请求的时候,把请求封装成 http.Request,调用对应的 handler 的 ServeHTTP 方法,然后把操作后的http.ResponseWriter 解析,返回到客户端。

2. 封装

上面的代码没有什么问题,但是有一个不便:每次写 Handler 的时候,都要定义一个类型,然后编写对应的 ServeHTTP 方法,这个步骤对于所有 Handler 都是一样的。重复的工作总是可以抽象出来,net/http 也正这么做了,它提供了 http.HandleFunc 方法,允许直接把特定类型的函数作为 handler。上面的代码可以改成:

  1. package main
  2.  
  3. import (
  4. "io"
  5. "net/http"
  6. )
  7.  
  8. func helloHandler(w http.ResponseWriter, req *http.Request) {
  9. io.WriteString(w, "hello, world!\n")
  10. }
  11.  
  12. func main() {
  13. http.HandleFunc("/", helloHandler)
  14. http.ListenAndServe(":12345", nil)
  15. }

其实,HandleFunc 只是一个适配器,

  1. // The HandlerFunc type is an adapter to allow the use of
  2. // ordinary functions as HTTP handlers. If f is a function
  3. // with the appropriate signature, HandlerFunc(f) is a
  4. // Handler object that calls f.
  5. type HandlerFunc func(ResponseWriter, *Request)
  6.  
  7. // ServeHTTP calls f(w, r).
  8. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  9. f(w, r)
  10. }

自动给 f 函数添加了 HandlerFunc 这个壳,最终调用的还是 ServerHTTP,只不过会直接使用 f(w, r)。这样封装的好处是:使用者可以专注于业务逻辑的编写,省去了很多重复的代码处理逻辑。如果只是简单的 Handler,会直接使用函数;如果是需要传递更多信息或者有复杂的操作,会使用上部分的方法。

如果需要我们自己写的话,是这样的:

  1. package main
  2.  
  3. import (
  4. "io"
  5. "net/http"
  6. )
  7.  
  8. func helloHandler(w http.ResponseWriter, req *http.Request) {
  9. io.WriteString(w, "hello, world!\n")
  10. }
  11.  
  12. func main() {
  13. // 通过 HandlerFunc 把函数转换成 Handler 接口的实现对象
  14. hh := http.HandlerFunc(helloHandler)
  15. http.Handle("/", hh)
  16. http.ListenAndServe(":12345", nil)
  17. }

3. 默认

大部分的服务器逻辑都需要使用者编写对应的 Handler,不过有些 Handler 使用频繁,因此 net/http 提供了它们的实现。比如负责文件 hosting 的 FileServer、负责 404 的NotFoundHandler 和 负责重定向的RedirectHandler。下面这个简单的例子,把当前目录所有文件 host 到服务端:

  1. package main
  2.  
  3. import (
  4. "net/http"
  5. )
  6.  
  7. func main() {
  8. http.ListenAndServe(":12345", http.FileServer(http.Dir(".")))
  9. }

强大吧!只要一行逻辑代码就能实现一个简单的静态文件服务器。从这里可以看出一件事:http.ListenAndServe 第二个参数就是一个 Handler 函数(请记住这一点,后面有些内容依赖于这个)。

运行这个程序,在浏览器中打开 http://127.0.0.1:12345,可以看到所有的文件,点击对应的文件还能看到它的内容。

其他两个 Handler,这里就不再举例子了,读者可以自行参考文档。

4. 路由

虽然上面的代码已经工作,并且能实现很多功能,但是实际开发中,HTTP 接口会有许多的 URL 和对应的 Handler。这里就要讲 net/http 的另外一个重要的概念:ServeMuxMuxmultiplexor 的缩写,就是多路传输的意思(请求传过来,根据某种判断,分流到后端多个不同的地方)。ServeMux 可以注册多了 URL 和 handler 的对应关系,并自动把请求转发到对应的 handler 进行处理。我们还是来看例子吧:

  1. package main
  2.  
  3. import (
  4. "io"
  5. "net/http"
  6. )
  7.  
  8. func helloHandler(w http.ResponseWriter, r *http.Request) {
  9. io.WriteString(w, "Hello, world!\n")
  10. }
  11.  
  12. func echoHandler(w http.ResponseWriter, r *http.Request) {
  13. io.WriteString(w, r.URL.Path)
  14. }
  15.  
  16. func main() {
  17. mux := http.NewServeMux()
  18. mux.HandleFunc("/hello", helloHandler)
  19. mux.HandleFunc("/", echoHandler)
  20.  
  21. http.ListenAndServe(":12345", mux)
  22. }

这个服务器的功能也很简单:如果在请求的 URL 是 /hello,就返回 hello, world!;否则就返回 URL 的路径,路径是从请求对象 http.Requests 中提取的。

这段代码和之前的代码有两点区别:

  1. 通过 NewServeMux 生成了 ServerMux 结构,URL 和 handler 是通过它注册的
  2. http.ListenAndServe 方法第二个参数变成了上面的 mux 变量

还记得我们之前说过,http.ListenAndServe 第二个参数应该是 Handler 类型的变量吗?这里为什么能传过来 ServeMux?嗯,估计你也猜到啦:ServeMux 也是是 Handler 接口的实现,也就是说它实现了 ServeHTTP 方法,我们来看一下:

  1. type ServeMux struct {
  2. // contains filtered or unexported fields
  3. }
  4.  
  5. func NewServeMux() *ServeMux
  6. func (mux *ServeMux) Handle(pattern string, handler Handler)
  7. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  8. func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
  9. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

哈!果然,这里的方法我们大都很熟悉,除了 Handler() 返回某个请求的 Handler。HandleHandleFunc 这两个方法 net/http 也提供了,后面我们会说明它们之间的关系。而 ServeHTTP 就是 ServeMux 的核心处理逻辑:根据传递过来的 Request,匹配之前注册的 URL 和处理函数,找到最匹配的项,进行处理。可以说ServeMux 是个特殊的 Handler,它负责路由和调用其他后端 Handler 的处理方法。

关于ServeMux ,有几点要说明:

  • URL 分为两种,末尾是 /:表示一个子树,后面可以跟其他子路径; 末尾不是 /,表示一个叶子,固定的路径
  • / 结尾的 URL 可以匹配它的任何子路径,比如 /images 会匹配 /images/cute-cat.jpg
  • 它采用最长匹配原则,如果有多个匹配,一定采用匹配路径最长的那个进行处理
  • 如果没有找到任何匹配项,会返回 404 错误
  • ServeMux 也会识别和处理 ...,正确转换成对应的 URL 地址

你可能会有疑问?我们之间为什么没有使用 ServeMux 就能实现路径功能?那是因为net/http 在后台默认创建使用了 DefaultServeMux

5. 深入

嗯,上面基本覆盖了编写 HTTP 服务端需要的所有内容。这部分就分析一下,它们的源码实现,加深理解,以后遇到疑惑也能通过源码来定位和解决。

Server

首先来看 http.ListenAndServe():

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

这个函数其实也是一层封装,创建了 Server 结构,并调用它的 ListenAndServe 方法,那我们就跟进去看看:

  1. // A Server defines parameters for running an HTTP server.
  2. // The zero value for Server is a valid configuration.
  3. type Server struct {
  4. Addr string // TCP address to listen on, ":http" if empty
  5. Handler Handler // handler to invoke, http.DefaultServeMux if nil
  6. ......
  7. }
  8.  
  9. // ListenAndServe listens on the TCP network address srv.Addr and then
  10. // calls Serve to handle requests on incoming connections. If
  11. // srv.Addr is blank, ":http" is used.
  12. func (srv *Server) ListenAndServe() error {
  13. addr := srv.Addr
  14. if addr == "" {
  15. addr = ":http"
  16. }
  17. ln, err := net.Listen("tcp", addr)
  18. if err != nil {
  19. return err
  20. }
  21. return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
  22. }

Server 保存了运行 HTTP 服务需要的参数,调用 net.Listen 监听在对应的 tcp 端口,tcpKeepAliveListener 设置了 TCP 的 KeepAlive 功能,最后调用 srv.Serve()方法开始真正的循环逻辑。我们再跟进去看看 Serve 方法:

  1. // Serve accepts incoming connections on the Listener l, creating a
  2. // new service goroutine for each. The service goroutines read requests and
  3. // then call srv.Handler to reply to them.
  4. func (srv *Server) Serve(l net.Listener) error {
  5. defer l.Close()
  6. var tempDelay time.Duration // how long to sleep on accept failure
  7. // 循环逻辑,接受请求并处理
  8. for {
  9. // 有新的连接
  10. rw, e := l.Accept()
  11. if e != nil {
  12. if ne, ok := e.(net.Error); ok && ne.Temporary() {
  13. if tempDelay == 0 {
  14. tempDelay = 5 * time.Millisecond
  15. } else {
  16. tempDelay *= 2
  17. }
  18. if max := 1 * time.Second; tempDelay > max {
  19. tempDelay = max
  20. }
  21. srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
  22. time.Sleep(tempDelay)
  23. continue
  24. }
  25. return e
  26. }
  27. tempDelay = 0
  28. // 创建 Conn 连接
  29. c, err := srv.newConn(rw)
  30. if err != nil {
  31. continue
  32. }
  33. c.setState(c.rwc, StateNew) // before Serve can return
  34. // 启动新的 goroutine 进行处理
  35. go c.serve()
  36. }
  37. }

最上面的注释也说明了这个方法的主要功能:

  • 接受 Listener l 传递过来的请求
  • 为每个请求创建 goroutine 进行后台处理
  • goroutine 会读取请求,调用 srv.Handler
  1. func (c *conn) serve() {
  2. origConn := c.rwc // copy it before it's set nil on Close or Hijack
  3.  
  4. ...
  5.  
  6. for {
  7. w, err := c.readRequest()
  8. if c.lr.N != c.server.initialLimitedReaderSize() {
  9. // If we read any bytes off the wire, we're active.
  10. c.setState(c.rwc, StateActive)
  11. }
  12.  
  13. ...
  14.  
  15. // HTTP cannot have multiple simultaneous active requests.[*]
  16. // Until the server replies to this request, it can't read another,
  17. // so we might as well run the handler in this goroutine.
  18. // [*] Not strictly true: HTTP pipelining. We could let them all process
  19. // in parallel even if their responses need to be serialized.
  20. serverHandler{c.server}.ServeHTTP(w, w.req)
  21.  
  22. w.finishRequest()
  23. if w.closeAfterReply {
  24. if w.requestBodyLimitHit {
  25. c.closeWriteAndWait()
  26. }
  27. break
  28. }
  29. c.setState(c.rwc, StateIdle)
  30. }
  31. }

看到上面这段代码 serverHandler{c.server}.ServeHTTP(w, w.req)这一句了吗?它会调用最早传递给 Server 的 Handler 函数:

  1. func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  2. handler := sh.srv.Handler
  3. if handler == nil {
  4. handler = DefaultServeMux
  5. }
  6. if req.RequestURI == "*" && req.Method == "OPTIONS" {
  7. handler = globalOptionsHandler{}
  8. }
  9. handler.ServeHTTP(rw, req)
  10. }

哇!这里看到 DefaultServeMux 了吗?如果没有 handler 为空,就会使用它。handler.ServeHTTP(rw, req),Handler 接口都要实现 ServeHTTP 这个方法,因为这里就要被调用啦。

也就是说,无论如何,最终都会用到 ServeMux,也就是负责 URL 路由的家伙。前面也已经说过,它的 ServeHTTP 方法就是根据请求的路径,把它转交给注册的 handler 进行处理。这次,我们就在源码层面一探究竟。

ServeMux

我们已经知道,ServeMux 会以某种方式保存 URL 和 Handlers 的对应关系,下面我们就从代码层面来解开这个秘密:

  1. type ServeMux struct {
  2. mu sync.RWMutex
  3. m map[string]muxEntry // 存放路由信息的字典!\(^o^)/
  4. hosts bool // whether any patterns contain hostnames
  5. }
  6.  
  7. type muxEntry struct {
  8. explicit bool
  9. h Handler
  10. pattern string
  11. }

没错,数据结构也比较直观,和我们想象的差不多,路由信息保存在字典中,接下来就看看几个重要的操作:路由信息是怎么注册的?ServeHTTP 方法到底是怎么做的?路由查找过程是怎样的?

  1. // Handle registers the handler for the given pattern.
  2. // If a handler already exists for pattern, Handle panics.
  3. func (mux *ServeMux) Handle(pattern string, handler Handler) {
  4. mux.mu.Lock()
  5. defer mux.mu.Unlock()
  6.  
  7. // 边界情况处理
  8. if pattern == "" {
  9. panic("http: invalid pattern " + pattern)
  10. }
  11. if handler == nil {
  12. panic("http: nil handler")
  13. }
  14. if mux.m[pattern].explicit {
  15. panic("http: multiple registrations for " + pattern)
  16. }
  17.  
  18. // 创建 `muxEntry` 并添加到路由字典中
  19. mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
  20.  
  21. if pattern[0] != '/' {
  22. mux.hosts = true
  23. }
  24.  
  25. // 这是一个很有用的小技巧,如果注册了 `/tree/`, `serveMux` 会自动添加一个 `/tree` 的路径并重定向到 `/tree/`。当然这个 `/tree` 路径会被用户显示的路由信息覆盖。
  26. // Helpful behavior:
  27. // If pattern is /tree/, insert an implicit permanent redirect for /tree.
  28. // It can be overridden by an explicit registration.
  29. n := len(pattern)
  30. if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
  31. // If pattern contains a host name, strip it and use remaining
  32. // path for redirect.
  33. path := pattern
  34. if pattern[0] != '/' {
  35. // In pattern, at least the last character is a '/', so
  36. // strings.Index can't be -1.
  37. path = pattern[strings.Index(pattern, "/"):]
  38. }
  39. mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
  40. }
  41. }

路由注册没有什么特殊的地方,很简单,也符合我们的预期,注意最后一段代码对类似/tree URL 重定向的处理。

  1. // ServeHTTP dispatches the request to the handler whose
  2. // pattern most closely matches the request URL.
  3. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
  4. if r.RequestURI == "*" {
  5. if r.ProtoAtLeast(1, 1) {
  6. w.Header().Set("Connection", "close")
  7. }
  8. w.WriteHeader(StatusBadRequest)
  9. return
  10. }
  11. h, _ := mux.Handler(r)
  12. h.ServeHTTP(w, r)
  13. }

好吧,ServeHTTP 也只是通过 mux.Handler(r) 找到请求对应的 handler,调用它的ServeHTTP 方法,代码比较简单我们就显示了,它最终会调用 mux.match() 方法,我们来看一下它的实现:

  1. // Does path match pattern?
  2. func pathMatch(pattern, path string) bool {
  3. if len(pattern) == 0 {
  4. // should not happen
  5. return false
  6. }
  7. n := len(pattern)
  8. if pattern[n-1] != '/' {
  9. return pattern == path
  10. }
  11. // 匹配的逻辑很简单,path 前面的字符和 pattern 一样就是匹配
  12. return len(path) >= n && path[0:n] == pattern
  13. }
  14.  
  15. // Find a handler on a handler map given a path string
  16. // Most-specific (longest) pattern wins
  17. func (mux *ServeMux) match(path string) (h Handler, pattern string) {
  18. var n = 0
  19. for k, v := range mux.m {
  20. if !pathMatch(k, path) {
  21. continue
  22. }
  23. // 最长匹配的逻辑在这里
  24. if h == nil || len(k) > n {
  25. n = len(k)
  26. h = v.h
  27. pattern = v.pattern
  28. }
  29. }
  30. return
  31. }

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2016-11-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于 2018-03-23
 
 

http 服务器编程 适配器的更多相关文章

  1. c++游戏服务器编程学习笔记(一)TCP/IP

    1. c++游戏服务器编程c++运行效率非常高2. TCP传输控制协议IP网际协议Socket 3.Linux 乌班图开源第三方库BOOST 4.80%游戏服务器端用C++工作量最大的地方是具体的游戏 ...

  2. acl 是一个跨平台的网络通信库及服务器编程框架

    acl 工程是一个跨平台(支持LINUX,WIN32,Solaris,MacOS,FreeBSD)的网络通信库及服务器编程框架,同时提供更多的实用功能库.通过该库,用户可以非常容易地编写支持多种模式( ...

  3. 开源软件实践之linux高性能服务器编程框架和选型

    很多人学习编程技术一般都通过一本编程语言的入门书籍,然后尝试做一些例子和小项目.但是这些都不能让我们深入的学习很多的编程技巧和高深技术,当然这个时候很多有经验的学习人员就会告诉大家,找一个好的开源软件 ...

  4. 服务器编程入门(11)TCP并发回射服务器实现 - 单线程select实现

    问题聚焦: 当客户端阻塞于从标准输入接收数据时,将读取不到别的途径发过来的必要信息,如TCP发过来的FIN标志. 因此,进程需要内核一旦发现进程指定的一个或多个IO条件就绪(即输入已准备好被读取,或者 ...

  5. 服务器编程入门(10)TCP回射服务器实现 - 并发

    问题聚焦:     在前面我们大概浏览了一下服务器编程需要掌握的一些知识和技术,以及架构思想.        实践,才是检验真理的唯一标准..从这节起我们将在这些技术的基础上,一步步实现以及完善一个服 ...

  6. 服务器编程入门(7)I/O复用

    问题聚焦:     前篇提到了I/O处理单元的四种I/O模型.     本篇详细介绍实现这些I/O模型所用到的相关技术.     核心思想:I/O复用 使用情景: 客户端程序要同时处理多个socket ...

  7. 服务器编程入门(5)Linux服务器程序规范

    问题聚焦:     除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范.     工欲善其事,必先利其器,这篇主要来探 ...

  8. 服务器编程入门(4)Linux网络编程基础API

      问题聚焦:     这节介绍的不仅是网络编程的几个API     更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系.     这节主要介绍三个方面的内容:套接字( ...

  9. 服务器编程入门(3)TCP协议详解

    问题聚焦:     本节从如下四个方面讨论TCP协议:     TCP头部信息:指定通信的源端端口号.目的端端口号.管理TCP连接,控制两个方向的数据流     TCP状态转移过程:TCP连接的任意一 ...

随机推荐

  1. zend server 和zend studio 最佳实践

    1.zend server 安装好后需要重启下.无论是win还是mac..win不重启组件不能用.mac 不重启守护进程是离线的 2.修改apache配置.的根目录.到zendstudio的工作空间 ...

  2. php代码检查

    最近写php,几个同事都是没写过c的,经常写的变量没有定义,而php没有编译,错误无法发现. 我们现在用的是NetBeans,好在其提供了语法检测,如下图,让编辑器强制显示我错误

  3. PHP——0127加登录页面,加查询,加方法,加提示框

    数据库mydb 表格info,nation,login 效果 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN ...

  4. socket相关函数中断后重试

    慢系统调用accept,read,write被信号中断时应该重试.对于accept,如果errno为ECONNABORTED,也应该重试. connect虽然也会阻塞,但被信号中断时不能立即重试,该s ...

  5. zabbix的agent端的主动模式关键三个参数

    如多主机超过300+和队列内容过多,就采用主动模式. [root@web03 zabbix]# egrep -v "^#|^$" zabbix_agentd.conf PidFil ...

  6. dedecms中如何去掉文章页面的广告

    在arcticle_arcticle.htm页面找到广告调用代码{dede:myad name='myad'/}全部去掉就好了,如果要换成自己的广告,就换广告位标识 myad 就可以了

  7. 第二百五十四节,Bootstrap项目实战--案例

    Bootstrap项目实战--案例 html <!DOCTYPE html> <html lang="zh-cn"> <head> <me ...

  8. 【BZOJ】1692 & 1640: [Usaco2007 Dec]队列变换(后缀数组+贪心)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1692 http://www.lydsy.com/JudgeOnline/problem.php?id ...

  9. poj 1090:The Circumference of the Circle(计算几何,求三角形外心)

    The Circumference of the Circle Time Limit: 2 Seconds      Memory Limit: 65536 KB To calculate the c ...

  10. NPOI例子

    例子链接:http://www.cnblogs.com/atao/tag/NPOI/default.html?page=1