go http分析

go标准库剖析1 (transport http请求的承载者)

使用golang net/http库发送http请求, 最后都是调用 transport的 RoundTrip方法,

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

RoundTrip executes a single HTTP transaction, returning the Response for the request req. (RoundTrip 代表一个http事务,给一个请求返回一个响应)

说白了,就是你给它一个request,它给你一个response

下面我们来看一下他的实现,对应源文件

net/http/transport.go , 我感觉这里是http package里面的精髓所在, go里面一个struct就跟一个类一样, transport这个类长这样的

type Transport struct {
	idleMu	 sync.Mutex
	wantIdle   bool // user has requested to close all idle conns
	idleConn   map[connectMethodKey][]*persistConn
	idleConnCh map[connectMethodKey]chan *persistConn
	reqMu	   sync.Mutex
	reqCanceler map[*Request]func()
	altMu	sync.RWMutex
	altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
	//Dial获取一个tcp 连接,也就是net.Conn结构,你就记住可以往里面写request
	//然后从里面搞到response就行了
	Dial func(network, addr string) (net.Conn, error)
}

篇幅所限, https和代理相关的我就忽略了, 两个 map 为 idleConn idleConnCh , idleConn 是保存从 connectMethodKey (代表着不同的协议 不同的host,也就是不同的请求)到 persistConn 的映射, idleConnCh 用来在并发http请求的时候在多个 goroutine 里面相互发送持久连接,也就是说, 这些持久连接是可以重复利用的, 你的http请求用某个 persistConn 用完了,通过这个 channel 发送给其他http请求使用这个 persistConn

然后我们找到 transport 的 RoundTrip 方法

func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
	...
	pconn, err := t.getConn(req, cm)
	if err != nil {
		t.setReqCanceler(req, nil)
		req.closeBody()
		return nil, err
	}
	return pconn.roundTrip(treq)
}

前面对输入的错误处理部分我们忽略, 其实就2步,先获取一个TCP长连接,所谓TCP长连接就是三次握手建立连接后不 close 而是一直保持重复使用(节约环保) 然后调用这个持久连接persistConn 这个struct的roundTrip方法

我们跟踪第一步

func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) {
	if pc := t.getIdleConn(cm); pc != nil {
		// set request canceler to some non-nil function so we
		// can detect whether it was cleared between now and when
		// we enter roundTrip
		t.setReqCanceler(req, func() {})
		return pc, nil
	}
	type dialRes struct {
		pc  *persistConn
		err error
	}
	dialc := make(chan dialRes)
	//定义了一个发送 persistConn的channel
	prePendingDial := prePendingDial
	postPendingDial := postPendingDial
	handlePendingDial := func() {
		if prePendingDial != nil {
			prePendingDial()
		}
		go func() {
			if v := <-dialc; v.err == nil {
				t.putIdleConn(v.pc)
			}
			if postPendingDial != nil {
				postPendingDial()
			}
		}()
	}
	cancelc := make(chan struct{})
	t.setReqCanceler(req, func() { close(cancelc) })
	// 启动了一个goroutine, 这个goroutine 获取里面调用dialConn搞到
	// persistConn, 然后发送到上面建立的channel  dialc里面,	
	go func() {
		pc, err := t.dialConn(cm)
		dialc <- dialRes{pc, err}
	}()
	idleConnCh := t.getIdleConnCh(cm)
	select {
	case v := <-dialc:
		// dialc 我们的 dial 方法先搞到通过 dialc通道发过来了
		return v.pc, v.err
	case pc := <-idleConnCh:
		// 这里代表其他的http请求用完了归还的persistConn通过idleConnCh这个	
		// channel发送来的
		handlePendingDial()
		return pc, nil
	case <-req.Cancel:
		handlePendingDial()
		return nil, errors.New("net/http: request canceled while waiting for connection")
	case <-cancelc:
		handlePendingDial()
		return nil, errors.New("net/http: request canceled while waiting for connection")
	}
}

这里面的代码写的很有讲究 , 上面代码里面我也注释了, 定义了一个发送 persistConn 的channel dialc , 启动了一个 goroutine , 这个 goroutine 获取里面调用 dialConn 搞到 persistConn , 然后发送到 dialc 里面,主协程 goroutine 在 select 里面监听多个 channel ,看看哪个通道里面先发过来 persistConn ,就用哪个,然后 return ,

