起因
最近写了一个Go的服务端,做图床用,HTTP 服务框架用的大众周知的 Gin ,结果就遇上了这么一个问题,我想再服务端获取请求完整的URL路径以及端口号。
结果就发现了这样一个结果,代码如下所示:
func main() {
// 请求地址: http://121.5.62.93/?name=123 输出结果如下......
g := gin.Default()
g.GET("/", func(c *gin.Context) {
fmt.Println("请求c.Request.URL.Host:", c.Request.URL.Host) // 没有任何输出
fmt.Println("请求c.Request.URL.Hostname():", c.Request.URL.Hostname()) // 没有任何输出
fmt.Println("请求c.Request.URL.Port():", c.Request.URL.Port()) // 没有任何输出
fmt.Println("请求c.Request.URL.String():", c.Request.URL.String()) // 输出: /?name=123
fmt.Println("请求c.Request.URL.Scheme", c.Request.URL.Scheme) // 没有任何输出
fmt.Println("请求c.Request.URL.RequestURI():", c.Request.URL.RequestURI()) // 输出: /?name=123
fmt.Println("请求c.Request.Host:", c.Request.Host) // 输出: 121.5.62.93:9050
fmt.Println("请求c.Request.RequestURI:", c.Request.RequestURI) // 输出: /?name=123
})
panic(g.Run("0.0.0.0:9050"))
}
我还在纳闷呢.....用了Golang 自带的包一试
package main
import (
"fmt"
"net/http"
)
func main() {
// 请求地址: http://121.5.62.93/?name=123 输出结果如下......
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
fmt.Println("请求request.URL.String()", request.URL.String()) // 输出:/?name=123
fmt.Println("请求request.RequestURI", request.RequestURI) // 输出: /?name=123
fmt.Println("请求request.Host", request.Host) // 输出: 121.5.62.93:9050
fmt.Println("请求request.URL.Scheme", request.URL.Scheme) // 没有任何输出
fmt.Println("请求request.URL.Port()", request.URL.Port()) // 没有任何输出
fmt.Println("请求request.URL.Path", request.URL.Path) // 输出:/
})
err := http.ListenAndServe("0.0.0.0:9050", nil)
if err != nil {
panic(err)
}
}
结果已经出来了,你们自己看....我还是没有拿到完整的 URL .......,以至于我以为这是官方的BUG。
直至这个问题出现的3天后,看到了一篇文章的发布,才解决了我心中的疑惑......
以下为转载:
HTTP1.1中为什么无法获取完整的连接
HTTP1.1的Server读取请求并构建Request.URL
对象的逻辑在request.go文件的readRequest
方法中,下面我对其源码做一个简单分析总结。读取请求的第一行,HTTP请求的第一行又称为请求行。读取请求的第一行,HTTP请求的第一行又称为请求行。
- 读取请求的第一行,HTTP请求的第一行又称为请求行。
// First line: GET /index.html HTTP/1.0
var s string
if s, err = tp.ReadLine(); err != nil {
return nil, err
}
- 将请求行的内容分别解析为
req.Method
、req.RequestURI
和req.Proto
。
var ok bool
req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
- 将
req.RequestURI
解析为req.URL
。
rawurl := req.RequestURI
if req.URL, err = url.ParseRequestURI(rawurl); err != nil {
return nil, err
}
注:当请求方法是CONNECT时,上述流程略有变化
通过上面的流程我们知道req.URL
的数据来源为req.RequestURI
,而req.RequestURI
到底是什么让我们继续阅读后文。
请求资源
根据rfc7230中的定义, 请求行分为请求方法、请求资源和HTTP版本,分别对应上述的req.Method
、req.RequestURI
和req.Proto
(request-target在本文均被译作请求资源)。

关于请求方法有哪些想必不用我在这儿科普了吧。至于常用的HTTP版本无非就是HTTP1.1和HTTP2。下面主要介绍请求资源的几种形式。
origin-form
这种形式是请求资源中最常见的形式,其格式定义如下。
origin-form = absolute-path [ "?" query ]
当直接向服务器发起请求时,除开CONNECT和OPTIONS请求,只允许发送path和query作为请求资源。如果请求链接的path为空,则必须发送/
作为请求资源。请求链接中的Host信息以Header头的形式发送。
以http://www.example.org/where?q=now
为例,请求行和Host请求头信息如下
GET /where?q=now HTTP/1.1
Host: www.example.org
absolute-form
这种形式目前仅在向代理发起请求时使用,其格式定义如下。
absolute-form = absolute-URI
根据rfc7230中的定义,目前client仅会向代理发送这种形式的请求资源,但为了将来某个HTTP版本可能会转换为这种形式的请求资源所以server需要支持这种形式的请求资源。这大概就是为什么req.URL
中大部分字段值为空却仍然将URL各部分定义完整的原因。
一个absolute-form
形式的请求行例子如下。
GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
authority-form
authority-form
形式的请求资源仅用于CONNECT
请求中,其格式定义如下。
authority-form = authority
发送CONNECT
请求时,client只能发送URI的authority部分(不包含userinfo和@定界符)作为请求资源。这样讲比较抽象, 我们先来看看http-URI
的定义。

