zoukankan      html  css  js  c++  java
  • golang http server分析(一)

    golang中使用的http协议版本是RFC2616

    对于一个http服务来讲,需要兼容新旧版本的http协议,http1.0/2.0,以及https的支持,http的通信是建立在tcp连接基础上的通信。

    现在协议有了,连接通信也有了,还剩一个问题就是如何处理client request请求,这个问题可以分为路由和具体逻辑实现,下面看看在golang中是如何解决这些问题的。

    路由部分 

    在golang中有个Handler的概念,一个URL对应一个Handler,在Handler中处理request的具体逻辑,对应关系保存在一个map结构中

    type ServeMux struct {
        mu    sync.RWMutex
        m     map[string]muxEntry //key是URL匹配字符串,muxEntry是对应的处理handler
        hosts bool // 路由匹配时,是否包含host
    }

    Handler分为一般类型Handler和特殊类型Handler,特殊类型Handler是指包含特定功能处理的Handler,

    比如redirectHandler用来处理302跳转、NotFoundHandler用来处理404请求等。

    Handler定义如下:

    // Handler类型定义
    type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
    }
    
    //一般Handler是一函数体,实现了Handler接口,通过该函数可以将一个自定义函数转换为Handler
    type HandlerFunc func(ResponseWriter, *Request)
    // 绑定对象就是自定义函数本身,通过在ServerHTTP中调用函数本身,实现了钩子功能。
    // 也就是说,当程序调用Handler.ServerHTTP()方法的时候,实际上是调用的跟Handler绑定的自定义函数
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        // 调用绑定对象函数
        f(w, r)
    }

    明确了Handler的定义,接下来就要看看如何注册Handler了,Handler的注册是通过HandleFunc()函数实现的,在HandleFunc中调用ServerMux的HandleFunc()

    方法将一个自定义的方法转换为一个一般Handler,最后再调用Server.Mux的handle()方法,完成URL与Handler的绑定,下面详细看看handle()的实现,

    func (mux *ServeMux) Handle(pattern string, handler Handler) {
        // 加一个写锁
        mux.mu.Lock()
        defer mux.mu.Unlock()
        // url匹配字符串不能为空
        if pattern == "" {
            panic("http: invalid pattern " + pattern)
        }
        // handler不能为空
        if handler == nil {
            panic("http: nil handler")
        }
        // 对应关系没有被注册过
        if mux.m[pattern].explicit {
            panic("http: multiple registrations for " + pattern)
        }
    
        // 添加到map
        mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
    
        // 判断是否以hosts开头的url
        if pattern[0] != '/' {
            mux.hosts = true
        }
        
        // 如果URL以字符/结尾,则多注册注册一个redirectHandler,访问/tree时重定向到/tree/
        n := len(pattern)
        if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
            path := pattern
            if pattern[0] != '/' {
                path = pattern[strings.Index(pattern, "/"):]
            }
            url := &url.URL{Path: path}
            mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
        }
    }

    redirectHandle定义:

    // redirectHandler是一个结构体,实现了Handler接口
    type redirectHandler struct {
        url  string
        code int
    }
    
    func (rh *redirectHandler) ServeHTTP(w ResponseWriter, r *Request) {
        // 重定向,设置location,status
        Redirect(w, r, rh.url, rh.code)
    }

    通信部分

    http请求过程实质上是一个tcp连接通信,具体通过socket接口编码实现,socket部分另起文章详细说明,这里只做简单介绍,socket操作流程如下:

    在golang中的使用,通过listenAndServer()函数一步完成

    func (srv *Server) ListenAndServe() error {
        addr := srv.Addr
        if addr == "" {
            addr = ":http"
        }
        // 创建socket文件描述符,绑定ip:port,改变socket状态为监听状态
        ln, err := net.Listen("tcp", addr)
        if err != nil {
            return err
        }
        // 启动服务,处理连接请求,tcpKeepAliveListener是一个长连接
         return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) 
    }

     在svr.Server()函数中会循环Accept() tcp 连接请求,每当读取到一个tcp conn就启动一个goroutine去处理连接信息,

     对于http服务来讲,在goroutine中完成了http request的处理流程如下

    readRequest():从tcp conn中读取一个http request,完成了http请求的heand以及body的解析依据http协议对请求信息的校验,详细过程如下

    1. 设置tcp conn读取数据超时时间,设置请求头数据大小限制,过滤http1.0 post请求后多添加的空格

    2. 读取请求信息并格式化

    3. 校验请求头信息是否合法

    4. 封装成一个response返回,下面详细介绍

    获取到请求信息,接下来就该调用URL对应的具体逻辑了,通过Handler.ServerHTTP()完成了对Handler的调用,主要操作如下:

    1. 根据请求信息host,path在serverMux中查找对应的Handler

    2. 如果找不到对应Handler,会返回一个NotFoundHandler

    3. 调用handler.ServerHttp()

    finishRequest():关闭http request请求处理,主要操作如下

    1. 刷掉bufio.writer里的数据

    2. 关闭chunkWriter()写入流

    3. 刷掉conn缓冲流里的数据

    4. 关闭tcp连接

    接下来介绍下http是如何完成数据的读写的,对数据的读写操作本质上都是在对socket fd进行操作,通过connReader结构体包装好了对net.Con的操作,

    这样就可以通过对connReader的操作最终作用到net.Con,而在net.Con中完成了fd的读写操作,golang中io部分很多地方都用来这种思想,通过包装器包装好需要操作的对象,在调用过程中只需要操作包装器就可以了。

    下面看看golang中是如何实现的,拿解析请求函数举例来看看读取数据的过程,先看看函数定义:

    func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error) {
        tp := newTextprotoReader(b)
        req = new(Request)
    
        var s string
        if s, err = tp.ReadLine(); err != nil {
            return nil, err
        }
            ...
    }

    没错请求的处理是通过bufio.Reader.ReadLine()一行一行的从tcp conn中读取出来的,在看看这个函数是怎么被调用的

    c.r = &connReader{r: c.rwc} // connReader就是上面提到的tcp连接包装器
    c.bufr = newBufioReader(c.r) // 定义一个读缓存io流
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) //定义一个写缓存io流
    // 调用解析请求函数
    req, err := readRequest(c.bufr, keepHostHeader)

    接下来看看写操作过程,http中通过response对象将数据写入tcp连接的,并对写入流做了优化操作,对response的使用如下

    w = &response{
        conn:          c, //当前tcp conn
        req:           req, // 当前请求对应的request对象
        reqBody:       req.Body, 
        handlerHeader: make(Header), //初始化header头存储块
        contentLength: -1, // 内容长度
    }
    w.cw.res = w // 第二层缓冲流,通过chunkWriter将内容写入conn缓冲区
    w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize) // 第一层缓冲流,通过buio writer将数据写入chunkWriter

    封装好response对象后,通过serverHandler{c.server}.ServeHTTP(w, w.req)方法将对象传入Handler中,最终装换为一个http.ResponseWriter对象执行,本质上

    都是io.Writer对象

    总结:

    这篇文字主要介绍了http包中的路由和通信部分,由于通信部分涉及到一些socket通信的问题,只是简单的提了一下,后面会专门针对socket总结一篇文章,http包中

    Request和Response没有详细展开介绍,基本上对http协议规范的实现都在这两个里面体现,还有前文提到的http服务需要支持的一些特性也都没有提及到,后面陆续来吧。

  • 相关阅读:
    BZOJ 4032: [HEOI2015]最短不公共子串 (dp*3 + SAM)
    后缀自动机详解!
    BZOJ 3926: [Zjoi2015]诸神眷顾的幻想乡(广义后缀自动机 多串)
    BZOJ 3938 Robot
    [JSOI2008]Blue Mary开公司
    [ZJOI2017]树状数组
    [JSOI2015]非诚勿扰
    [HNOI2011]任务调度
    BZOJ 3680 吊打XXX
    POJ 3318 Matrix Multiplication
  • 原文地址:https://www.cnblogs.com/zongjiang/p/6550319.html
Copyright © 2011-2022 走看看