关于response
在接收和解释请求消息后,服务器会返回一个 HTTP 响应消息,与 HTTP 请求类似,HTTP 响应也是由三个部分组成,分别是:状态行、消息报头和响应正文
代码解析
Response 结构体
Response表示来自HTTP请求的响应。 一旦收到响应头,客户机和传输将从服务器返回响应。 响应主体在读取主体字段时按需流式传输
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
// Header将头键映射到值。
//如果响应有多个具有相同密钥的头,则可以使用逗号限制将它们串联起来。
//当此结构中的其他字段(如ContentLength、transferncode、Trailer)复制头值时,字段值是权威的。
Header Header
// Body代表响应体。
//响应主体在读取主体字段时按需流式传输。
//如果网络连接失败或服务器终止响应,Body.Read调用返回错误,http客户机和传输保证Body始终为非nil, 关闭响应体时调用者的责任
// 如果响应体未读到完成并关闭,则默认HTTP客户端的传输可能不会重用HTTP/1.x“keep alive”TCP连接
// 如果服务器用“chunked”传输编码答复,则响应体将自动取消dechunked
Body io.ReadCloser
// ContentLength记录关联内容的长度。值>=0表示可以从Body读取给定的字节数。
ContentLength int64
// 包含从最外层到最内层的传输编码。值为nil,表示使用“identity”编码。
TransferEncoding []string
//Close记录响应头是否指示在读取Body之后关闭连接(是为客户提供建议)
Close bool
// 报告响应是否已压缩发送,但已由http包解压缩
// 如果为true,则从Body读取生成未压缩的内容,而不是从服务器实际设置的压缩内容,ContentLength设置为-1,并且从responseHeader中删除“content Length”和“content Encoding”字段
// 一般是压缩方式,利用gzip压缩文档能够显著地减少HTML文档的下载时间。
Uncompressed bool
// 将Trailer 键映射到与标头相同格式的值,初始是nil,服务器的Trailer头值中,指定的每个键对应一个值
Trailer Header
// Request 请求是为获取此响应而发送的请求。请求的主体为零(已被消耗)这仅为客户端请求填充
Request *Request
// TLS包含有关接收响应的TLS连接的信息
TLS *tls.ConnectionState
}
将响应对象 写入(入参)写入器
func (r *Response) Write(w io.Writer) error {
// 状态行
text := r.Status
if text == "" {
var ok bool
text, ok = statusText[r.StatusCode]
if !ok {
text = "status code " + strconv.Itoa(r.StatusCode)
}
} else {
// 为了减少stutter, 如果用户设置响应码为2000 ok, 状态码为200
text = strings.TrimPrefix(text, strconv.Itoa(r.StatusCode)+" ")
}
if _, err := fmt.Fprintf(w, "HTTP/%d.%d %03d %s\r\n", r.ProtoMajor, r.ProtoMinor, r.StatusCode, text); err != nil {
return err
}
// 克隆r,这样我们可以根据需要修改r1
r1 := new(Response)
*r1 = *r
if r1.ContentLength == 0 && r1.Body != nil {
// Is it actually 0 length? Or just unknown?
var buf [1]byte
n, err := r1.Body.Read(buf[:])
if err != nil && err != io.EOF {
return err
}
if n == 0 {
// 将其重置为已知的零读取器,以防重复读取底层读取器
r1.Body = NoBody
} else {
r1.ContentLength = -1
r1.Body = struct {
io.Reader
io.Closer
}{
io.MultiReader(bytes.NewReader(buf[:1]), r.Body),
r.Body,
}
}
}
//如果我们发送的是一个没有内容长度的non-chunked HTTP/1.1响应,唯一的方法就是使用旧的HTTP/1.0方式,通过在连接关闭时注意EOF,所以我们需要设置close
if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) && !r1.Uncompressed {
r1.Close = true
}
// 进程主体,内容长度,关闭,Trailer
tw, err := newTransferWriter(r1)
if err != nil {
return err
}
err = tw.writeHeader(w, nil)
if err != nil {
return err
}
// 响应头的剩余部分
err = r.Header.WriteSubset(w, respExcludeHeader)
if err != nil {
return err
}
// Cordon RealTrayRead可能已经发送了POST/PUT请求,即使是零长度
contentLengthAlreadySent := tw.shouldSendContentLength()
if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) && !contentLengthAlreadySent && bodyAllowedForStatus(r.StatusCode) {
if _, err := io.WriteString(w, "Content-Length: 0\r\n"); err != nil {
return err
}
}
// 响应头的尾部
if _, err := io.WriteString(w, "\r\n"); err != nil {
return err
}
// 写响应体和 trailer
err = tw.writeBody(w)
if err != nil {
return err
}
// 成功
return nil
}
func (r *Response) closeBody() {
if r.Body != nil {
r.Body.Close()
}
}
获取Cookies,Location,设置无缓存
// 解析并返回Set-Cookie头中设置的Cookies(所以其实是内部方法readSetCookies的封装)
func (r *Response) Cookies() []*Cookie {
return readSetCookies(r.Header)
}
// 当不存在位置头时,响应的Location方法将返回ErrNoLocation
var ErrNoLocation = errors.New("http: no Location header in response")
// 返回响应的“Location”头的URL(如果存在,用于重定向一个新的位置, 包含新的URL地址)
// 相对重定向是相对于响应的请求来解决的。如果不存在位置标头,则返回ErrNoLocation。
func (r *Response) Location() (*url.URL, error) {
lv := r.Header.Get("Location")
if lv == "" {
return nil, ErrNoLocation
}
if r.Request != nil && r.Request.URL != nil {
return r.Request.URL.Parse(lv)
}
return url.Parse(lv)
}
// 从r读取器 读取并返回HTTP响应,req参数可选地指定与此响应相对应的请求,如果为nil,则假定为GET请求。
// 当完成读取响应体时,客户端必须调用 resp.Body.Close,之后,客户端可检查 resp.Trailer中包含的键值对
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {
tp := textproto.NewReader(r)
resp := &Response{
Request: req,
}
// Parse the first line of the response.
line, err := tp.ReadLine()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return nil, err
}
if i := strings.IndexByte(line, ' '); i == -1 {
return nil, badStringError("malformed HTTP response", line)
} else {
resp.Proto = line[:i]
resp.Status = strings.TrimLeft(line[i+1:], " ")
}
statusCode := resp.Status
if i := strings.IndexByte(resp.Status, ' '); i != -1 {
statusCode = resp.Status[:i]
}
if len(statusCode) != 3 {
return nil, badStringError("malformed HTTP status code", statusCode)
}
resp.StatusCode, err = strconv.Atoi(statusCode)
if err != nil || resp.StatusCode < 0 {
return nil, badStringError("malformed HTTP status code", statusCode)
}
var ok bool
if resp.ProtoMajor, resp.ProtoMinor, ok = ParseHTTPVersion(resp.Proto); !ok {
return nil, badStringError("malformed HTTP version", resp.Proto)
}
// Parse the response headers.
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return nil, err
}
resp.Header = Header(mimeHeader)
fixPragmaCacheControl(resp.Header)
err = readTransfer(resp, r)
if err != nil {
return nil, err
}
return resp, nil
}
// Pragma是为了防止响应页面被缓存,和 Cache-Control:no-cache作用一样的作用。(Pargma只有一个用法, 例如: Pragma: no-cache)
func fixPragmaCacheControl(header Header) {
if hp, ok := header["Pragma"]; ok && len(hp) > 0 && hp[0] == "no-cache" {
// Cache-Control用来指定Response-Request遵循的缓存机制,有三个方式:Public\Private\no-cache
if _, presentcc := header["Cache-Control"]; !presentcc {
header["Cache-Control"] = []string{"no-cache"}
}
}
}
对响应体对象的检查方法
// 报告响应中使用的HTTP协议是否至少是minor(0)
func (r *Response) ProtoAtLeast(major, minor int) bool {
return r.ProtoMajor > major ||
r.ProtoMajor == major && r.ProtoMinor >= minor
}
// 检查响应体 是否支持写入
func (r *Response) bodyIsWritable() bool {
// 通过将 响应体转成 写入器类型判断
_, ok := r.Body.(io.Writer)
return ok
}
// 检查响应 是否是对成功的协议升级的响应
func (r *Response) isProtocolSwitch() bool {
return r.StatusCode == StatusSwitchingProtocols &&
r.Header.Get("Upgrade") != "" &&
httpguts.HeaderValuesContainsToken(r.Header["Connection"], "Upgrade")
}
应用
实际项目中,当服务器监听请求返回响应时,该响应结果是一个结构体,需要对结构体进行处理
func (b *HttpRequest) Bytes() ([]byte, error) {
resp, err := b.getResponse() // 服务器监听请求返回响应
if err != nil {
return nil, err
}
if resp.Body == nil {
return nil, nil
}
defer resp.Body.Close() // 有义务关闭响应体,避免内存泄漏
data, err := ioutil.ReadAll(resp.Body) // 将struct转成[]byte
if err != nil {
return nil, err
}
return data, nil
}
将处理好的[]byte类型响应结果,进行判断分析
resp, err := req.Bytes()
if err != nil {
logger.Info("get resp err", logger.WithError(err))
server.WriteError(c, err)
return
}
logger.Infof("indoor resp : %s", resp)
err = json.Unmarshal(resp, &pmResp)
if err != nil {
logger.Info("Unmarshal DrawbackToOwner resp err", logger.WithError(err))
server.WriteError(c, err)
return
}
if !pmResp.Success {
logger.Infof("resp:%s", pmResp.ErrorMessage)
server.WriteError(c, err)
return
}