这里要注意的是 idleConnCh 这个通道里面发送来的是其他的http请求用完了归还的 persistConn , 如果从这个通道里面搞到了, dialc 这个通道也等着发呢,不能浪费,就通过 handlePendingDial 这个方法把 dialc 通道里面的 persistConn 也发到 idleConnCh , 等待后续给其他http请求使用,

还有就是,读者可以翻一下代码,每个新建的persistConn的时候都把tcp连接里地输入流,和输出流用br( br *bufio.Reader ),和bw( bw *bufio.Writer )包装了一下,往bw写就写到tcp输入流里面了,读输出流也是通过br读,并启动了读循环和写循环

pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF})
pconn.bw = bufio.NewWriter(pconn.conn)
go pconn.readLoop()
go pconn.writeLoop()

我们跟踪第二步 pconn.roundTrip 调用这个持久连接persistConn 这个struct的 roundTrip方法。

先瞄一下 persistConn 这个struct

type persistConn struct {

t        *Transport
cacheKey connectMethodKey
conn     net.Conn
tlsState *tls.ConnectionState
br       *bufio.Reader       // 从tcp输出流里面读
sawEOF   bool                // whether we've seen EOF from conn; owned by readLoop
bw       *bufio.Writer       // 写到tcp输入流
 reqch    chan requestAndChan // 主goroutine 往channnel里面写,读循环从     
	// channnel里面接受
writech  chan writeRequest   // 主goroutine 往channnel里面写	         
	// 写循环从channel里面接受
closech  chan struct{}       // 通知关闭tcp连接的channel 
writeErrCh chan error
lk                   sync.Mutex // guards following fields
numExpectedResponses int
closed               bool // whether conn has been closed
broken               bool // an error has happened on this connection; marked broken so it's not reused.
canceled             bool // whether this conn was broken due a CancelRequest
// mutateHeaderFunc is an optional func to modify extra
// headers on each outbound request before it's written. (the
// original Request given to RoundTrip is not modified)
mutateHeaderFunc func(Header)

}

里面是各种channel, 用的是出神入化, 各位要好好理解一下, 我这里画一下

这里有三个goroutine,分别用三个圆圈表示, channel用箭头表示

有两个channel writeRequest 和 requestAndChan

type writeRequest struct {
    req *transportRequest
    ch  chan<- error
}

主goroutine 往writeRequest里面写,写循环从writeRequest里面接受

type responseAndError struct {
	res *Response
	err error
}
type requestAndChan struct {
	req *Request
	ch  chan responseAndError
	addedGzip bool
}

主goroutine 往requestAndChan里面写,读循环从requestAndChan里面接受

注意这里的channel都是双向channel,也就是channel 的struct里面有一个chan类型的字段, 比如 reqch chan requestAndChan 这里的 requestAndChan 里面的 ch chan responseAndError

这个是很牛叉, 主goroutine 通过 reqch 发送requestAndChan 给读循环, 然后读循环搞到response后通过 requestAndChan 里面的通道responseAndError把response返给主goroutine, 所以我画了一个双向箭头

我们研究一下代码,我理解下来其实就是三个goroutine通过channel互相协作的过程主循环


func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
	... 忽略
	// Write the request concurrently with waiting for a response,
	// in case the server decides to reply before reading our full
	// request body.
	writeErrCh := make(chan error, 1)
	pc.writech <- writeRequest{req, writeErrCh}
	//把request发送给写循环
	resc := make(chan responseAndError, 1)
	pc.reqch <- requestAndChan{req.Request, resc, requestedGzip}
	//发送给读循环
	var re responseAndError
	var respHeaderTimer <-chan time.Time
	cancelChan := req.Request.Cancel