通过上面这张图大概能够猜出来authority
应该是指Host信息。Very Good!你没有猜错!
The origin server for an "http" URI is identified by the authority component, which includes a host identifier and optional TCP port.
上面是rfc7230对于authority的解释。我根据自己的翻译,在这里单方面宣布authority
包括主机标识符和可选的端口信息。一个authority-form
形式的请求行例子如下。
CONNECT www.example.com:80 HTTP/1.1
asterisk-form
asterisk-form
形式的请求资源仅适用于OPTIONS
请求且只能为*
,其格式定义如下。
asterisk-form = "*"
一个asterisk-form
形式的请求行例子如下。
OPTIONS * HTTP/1.1
对上面几种形式的请求资源有所了解后,我们再次回到获取请求的完整URL这一问题本身。以最常用的absolute-form
为例(其他形式的请求资源我们在开发中几乎不用考虑),请求资源中本身就缺少Host
和Scheme
信息,所以一行代码自然无法获取请求的完整URL。难道我们就无法获取到请求的完整URL嘛?当然不是,我们还可以通过以下两种方案得到完整的URL。
方案一:
- 通过req.Host得到Host相关信息。
- 如果req.TLS == nil则为HTTP请求,否则为HTTPS请求。
- 通过步骤1、步骤2并结合请求行信息即可得到完整的URL。
方案二:
在配置文件中配置好服务的Host信息,获取完整请求时只需要读取配置文件并拼接req.RequestURI即可。事实上我采用的就是方案二,因为很多服务都在网关后面。当客户端使用HTTPS请求网关,网关以HTTP请求服务时使用req.TLS == nil判断就不合理了。
HTTP2中为什么无法获取完整的连接
需要注意的是在HTTP2中已经没有请求行的概念了,取而代之的是请求伪标头,这一点我在Go发起HTTP2.0请求流程分析(后篇)——标头压缩这篇文章中提到过。
下图为一次HTTP2请求的部分Header信息。

从图中可以发现,HTTP1.1中的请求行已经没有了。根据rfc7540中的定义,请求的伪标头字段有:method
、:scheme
、:authority
和:path
。
:method
和:scheme
不需要我多说,看英文单词的意思就可以了。
:authority
: 根据前文的解释,其值为主机标识符和可选的端口信息。另外需要注意的是HTTP2中没有Host
请求头。
:path
: 如果是OPTIONS
请求,则其值为*
。其他情况该值为请求URI的path和query,如果path为空则其值为/
。
在对HTTP2请求的伪标头有了一个基本了解后,下面我们来看一下Request.URL
的赋值过程。HTTP2的Server读取请求并构建Request.URL
对象的逻辑在h2_bundle.go文件的(*http2serverConn).newWriterAndRequestNoBody
方法中。
- 如果是
CONNECT
请求通过:authority
构建url_
,否则通过:path
构建url_
。
if rp.method == "CONNECT" {
url_ = &url.URL{Host: rp.authority}
requestURI = rp.authority // mimic HTTP/1 server behavior
} else {
var err error
url_, err = url.ParseRequestURI(rp.path)
if err != nil {
return nil, nil, http2streamError(st.id, http2ErrCodeProtocol)
}
requestURI = rp.path
}
- 将
url_
赋值给req.URL
。
req := &Request{
Method: rp.method,
URL: url_,
RemoteAddr: sc.remoteAddrStr,
Header: rp.header,
RequestURI: requestURI,
Proto: "HTTP/2.0",
ProtoMajor: 2,
ProtoMinor: 0,
TLS: tlsState,
Host: rp.authority,
Body: body,
Trailer: trailer,
}
由于:path
标头的值也不包含Host信息,所以HTTP2的server也无法通过req.URL.String()
得到请求的完整URL。
在这里我们反思一个问题。通过伪标头字段已经能够得到完整的URL,为什么仍然只读取:path
和:authority
中的一个来赋值req.URL
呢?
我在这里猜测可能原因是希望开发者无需关心请求是HTTP1.1还是HTTP2,避免不必要的HTTP版本判断。
关于获取请求完整URL的思考就到这里。最后,衷心希望本文能够对各位读者有一定的帮助。
文章评论(0)