Go Web (Part 3): response

有请求就要有响应,一来一回。这篇文章来看看response的细节。

1. 从处理器Handler开始

对于Request,在前一篇文章中已经知道了整个的流程。Go会构造一个Request结构体,将接收到的请求中的信息解析到这个结构体中。对于一些信息(比如三个Form)需要手动调用一些函数来填充。这样在之后的处理过程中就可以通过这个Request结构来获取具体的请求信息了。

那么对于Response,是不是也是这个过程呢?创建一个Response结构,将数据保存在这个结构中,然后返回给客户端?

不是的,服务端返回响应的时候,使用的是ResponseWriter接口。这一点可以从处理器Handler的定义来看:

ServeHTTP(ResponseWriter, *Request)

两个参数分别对应响应和请求。

net/http包内部使用response结构来处理响应,由于是非导出类型,不能直接操作,所以通过这个接口,来操作具体的响应。

而在通过ResponseWriter接口操作response时,传递的也是指向response结构的指针,这也解释了为什么处理器Handler的两个参数一个是值一个是引用,因为底层实际上都是引用。

2. 使用ResponseWriter进行写入

在具体看看Go是怎么处理response的之前,先看看怎么使用ResponseWriter进行写入。

ResponseWriter接口有三个方法:

  • Write:将指定的数据写入对应的链接作为响应结果;
  • WriteHeader:用来写入一个HTTP响应码(比如200 OK);
  • Header:返回Header,可以用来设置值。

response结构实现了这几个方法。

2.1 Sending Header

可以直接通过WriteHeader()方法来写入HTTP响应码:

w.Header().Set("Server", "A Test Server")
w.WriteHeader(200)

这里首先通过Header()方法返回Header然后设置一个值,接下来通过WriteHeader()方法设置HTTP响应码为200。

通过curl可以看到下面的结果:

HTTP/1.1 200 OK
Server: A Test Server
Date: Thu, 02 Jan 2020 08:18:24 GMT
Content-Length: 0

其实,对于HTTP响应码200来说,不用显示写入,默认就会写入这个响应码。对于WriteHeader方法来说,最常见的应用是写入HTTP的错误码。

比如,下面的代码实现了一次HTTP重定向:

w.Header().Set("Location", "http://google.com")
w.WriteHeader(302)

这时启动浏览器之后就会自动跳到Google的页面了。

2.2 Rendering Plain Text

在之前的文章中,都是通过fmt.Fprintf(w, "hello")这种方式写入响应的。原理都是一样,也可以使用Write()方法来写入结果:

w.Write([]byte("I'm a Test Server"))

不同在于,这里的参数是一个[]byte。结果:

HTTP/1.1 200 OK
Date: Thu, 02 Jan 2020 08:27:49 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

I'm a Test Server

可以看出:

  • 对于200响应码不用手动写入;
  • Content-Length字段标识了响应的长度。

2.3 Rendering JSON

上面写入的响应是一个纯文本。同样可以对响应写入JSON格式的数据:

type User struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    data, _ := json.Marshal(User{"Valineliu", 25})
    w.Header().Set("Content-Type", "application/json")
    w.Write(data)

注意需要通过Header()方法将内容类型设置成application/json,不然就是纯文本了。结果:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 02 Jan 2020 08:32:49 GMT
Content-Length: 29

{"name":"Valineliu","age":25}

当然除了JSON格式外,还可以写入XML等格式。

2.4 Serving a File

还可以发送一个文件:

http.ServeFile(w, r, "luffy.jpg")

通过这个函数就可以了。打开浏览器就可以看到结果。使用curl结果:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 54146
Content-Type: image/jpeg
Last-Modified: Wed, 06 Nov 2019 01:50:35 GMT
Date: Thu, 02 Jan 2020 08:39:48 GMT

3. 看看具体发生了什么

除了上面的那些类型的内容外,还可以返回一个HTML模板,这里就不详细说了。

接下来看看net/http包是怎么通过ResponseHeader操作response的。

response定义如下:

type response struct {
    conn             *conn
    req              *Request // request for this response
    reqBody          io.ReadCloser
    w  *bufio.Writer
    ...
}

这里只是一部分的字段,原来这个response结构竟然还有请求的字段。

当链接收到一个请求后,会调用server()方法进行处理,简略的过程如下:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    ...
    w, err := c.readRequest(ctx)
    ...
    req := w.req
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
    ...
}

先通过readRequest()方法获得response,这个时候里面的Resquest已经构造好了。之后就可以使用ww.req作为参数调用ServeHTTP了。

而在readRequest()方法中:

// Read next request from connection.
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
    ...
    req, err := readRequest(c.bufr, keepHostHeader)
    ...
    w = &response{
        conn:          c,
        cancelCtx:     cancelCtx,
        req:           req,
        reqBody:       req.Body,
        handlerHeader: make(Header),
        contentLength: -1,
        closeNotifyCh: make(chan bool, 1),

        // We populate these ahead of time so we're not
        // reading from req.Header after their Handler starts
        // and maybe mutates it (Issue 14940)
        wants10KeepAlive: req.wantsHttp10KeepAlive(),
        wantsClose:       req.wantsClose(),
    }
    if isH2Upgrade {
        w.closeAfterReply = true
    }
    w.cw.res = w
    w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
    return w, nil
}

可以看到这个response的整个构造过程。这样,后序的处理就可以通过ResponseWriter接口来处理了。

To Be Continued…


  You Will See...