WaitResponse:
	for {
		select {
		case err := <-writeErrCh:
			if isNetWriteError(err) {
				//写循环通过这个channel报告错误
				select {
				case re = <-resc:
					pc.close()
					break WaitResponse
				case <-time.After(50 * time.Millisecond):
					// Fall through.
				}
			}
			if err != nil {
				re = responseAndError{nil, err}
				pc.close()
				break WaitResponse
			}
			if d := pc.t.ResponseHeaderTimeout; d > 0 {
				timer := time.NewTimer(d)
				defer timer.Stop() // prevent leaks
				respHeaderTimer = timer.C
			}
		case <-pc.closech:
			// 如果长连接挂了, 这里的channel有数据, 进入这个case, 进行处理
			select {
			case re = <-resc:
				if fn := testHookPersistConnClosedGotRes; fn != nil {
					fn()
				}
			default:
				re = responseAndError{err: errClosed}
				if pc.isCanceled() {
					re = responseAndError{err: errRequestCanceled}
				}
			}
			break WaitResponse
		case <-respHeaderTimer:
			pc.close()
			re = responseAndError{err: errTimeout}
			break WaitResponse
			// 如果timeout,这里的channel有数据, break掉for循环
		case re = <-resc:
			break WaitResponse
		   // 获取到读循环的response, break掉 for循环
		case <-cancelChan:
			pc.t.CancelRequest(req.Request)
			cancelChan = nil
		}
	}
	if re.err != nil {
		pc.t.setReqCanceler(req.Request, nil)
	}
	return re.res, re.err
}

这段代码主要就干了三件事

主goroutine ->requestAndChan -> 读循环goroutine

主goroutine ->writeRequest-> 写循环goroutine

主goroutine 通过select 监听各个channel上的数据, 比如请求取消, timeout,长连接挂了,写流出错,读流出错, 都是其他goroutine 发送过来的, 跟中断一样,然后相应处理,上面也提到了,有些channel是主goroutine通过channel发送给其他goroutine的struct里面包含的channel, 比如 case err := <-writeErrCh: case re = <-resc:

读循环代码


func (pc *persistConn) readLoop() {
	... 忽略
	alive := true
	for alive {
		... 忽略
		rc := <-pc.reqch
		var resp *Response
		if err == nil {
			resp, err = ReadResponse(pc.br, rc.req)
			if err == nil && resp.StatusCode == 100 {
				//100  Continue  初始的请求已经接受,客户应当继续发送请求的其 
				// 余部分
				resp, err = ReadResponse(pc.br, rc.req)
				// 读pc.br(tcp输出流)中的数据,这里的代码在response里面
				//解析statusCode,头字段, 转成标准的内存中的response 类型
				//  http在tcp数据流里面,head和body以 /r/n/r/n分开, 各个头
				// 字段 以/r/n分开
			}
		}
		if resp != nil {
			resp.TLS = pc.tlsState
		}
		...忽略
		//上面处理一些http协议的一些逻辑行为,
		rc.ch <- responseAndError{resp, err} //把读到的response返回给	
											 //主goroutine
		.. 忽略
		//忽略部分, 处理cancel req中断, 发送idleConnCh归还pc(持久连接)到持久连接池中(map)	
	pc.close()
}

无关代码忽略,

这段代码主要干了一件事情

读循环goroutine 通过channel requestAndChan 接受主goroutine发送的request( rc := <-pc.reqch ), 并从tcp输出流中读取response, 然后反序列化到结构体中, 最后通过channel 返给主goroutine ( rc.ch <- responseAndError{resp, err} )

func (pc *persistConn) writeLoop() {
	for {
		select {
		case wr := <-pc.writech:   //接受主goroutine的 request
			if pc.isBroken() {
				wr.ch <- errors.New("http: can't write HTTP request on broken connection")
				continue
			}
			err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra)   //写入tcp输入流
			if err == nil {
				err = pc.bw.Flush()
			}
			if err != nil {
				pc.markBroken()
				wr.req.Request.closeBody()
			}
			pc.writeErrCh <- err 
			wr.ch <- err		 //  出错的时候返给主goroutineto 
		case <-pc.closech:
			return
		}
	}
}

写循环就更简单了, select channel中主gouroutine的request,然后写入tcp输入流如果出错了,channel 通知调用者

整体看下来, 过程都很简单,但是代码中有很多值得我们学习的地方,比如高并发请求如何复用tcp连接, 这里是连接池的做法, 如果使用多个 goroutine相互协作完成一个http请求,

出现错误的时候如何通知调用者中断错误, 代码风格也有很多可以借鉴的地方

