golang tcp的keepalive

1,322 阅读3分钟

socket tcp 如何维护长连接

操作系统实现:

tcp的keepalive机制开启后,在一定时间内,一般为7200s(参数tcp_keepalive_time)在链路上没有数据传送的情况下,tcp层将发送相应的keepalive探针以确定连接的可用性,探测失败后重试9次(tcp_keepalive_probes),每次间隔时间75s(tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用。

这些参数是机器级别的,可以调整。

keepalive保活机制只在链路空闲的情况下才会起作用。

image.png

应用层实现:

如果客户端已经消失而连接未断开,则会使得服务器上保留一个半开放的连接。保活功能就是试图在服务器端检测到这种半开放的连接。

1)客户端依然正常运行且可达。此时客户端正常响应,服务器将保活定时器复位。

2)客户端主机已经崩溃,并且已经重新启动。服务端会收到一个对其保活探测的响应,这个响应是一个复位(RST),使得服务器终止这个连接。

3)客户端主机已经崩溃,并且关闭或者正在重启。客户端无法响应,服务端将无法收到客户端的探测响应,服务端连续发送多个探测,每个间隔指定时间,若服务器没有收到响应,就认为客户端已经关闭并终止连接。

4)客户端正常,服务端不可达,同第3点

net包的Dial源码

func (d *Dialer) Dial(network, address string) (Conn, error) {
   return d.DialContext(context.Background(), network, address)
}


func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
    ...
    
    var c Conn
    if len(fallbacks) > 0 {
       c, err = sd.dialParallel(ctx, primaries, fallbacks)
    } else {
       c, err = sd.dialSerial(ctx, primaries)
    }
    if err != nil {
       return nil, err
    }

    if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
       setKeepAlive(tc.fd, true) //
       ka := d.KeepAlive
       if d.KeepAlive == 0 {
          ka = 15 * time.Second
       }
       setKeepAlivePeriod(tc.fd, ka) //
       testHookSetKeepAlive(ka)
    }
    return c, nil
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
	c, err = sd.dialSerial(ctx, primaries)
}

func (sd *sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) {
	c, err := sd.dialSingle(dialCtx, ra)		
}

func (sd *sysDialer) dialSingle(ctx context.Context, ra Addr) (c Conn, err error) {
	c, err = sd.dialTCP(ctx, la, ra)
}

func (sd *sysDialer) dialTCP(ctx context.Context, laddr, raddr *TCPAddr) (*TCPConn, error) {
	return sd.doDialTCP(ctx, laddr, raddr)
}

func (sd *sysDialer) doDialTCP(ctx context.Context, laddr, raddr *TCPAddr) (*TCPConn, error) {
	fd, err := internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control) //

	for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
		if err == nil {
			fd.Close()
		}
		fd, err = internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control)
	}

	if err != nil {
		return nil, err
	}
	return newTCPConn(fd), nil
}

net包的listen

image.png

由图可以看出,http的监听是对net.Listen()的封装,而net.Listen()是对Tcp和Unix的封装。而http的监听传入的都是tcp。所有http的监听最终都是实现的net.ListenTcp()。

其中net.ListenTcp,net.ListenUdp(),net.ListenIP()最终走的都是internetSocket()。

func Listen(network, address string) (Listener, error) {
   var lc ListenConfig
   return lc.Listen(context.Background(), network, address)
}


func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
    ...
    switch la := la.(type) {
    case *TCPAddr:
       l, err = sl.listenTCP(ctx, la)
    case *UnixAddr:
       l, err = sl.listenUnix(ctx, la)
    }
    ...
}

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
   fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control) //
   if err != nil {
      return nil, err
   }
   return &TCPListener{fd}, nil
}

net/http/server.go

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
type tcpKeepAliveListener struct {
	*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
	tc, err := ln.AcceptTCP()
	if err != nil {
		return nil, err
	}
	tc.SetKeepAlive(true) //
	tc.SetKeepAlivePeriod(3 * time.Minute)
	return tc, nil
}

net.TCPConn 的 SetKeepAlive 用来启用TCP keepalive, SetKeepAlivePeriod

参考:

net.Dail 源码分析 blog.csdn.net/yyj18085644…

net.Listen 源码 blog.csdn.net/aixinaxc/ar…