TCP的长连接与短连接

Server和Client建立通讯后,确保连接的及时断开就非常重要。否则,多个客户端长时间占用着连接不关闭,是非常可怕的服务器资源浪费。会使得服务器可服务的客户端数量大幅度减少。

而TCP频繁的建立连接,会有一些问题:

  • 三次握手建立连接、四次握手断开连接都会对性能有损耗;
  • 断开的连接断开不会立刻释放,会等待2MSL的时间,据我观察是1分钟;
  • 大量TIME_WAIT会占用内存,一个连接实测是3.155KB。而且占用太多,有可能会占满端口,一台服务器最多只能有6万多个端口;

因此,针对短链接和长连接,根据业务的需求,配套不同的处理机制。

短连接

一般建立完连接,就立刻传输数据。传输完数据,连接就关闭。服务端根据需要,设定连接的时长。超过时间长度,就算客户端超时。立刻关闭连接。

长连接

建立连接后,传输数据,然后要保持连接,然后再次传输数据。直到连接关闭。

Go里可以利用下面的函数简单地开启长连接

1
func (c *TCPConn) SetKeepAlive(keepalive bool) error

socket读写可以通过 SetDeadline、SetReadDeadline、SetWriteDeadline设置阻塞的时间。

1
2
3
4
5
6
func (c *IPConn) SetDeadline(t time.Time) error

func (c *IPConn) SetReadDeadline(t time.Time) error

func (*IPConn) SetWriteDeadline
func (c *IPConn) SetWriteDeadline(t time.Time) error

HTTP 包如何使用 TCP 连接?

http 服务器启动之后,会循环接受新请求,为每一个请求(连接)创建一个协程。

1
2
3
4
5
// net/http/server.go L1892
for {
	rw, e := l.Accept()
	go c.serve()
}

下面是每个协程的执行的代码,我只摘录了一部分关键的逻辑。可以发现,serve方法里面还有一个for循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// net/http/server.go L1320
func (c *conn) serve() {
	defer func() {
		if !c.hijacked() {
			c.close()
		}
	}()

	for {
		w, err := c.readRequest()

		if err != nil {
		}

		serverHandler{c.server}.ServeHTTP(w, w.req)
	}
}

这个循环是用来做什么的?其实也容易理解,如果是长连接,一个协程可以执行多次响应。如果只执行了一次,那就是短连接。长连接会在超时或者出错后退出循环,也就是关闭长连接。defer函数可以让协程结束之后关闭 TCP 连接。

readRequest函数用来解析 HTTP 协议。

 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
// net/http/server.go
func (c *conn) readRequest() (w *response, err error) {
	if d := c.server.ReadTimeout; d != 0 {
		c.rwc.SetReadDeadline(time.Now().Add(d))
	}
	if d := c.server.WriteTimeout; d != 0 {
		defer func() {
			c.rwc.SetWriteDeadline(time.Now().Add(d))
		}()
	}

	if req, err = ReadRequest(c.buf.Reader); err != nil {
		if c.lr.N == 0 {
			return nil, errTooLarge
		}
		return nil, err
	}
}

func ReadRequest(b *bufio.Reader) (req *Request, err error) {
	// 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, ok = parseRequestLine(s)

	mimeHeader, err := tp.ReadMIMEHeader()
}

具体参与解析 HTTP 协议的部分是ReadRequest方法,而调用它之前,设置了读写超时时间。超时时间设置的是绝对时间。所以这里都是通过time.Now().Add(d)来设置的。不同的是写超时是defer执行,也就是函数返回后才执行。

Go实现HTTP短连接

如果做短连接,直接在Server端的连接上设置SetReadDeadline。当你设置的时限到达,无论客户端是否还在继续传递消息,服务端都不会再接收。并且已经关闭连接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
    server := ":7373"
    netListen, err := net.Listen("tcp", server)
    if err != nil{
        Log("connect error: ", err)
        os.Exit(1)
    }
    Log("Waiting for Client ...")
    for{
        conn, err := netListen.Accept()
        if err != nil{
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            continue
        }

        //设置短连接(10秒)
        conn.SetReadDeadline(time.Now().Add(time.Duration(10)*time.Second))

        Log(conn.RemoteAddr().String(), "connect success!")
        ...
    }
}

这就可以了。在这段代码中,每当10秒中的时限一道,连接就终止了。

Go实现HTTP长连接

SetKeepAlive

GO 可以通过 net.TCPConn 的 SetKeepAlive 来启用 TCP keepalive。在 OS X 和 Linux 系统上,当一个连接空间了2个小时时,会以75秒的间隔发送8个TCP keepalive探测包。换句话说, 在两小时10分钟后(7200+8*75)Read将会返回一个 io.EOF 错误.

对于你的应用,这个超时间隔可能太长了。在这种情况下你可以调用SetKeepAlivePeriod方法。但这个方法在不同的操作系统上会有不同的表现。在OSX上它会更改发送探测包前连接的空间时间。在Linux上它会更改连接的空间时间与探测包的发送间隔。所以以30秒的参数调用 SetKeepAlivePeriod在OSX系统上会导致共10分30秒(30+8*75)的超时时间,但在linux上却是4分30秒(30+8*30).

当然,可以设置 http.Transport 的 DisableKeepAlives 来禁用掉持久连接。

自行实现心跳协议

client每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。发包方可以是客户也可以是服务端..

心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//长连接入口
func handleConnection(conn net.Conn,timeout int) {

	buffer := make([]byte, 2048)
	for {
		n, err := conn.Read(buffer)

		if err != nil {
			LogErr(conn.RemoteAddr().String(), " connection error: ", err)
			return
		}
		Data :=(buffer[:n])
		messnager := make(chan byte)
		postda :=make(chan byte)
		//心跳计时
		go HeartBeating(conn,messnager,timeout)
		//检测每次Client是否有数据传来
		go GravelChannel(Data,messnager)
		Log( "receive data length:",n)
		Log(conn.RemoteAddr().String(), "receive data string:", string(Data

	}
}

//心跳计时,根据GravelChannel判断Client是否在设定时间内发来信息
func HeartBeating(conn net.Conn, readerChannel chan byte,timeout int) {
		select {
		case fk := <-readerChannel:
			Log(conn.RemoteAddr().String(), "receive data string:", string(fk))
			conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
			//conn.SetReadDeadline(time.Now().Add(time.Duration(5) * time.Second))
			break
		case <-time.After(time.Second*5):
			Log("It's really weird to get Nothing!!!")
			conn.Close()
		}

}

func GravelChannel(n []byte,mess chan byte){
	for _ , v := range n{
		mess <- v
	}
	close(mess)
}


func Log(v ...interface{}) {
	log.Println(v...)
}

这样,就可以成功实现对于长连接的处理了~~,我们可以这么进行测试:

1
2
3
4
5
6
7
8
9
func sender(conn net.Conn) {
	for i := 0; i <5; i++ {
		words:= strconv.Itoa(i)+"This is a test for long conn"
		conn.Write([]byte(words))
		time.Sleep(2*time.Second)

	}
	fmt.Println("send over")
}

可以发现,Sender函数中time.Sleep阻塞的时间设定的比Server中的timeout短的时候,Client端的信息可以自由的发送到循环结束,而当我们设定Sender函数的阻塞时间较长时,就只能发出第一次循环的信息。

参考: https://blog.csdn.net/lengyuezuixue/article/details/79235850 http://blog.cyeam.com/golang/2017/05/31/go-http-keepalive https://blog.csdn.net/u010824081/article/details/78108984