我打算写一个系列, 全面剖析go标准库里面的精彩之处, 分享给大家



golang中http协议实现

写了一个爬虫,发现出现了socket泄露的情况。百度了一下发现是缺少了Response.Body.Close(),所以导致连接没有被正常的关闭。也没有被gc回收。下面是文档中的说明

Callers should close resp.Body when done reading from it. If resp.Bodyis not closed, the Client's underlying RoundTripper (typically Transport)may not be able to re-use a persistent TCP connection to the server for asubsequent "keep-alive" request.

解决问题很简单,不过引起了我想看看源码中简单的HTTP请求是如何实现的欲望。

入口函数send函数Transport.RoundTrip函数Transport.altProtoTransport.connectMethodTransport.getConn函数Transport.getIdleConn函数Transport.dialConn函数persistConn结构体persistConn.roundTrip函数Transport结构体中空闲连接Transport.dial函数persistConn.readLoop函数Do函数(包括Post,Get)

首先我们用NewRequest构建了一个Request,里面包含了我们请求的url,如果是post请求还会包含请求的body,随后会触发一个doFollowingRedirects函数,但是这里我们为了简化就不展开,直接看没有重定向的情况,也就是通过Client.send函数继续向下传递这个Request

send函数

Client.send函数是对send函数的一个包装,目的是提取中Client cookie Jar 中的cookie放入Request中,以及将Response中返回的cookie 装进Client的cookie Jar。

func send(ireq *Request, rt RoundTripper, deadline time.Time) (*Response, error)

当Client.send调用send的时候会将Transport作为rt参数传入进去,如果没有的话则会用Transport.go里面默认的DefaultTransport.

随后send做了一些微小的工作,检测不完整的Request,setRequestCancel(如果设置了超时时间Timeout则这个函数会生效,第一次读的时候会停止这个Timeout的计时,如果此时Request已经被Cancel了,那么返回一个error)。随后调用rt的RoundTrip函数来获得Response.

Transport.RoundTrip函数

首先检测一下Request的信息完整性,然后看一下altProto里面有没有符合Scheme的RoundTrip实现。随后进入for循环,构建一个connectMethod类型变量,随后通过Transport.getConn来拿到一个TCP连接,再通过调用persistConn.roundTrip来把Request写入TCP中,完成发送请求。如果发送失败,则调用checkTransportResend来尝试重新发送这个Request.

Transport.altProto

最开始我也没有看懂这是在干嘛,后来找到了一个RegisterProtocol函数,才看明白这是在干什么。Transport作为一个可以复用的结构体实际上可以处理不同协议的请求,那么不同协议的请求就要有不同的实现,诸如ftp,file等。如果出现了这种情况,我们就可以通过RegisterProtocol来注册一些针对不同协议的实现,从而当Transport发送Request之前就可以通过map来确定到底要使用哪个RoundTrip。

Transport.connectMethod+

结构体中包括了代理地址,协议(HTTP or HTTPS),以及目的地址。需要注意的是,connectMethod类型是很关键的,它不仅是Transport中一些map的键值,也是很多函数的参数。与其相似的结构体connectMethodKey中包含了和它一样的内容,只不过结构体内变量的类型不同(connectMethodKey中的proxy是string,而connectMethod中的proxy是*url.URL)

Transport.getConn函数

首先通过getIdleConn函数来获取可用的空闲连接,如果有的话,直接返回。如果没有的话,用go(异步)的方式创建一个dialConn,然后通过channel来将其送回getConn函数中。而在getConn中则是用select阻塞,等待返回。整个函数中比较复杂的机制在于情况的判定,譬如请求超时了connection仍然没有返回,这个时候函数会调用handlePendingDial对connection进行处理,放入idle队列或者将其关闭。又或者是当我们请求的connection没有返回而此时出现了一个空闲的connection,调用handlePendingDial等待我们申请的那个connection,将这个空闲的返回。

Transport.getIdleConn函数

关于空闲连接的在Transport中的两个map,搜索idleConn,如果存在多个则返回第一个,没有则返回nil

Transport.dialConn函数

