我目前正在努力寻找在Golang中发布HTTP帖子时重用连接的方法。
我创建了一个像这样的传输和客户端:
// Create a new transport and HTTP client
tr := &http.Transport{}
client := &http.Client{Transport: tr}
然后我将这个客户端指针传递给goroutine,这样就可以将多个帖子发送到同一个端点,如下所示:
r, err := client.Post(url, "application/json", post)
查看netstat,这似乎导致每个帖子的新连接导致大量并发连接被打开。
在这种情况下重用连接的正确方法是什么?
答案 0 :(得分:81)
在致电Close()
之前,您应该确保阅读,直到回复完成。
e.g。
res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
要确保http.Client
连接重用,请务必执行以下两项操作:
ioutil.ReadAll(resp.Body)
)Body.Close()
答案 1 :(得分:34)
编辑:对于为每个请求构建传输和客户端的人来说,这更像是一个注释。
Edit2:更改了指向godoc的链接。
Transport
是保存连接以供重用的结构;请参阅https://godoc.org/net/http#Transport(“默认情况下,传输缓存连接以供将来重复使用。”)
因此,如果您为每个请求创建一个新的传输,它将每次创建新的连接。在这种情况下,解决方案是在客户端之间共享一个Transport实例。
答案 2 :(得分:34)
如果有人仍在寻找如何做到这一点的答案,那么我就是这样做的。
package main
import (
"bytes"
"io/ioutil"
"log"
"net/http"
"time"
)
var httpClient *http.Client
const (
MaxIdleConnections int = 20
RequestTimeout int = 5
)
func init() {
httpClient = createHTTPClient()
}
// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
client := &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: MaxIdleConnections,
},
Timeout: time.Duration(RequestTimeout) * time.Second,
}
return client
}
func main() {
endPoint := "https://localhost:8080/doSomething"
req, err := http.NewRequest("POST", endPoint, bytes.NewBuffer([]byte("Post this data")))
if err != nil {
log.Fatalf("Error Occured. %+v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := httpClient.Do(req)
if err != nil && response == nil {
log.Fatalf("Error sending request to API endpoint. %+v", err)
}
// Close the connection to reuse it
defer response.Body.Close()
// Let's check if the work actually is done
// We have seen inconsistencies even when we get 200 OK response
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatalf("Couldn't parse response body. %+v", err)
}
log.Println("Response Body:", string(body))
}
去游乐场:http://play.golang.org/p/oliqHLmzSX
总之,我正在创建一个不同的方法来创建HTTP客户端并将其分配给全局变量,然后使用它来发出请求。 注意
defer response.Body.Close()
这将关闭连接并将其设置为可以再次使用。
希望这会对某人有所帮助。
答案 3 :(得分:9)
IIRC,默认客户端 重用连接。你关闭了response吗?
调用者在完成阅读后应该关闭resp.Body。如果resp.Body未关闭,则客户端的基础RoundTripper(通常为Transport)可能无法重新使用到服务器的持久TCP连接以用于后续“保持活动”请求。
答案 4 :(得分:3)
关于身体
// It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.
因此,如果要重用TCP连接,则每次读取完成后必须关闭Body。建议使用函数ReadBody(io.ReadCloser)。
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "https://github.com", nil)
if err != nil {
fmt.Println(err.Error())
return
}
client := &http.Client{}
i := 0
for {
resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return
}
_, _ = readBody(resp.Body)
fmt.Println("done ", i)
time.Sleep(5 * time.Second)
}
}
func readBody(readCloser io.ReadCloser) ([]byte, error) {
defer readCloser.Close()
body, err := ioutil.ReadAll(readCloser)
if err != nil {
return nil, err
}
return body, nil
}
答案 5 :(得分:0)
// avoid creating several instances, should be singleon
OkHttpClient client = new OkHttpClient();
String weburl = bund.getString("url");
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("subscriptionUR", weburl)
.build();
Request request = new Request.Builder()
.url("http://"+weburl+"/API/ReportSubscription/GetAllReportSubscription")
.post(requestBody)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// Do something when request failed
e.printStackTrace();
Log.d(TAG, "Request Failed.");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if(!response.isSuccessful()){
throw new IOException("Error : " + response);
}else {
Log.d(TAG,"Request Successful.");
}
// Read data in the worker thread
final String data = response.body().string();
}
});
的另一种方法是使用单例方法来获取http客户端。通过使用sync。一旦确定,您的所有请求将只使用一个实例。
init()
答案 6 :(得分:0)
这里遗漏的是“ goroutine”。传输具有其自己的连接池,默认情况下,该池中的每个连接都将被重用(如果主体已完全读取并关闭),但是如果有多个goroutine正在发送请求,则将创建新的连接(该池中的所有连接均处于繁忙状态,并将创建新的连接) )。为了解决这个问题,您将需要限制每个主机的最大连接数:Transport.MaxConnsPerHost
(https://golang.org/src/net/http/transport.go#L205)。
可能您还想设置IdleConnTimeout
和/或ResponseHeaderTimeout
。
答案 7 :(得分:0)
https://golang.org/src/net/http/transport.go#L196
您应将MaxConnsPerHost
明确设置为http.Client
。 Transport
确实重用了TCP连接,但是您应该限制MaxConnsPerHost
(默认0表示没有限制)。
func init() {
// singleton http.Client
httpClient = createHTTPClient()
}
// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
client := &http.Client{
Transport: &http.Transport{
MaxConnsPerHost: 1,
// other option field
},
Timeout: time.Duration(RequestTimeout) * time.Second,
}
return client
}
答案 8 :(得分:0)
GO http 调用非常有用的功能,可以保持连接存活并恢复连接。
var (
respReadLimit = int64(4096)
)
// Try to read the response body so we can reuse this connection.
func (c *Client) drainBody(body io.ReadCloser) error {
defer body.Close()
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
if err != nil {
return err
}
return nil
}
答案 9 :(得分:-1)
有两种可能的方法:
使用一个库,该库在内部重用和管理与每个请求关联的文件描述符。 Http Client内部执行相同的操作,但是您将可以控制打开多少个并发连接以及如何管理资源。如果您有兴趣,请查看netpoll实现,该实现在内部使用epoll / kqueue对其进行管理。
最简单的方法是,不必为网络连接建立池,而是为您的goroutine创建工作池。这很容易,并且 更好的解决方案,这不会妨碍您当前的代码库, 并且需要进行一些细微的更改。
让我们假设您在收到请求后需要发出n个POST请求。
您可以使用渠道来实现这一目标。
或者,您可以简单地使用第三方库。
喜欢:
https://github.com/ivpusic/grpool