我已经写了很多关于HTTP的文章。这篇博文只是另一篇文章,它讨论了 Go 处理方式中的另一个有趣概念,HTTP
以及它如何使与 HTTP 相关的东西变得更加有趣。
在这篇文章中,我将介绍Round tripping是什么,它的适用用例和一个显示它的应用程序的小演示。
我想谈的这个概念被称为Round tripping
或godoc
描述它执行单个 HTTP 事务的能力,获得给定 Request 的 Response。基本上,这意味着能够了解发出HTTP
请求和接收响应之间发生的情况。用外行的话来说,它就像中间件,但对于http.Client
. 我这样说是因为往返发生在请求实际发送之前。
尽管可以在该
RoundTrip
方法中执行任何操作(例如 HTTP 处理程序的中间件),但建议您不要检查响应、返回错误(nil 或非 nil)并且不应该执行用户之类的操作身份验证(或 cookie 处理)..
因为http.RoundTripper
是一个接口。要获得此功能,您所要做的就是实施RoundTrip
:
|
这只是与 stdlib 中的其他方法接口保持一致。小而简洁。
用例
-
缓存 http 响应。例如,您的网络应用程序必须连接到 Github 的 API 以获取内容(其中之一是趋势回购)。在现实生活中,这种情况经常发生变化,但我们假设他们每 30 分钟重建一次趋势板,并且您的应用程序拥有大量用户。您显然不想每次都点击 api 来请求趋势排行榜。因为它在30 分钟的窗口中总是相同的,并且考虑到 API 调用是有速率限制的,并且由于您的应用程序的高使用率,您几乎总是达到/超过限制。
一个解决方案是使用
http.RoundTripper
. 您可以http.Client
使用执行以下操作的 RoundTripper 配置您的:-
缓存商店有这个项目吗?
- 不要提出
HTTP
要求。 - 通过将缓存中的数据读取到响应正文中来返回新的响应。
- 不要提出
-
缓存存储没有此项(可能是因为缓存每 31 分钟失效一次)
- 向
HTTP
api 发出请求。 - 缓存从 api 接收到的数据。
- 向
-
您不必为此使用 RoundTripper,因为(在处理程序内)您可以在发出
HTTP
请求之前检查缓存中是否存在某个项目。但是通过 RoundTripper 实现,您可能会正确分配职责[0]
-
根据需要向请求添加适当的(授权)标头...... 一个很容易想到的例子是google/go-github,Github 的 api 的 Golang 客户端。Github 的 api 的某些部分需要对请求进行身份验证,有些则不需要。默认情况下,该库不处理身份验证,它使用默认的 HTTP 客户端,如果您需要能够访问 api 的经过身份验证的部分,您可以携带自己的 HTTP 客户端,例如
oauth2
受保护的端点。那么如何这个问题往返,有这个ghinstallation允许你使用 go-github 验证 Github 应用程序。如果您查看它的代码库,它所做的只是提供一个http.Client
实现http.RoundTripper
. 之后它在RoundTrip
方法。 -
速率限制。这与上面的非常相似,也许您有一个存储桶,用于保存最近建立的连接数。您检查您是否仍处于可接受的 API 状态,并决定是否应该发出请求、停止发出请求或安排它在将来运行。
-
不管你有什么……也许没有。
真实世界的使用
我们将HTTP
通过http.RoundTripper
. 我们将创建一个只响应一个路由的服务器,然后创建一个连接到该服务器的客户端包。客户端将利用它自己的实现,http.Client
因此我们可以提供我们自己的 RoundTripper,因为我们正在尝试缓存响应。
所以这就是它的样子,
- 客户端向服务器发出请求。
- 如果该 url 的响应存在于缓存存储中?
- 不要调用服务器。
- 从商店取货。
- 将其写入响应并直接返回。
- 如果缓存存储中不存在该 url 的响应
- 向服务器发出请求。
- 将响应的主体写入缓存存储。
- 返回响应。
- 如果该 url 的响应存在于缓存存储中?
这已经放在github上了。
|
我们将首先构建服务器,因为它的实现非常简单
|
然后我们将构建客户端包。这是最有趣的部分,虽然它很长(130 多个 LOC),但应该相对容易理解。我强烈建议您前往github存储库。
首先,我们需要一个缓存存储。由于这是一个最小的项目,字典/地图可以帮助我们尽快离开。我们将创建一个http.Transport
实现http.RoundTripper
但也是缓存存储的对象。
但在现实生活中,您会希望将它们彼此分开。
func cacheKey(r *http.Request) string {
return r.URL.String()
}
type cacheTransport struct {
data map[string]string
mu sync.RWMutex
originalTransport http.RoundTripper
}
func (c *cacheTransport) Set(r *http.Request, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[cacheKey(r)] = value
}
func (c *cacheTransport) Get(r *http.Request) (string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if val, ok := c.data[cacheKey(r)]; ok {
return val, nil
}
return "", errors.New("key not found in cache")
}
// Here is the main functionality
func (c *cacheTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// Check if we have the response cached..
// If yes, we don't have to hit the server
// We just return it as is from the cache store.
if val, err := c.Get(r); err == nil {
fmt.Println("Fetching the response from the cache")
return cachedResponse([]byte(val), r)
}
// Ok, we don't have the response cached, the store was probably cleared.
// Make the request to the server.
resp, err := c.originalTransport.RoundTrip(r)
if err != nil {
return nil, err
}
// Get the body of the response so we can save it in the cache for the next request.
buf, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}
// Saving it to the cache store
c.Set(r, string(buf))
fmt.Println("Fetching the data from the real source")
return resp, nil
}
func (c *cacheTransport) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
c.data = make(map[string]string)
return nil
}
func cachedResponse(b []byte, r *http.Request) (*http.Response, error) {
buf := bytes.NewBuffer(b)
return http.ReadResponse(bufio.NewReader(buf), r)
}
然后是我们引导程序的主要功能。我们会设置一个计时器来清除缓存存储,这样我们就可以向服务器发出请求,这使我们能够查看缓存或原始服务器正在为哪些请求提供服务。
func main() {
//client/main/go
cachedTransport := newTransport()
//Create a custom client so we can make use of our RoundTripper
//If you make use of http.Get(), the default http client located at http.DefaultClient is used instead
//Since we have special needs, we have to make use of our own http.RoundTripper implementation
client := &http.Client{
Transport: cachedTransport,
Timeout: time.Second * 5,
}
// Time to clear the cache store so we can make request to the original server rather than fetch from the cache store
// This is to replicate real expiration of data in a cache store
cacheClearTicker := time.NewTicker(time.Second * 5)
//Make a new request every second
//This would help demonstrate if the response is coming from the real server or the cache
reqTicker := time.NewTicker(time.Second * 1)
terminateChannel := make(chan os.Signal, 1)
signal.Notify(terminateChannel, syscall.SIGTERM, syscall.SIGHUP)
req, err := http.NewRequest(http.MethodGet, "http://localhost:8000", strings.NewReader(""))
if err != nil {
panic("Whoops")
}
for {
select {
case <-cacheClearTicker.C:
// Clear the cache so we can hit the original server
cachedTransport.Clear()
case <-terminateChannel:
cacheClearTicker.Stop()
reqTicker.Stop()
return
case <-reqTicker.C:
resp, err := client.Do(req)
if err != nil {
log.Printf("An error occurred.... %v", err)
continue
}
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("An error occurred.... %v", err)
continue
}
fmt.Printf("The body of the response is \"%s\" \n\n", string(buf))
}
}
}
为了测试这一点,我们必须构建两个程序 -client/main.go
和server/main.go
. ./client
使用和在各自的目录中运行它们./server
。你应该得到这样的东西
并观察打印到终端的内容,您会注意到有些地方说“从缓存中获取”,而有些地方说“从服务器中获取”。最有趣的部分是如果您查看 的实现server/main.go
,我们有一个fmt.Println
只有在调用服务器时才会执行,您会注意到只有在客户端打印“从服务器获取”时才会看到它。
另一件需要注意的事情是,无论我们是否访问服务器,响应的主体都会保持不变。