首先创建一个persistConn类型的变量,然后检测Scheme,如果是TLS,HTTPS或者是使用了代理,那么通过DialTLS函数来创建Conn,在这里我们不解释这个过程。如果是普通的HTTP,则通过Transport.dial来获得这个Conn.我们只看HTTP的处理过程,发现直接跳过了函数里面的80行+.随后创建了persistConn的读写缓冲区放入结构体中。以异步方式打开persistConn的读写函数(readLoop和writeLoop)

persistConn

//首先调用replaceReqCanceler来探测Request是否已经触发了删除行为,如果是,就把persistConn放入putOrCloseIdleConn中处理。实际上,go在实现HTTP请求的时候是有一个默认的Header,而在Request里面也实现了一个extraHeaders的方法。也就是说,在这一步的时候HTTP Header才会真正的被完善。包括Accept-Encoding(gzip),Range,Connection(close).随后向writech里面写入Request,在persistConn结构体中已经讲过,writech的接收者是writeloop,writeloop接收到了之后就会将其写入缓冲区并调用Flush,将err通过channel返回。接下来roundTrip向reqch中写入requestAndChan,reqch的接受者是readloop,接下来函数select挂起几个管道,用来监听一些写入错误,服务超时,连接关闭(或被删除),以及readloop传送回来的response.检查返回值没有问题之后将response返回。

 
Transport结构体中空闲连接部分idleConn map[connectMethodKey][]*persistConnidleConnCh map[connectMethodKey]chan *persistConn

第一个idleConn是以MethodKey作为键值的,为一个persistConn切片建立索引,可以想象的是倘若我们设置最大空闲连接为5(perhost),那么我们可以通过MethodKey获得的最大空闲连接应该就是5个。idleConnCh是对传送persistConn的管道建立索引,每次有人等待连接的时候都会建立一个这样管道。调用tryPutIdleConn的时候会尝试着将已经收到的空闲连接放入管道内,如果放入成功则返回,放入失败则在idleConnCh删除这个索引。然后将其放入idleConn中。

Transport.dial函数

dial函数是调用的Transport结构体中的Dial func(network, addr string) (net.Conn, error).如果你没有创建这个函数的话,默认的就是net.Dial函数。也就是调用底层函数了。

persistConn.readLoop函数

首先用defer注册一个close函数,用来关闭conn以及关闭persistConn中的closech以通知conn被关闭。然后进入循环,首先用Peek(1)来探测是否发生了IO错误。在persistConn.reqch管道中读出requestAndChan类型变量,这个变量是用来匹配Request,并且传入几个管道作为通信。随后调用persistConn.readResponse()来读出Response。后面做一些容错性的检查以及ResponseBody的消息管道,最后用select挂起,等到persistConn的关闭或者Request的cancel,又或者是body的关闭,这个时候才会触发退出循环或者继续循环的指令。那么最初因为没有写Response.Body.Close()所导致的问题就出在这里了。

persistConn.readResponse的实现;ReadResponse的实现;

总结

第一次看源码去解决问题,问题很快就得到解决了。这就正说明了绝大部分问题在源码中都有说明和注释。实话实说,我看的蛮吃力的,自己写了一圈下来发现自己写的内容对读者并不是特别友好,更多的是对源码的一种简化版翻译。水平较低难免出错,期盼如果有大神看到可以指出我的错误,也欢迎问题的交(gao)流(ji)



如何关闭Golang中的HTTP连接 How to Close Golang's HTTP connection


我们的一个服务是用Go写的,在测试的时候发现几个小时之后它就会core掉,而且core的时候没有打出任何堆栈信息,简单分析后发现该服务中的几个HTTP服务的连接数不断增长,而我们的开发机的fd limit只有1024,当该服务所属进程的连接数增长到系统的fd limit的时候,它被操作系统杀掉了。。。

    HTTP Connection中连接未被释放的问题在https://groups.google.com/forum/#!topic/golang-nuts/wliZf2_LUag和https://groups.google.com/forum/#!topic/golang-nuts/tACF6RxZ4GQ都有提到。

    这个服务中,我们会定期向一个HTTP服务器发起POST请求,因为请求非常不频繁,所以想采用短连接的方式去做。请求代码大概长这样:

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func dialTimeout(network, addr string) (net.Conn, error) {
     return  net.DialTimeout(network, addr,  time .Second*POST_REMOTE_TIMEOUT)
}
 
func DoRequest(URL string) xx, error {
        transport := http.Transport{
                 Dial:              dialTimeout,
         }
 
         client := http.Client{
                 Transport: &transport,
         }
 
         content := RequestContent{}
         // fill content here
 
         postStr, err := json.Marshal(content)
         if  err != nil {
                 return  nil, err
         }
 
         resp, err := client.Post(URL,  "application/json" , bytes.NewBuffer(postStr))
         if  err != nil {
                 return  nil, err
         }
 
         defer resp.Body.Close()
         body, err := ioutil.ReadAll(resp.Body)
         if  err != nil {
                 return  nil, err
         }
 
         // receive body, handle it
}

  运行这段代码一段时间后会发现,该进程下面有一堆ESTABLISHED状态的连接(用lsof -p pid查看某进程下的所有fd),因为每次DoRequest函数被调用后,都会新建一个TCP连接,如果对端不先关闭该连接(对端发FIN包)的话,我们这边即便是调用了resp.Body.Close()函数仍然不会改变这些处于ESTABLISHED状态的连接。为什么会这样呢?只有去源代码一探究竟了。

      

      Golang的net包中client.go, transport.go, response.go和request.go这几个文件中实现了HTTP Client。当应用层调用client.Do()函数后,transport层会首先找与该请求相关的已经缓存的连接(这个缓存是一个map,map的key是请求方法、请求地址和proxy地址,value是一个叫persistConn的连接描述结构),如果已经有可以复用的旧连接,就会在这个旧连接上发送和接受该HTTP请求,否则会新建一个TCP连接,然后在这个连接上读写数据。当client接受到整个响应后,如果应用层没有
调用response.Body.Close()函数,刚刚传输数据的persistConn就不会被加入到连接缓存中,这样如果您在下次发起HTTP请求的时候,就会重新建立TCP连接,重新分配persistConn结构,这是不调用response.Body.Close()的一个副作用。
      如果不调用response.Body.Close()还存在一个问题。如果请求完成后,对端关闭了连接(对端的HTTP服务器向我发送了FIN),如果这边不调用response.Body.Close(),那么可以看到与这个请求相关的TCP连接的状态一直处于CLOSE_WAIT状态(还记得么?CLOSE_WAIT是连接的半开半闭状态,它是收到对方的FIN并且我们也发送了ACK,但是本端还没有发送FIN到对端,如果本段不调用close关闭连接,那么连接将一直处于
CLOSE_WAIT状态,不会被系统回收)。

      调用了response.Body.Close()就万无一失了么?上面代码中也调用了body.Close()为什么还会有很多ESTABLISHED状态的连接呢?因为在函数DoRequest()的每次调用中,我们都会新创建transport和client结构,当HTTP请求完成并且接收到响应后,如果对端的HTTP服务器没有关闭连接,那么这个连接会一直处于ESTABLISHED状态。如何解呢?
有两个方法:
      第一个方法是用一个全局的client,函数DoRequest()中每次都只在这个全局client上发送数据。但是如果我就想用短连接呢?用方法二。
      第二个方法是在transport分配时将它的DisableKeepAlives参数置为false,像下面这样:

1
2
3
4
5
6
7
8
9
10
// ...
transport := http.Transport{
         Dial:              dialTimeout,
         DisableKeepAlives:  true ,
}
 
client := http.Client{
         Transport: &transport,
}
// ...

  从transport.go:L908可以看到,当应用层调用resp.Body.Close()时,如果DisableKeepAlives被开启,那么transport自动关闭本端连接。而不将它加入到连接缓存中。

 

    补充一下,在dialTimeout函数中disable tcp连接的keepalive选项是不可行的,它只是设置TCP连接的选项,不会影响到transport中对连接的控制。

1
2
3
4
5
6
7
8
9
10
11
func dialTimeout(network, addr string) (net.Conn, error) {
         conn, err := net.DialTimeout(network, addr,  time .Second*POST_REMOTE_TIMEOUT)
     if  err != nil {
         return  conn, err
     }
 
     tcp_conn := conn.(*net.TCPConn)                                                                                                 
     tcp_conn.SetKeepAlive( false )                                                                                                    
 
     return  tcp_conn, err
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值