go源码解析之TCP连接(二)——Accept

go源码解析之TCP连接系列基于go源码1.16.5

连接是如何建立的

上一章我们通过跟踪net.Listen的调用,了解了socket的创建、端口绑定、开启监听。最后net.Listen返回了一个Listener(具体对于TCP连接为TCPListener),本章将通过该Listener的Accept方法的跟踪来了解连接的建立过程。

ln, err := net.Listen("tcp", ":8080")
if err != nil {
        // handle error
}
for {
        conn, err := ln.Accept()
        if err != nil {
                // handle error
        }
        go handleConnection(conn)
}

下面我们通过逐行跟踪源码,来看连接建立的过程:

1.TCPListener的Accept方法

src/net/tcpsock.go

func (l *TCPListener) Accept() (Conn, error) {
    ...
    c, err := l.accept()
    ...
    return c, nil
}

Accept调用了TCPListener的内部方法accept:

src/net/tcpsock_posix.go

func (ln *TCPListener) accept() (*TCPConn, error) {
    fd, err := ln.fd.accept()
    if err != nil {
        return nil, err
    }
    tc := newTCPConn(fd)
    if ln.lc.KeepAlive >= 0 {
        setKeepAlive(fd, true)
        ka := ln.lc.KeepAlive
        if ln.lc.KeepAlive == 0 {
            ka = defaultTCPKeepAlive
        }
        setKeepAlivePeriod(fd, ka)
    }
    return tc, nil
}

我们先跳过ln.fd.accept和newTCPConn两个方法调用,将上一章遗留的KeepAlive配置项看一下:
大家应该还记得KeepAlive是ListenConfig中的一个属性,而ListenConfig和创建成功的监听netFD被赋值给了TCPListener:

src/net/tcpsock.go

type TCPListener struct {
    fd *netFD
    lc ListenConfig
}

如果KeepAlive大于等于0,设置socket开启KeepAlive,如果KeepAlive等于0,默认设置socket的TCP_KEEPINTVL和TCP_KEEPIDLE属性为15秒,否则设置为用户指定的时间。

2.setKeepAlive

setKeepAlive和setKeepAlivePeriod方法类似,都是设置socket的属性,我们放到一起来看:

func setKeepAlive(fd *netFD, keepalive bool) error {
    err := fd.pfd.SetsockoptInt(syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, boolint(keepalive))
    runtime.KeepAlive(fd)
    return wrapSyscallError("setsockopt", err)
}

func setKeepAlivePeriod(fd *netFD, d time.Duration) error {
    // The kernel expects seconds so round to next highest second.
    secs := int(roundDurationUp(d, time.Second))
    if err := fd.pfd.SetsockoptInt(syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, secs); err != nil {
        return wrapSyscallError("setsockopt", err)
    }
    err := fd.pfd.SetsockoptInt(syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, secs)
    runtime.KeepAlive(fd)
    return wrapSyscallError("setsockopt", err)
}

两个方法中都执行了fd.pfd.SetsockoptInt,而pfd则是netFD中的一个属性:

src/net/fd_posix.go

type netFD struct {
    pfd poll.FD

    ...
}

进一步看poll.FD的SetsockoptInt方法:

src/internal/poll/sockopt.go

func (fd *FD) SetsockoptInt(level, name, arg int) error {
    ...
    return syscall.SetsockoptInt(fd.Sysfd, level, name, arg)
}

可以看到进行了SetsockoptInt的系统调用,进行socket的属性设置。被设置的目标就是fd.Sysfd。回忆上一章中newFD方法,此处的Sysfd就是创建的系统socket的fd。
由于整个net包中不管是监听socket还是主动connect成功的socket还是accept建立的socket,都是使用netFD类进行包装,所以最好记住这个层级关系:
netFD对poll.FD进行包装,poll.FD对系统fd进行包装

介绍一下keepalive的三个内核参数:

  1. tcp_keepalive_time, 如果在该参数指定的秒数内连接始终处于空闲状态(没有收到远程主机的数据,ack不算),则内核向远程主机发起对该主机的探测
  2. tcp_keepalive_intvl,该参数以秒为单位,规定内核向远程主机发送探测的时间间隔
  3. tcp_keepalive_probes,该参数规定内核为了检测远程主机的存活而发送的探测的数量,如果探测的数量已经使用完毕仍旧没有得到响应,即断定不可达,关闭与该主机的连接,释放相关资源

setKeepAlive方法中的SO_KEEPALIVE则是设置keepalive的总开关,setKeepAlivePeriod中的TCP_KEEPINTVL对应tcp_keepalive_intvl参数,TCP_KEEPIDLE对应tcp_keepalive_time参数。TCP_KEEPCNT对应tcp_keepalive_probes,但是代码中没有搜索到使用的地方。

让我们回到accept的主流程,先跟一下ln.fd.accept方法调用:

3.netFD的accept方法

src/net/fd_unix.go

func (fd *netFD) accept() (netfd *netFD, err error) {
    d, rsa, errcall, err := fd.pfd.Accept()
    ...

    if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
        poll.CloseFunc(d)
        return nil, err
    }
    ...
    return netfd, nil
}

pfd.Accept即poll.FD的Accept方法,代码如下:

src/internal/poll/fd_unix.go

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
    ...
    for {
        s, rsa, errcall, err := accept(fd.Sysfd)
        if err == nil {
            return s, rsa, "", err
        }
        switch err {
        ...
        case syscall.EAGAIN:
            if fd.pd.pollable() {
                if err = fd.pd.waitRead(fd.isFile); err == nil {
                    continue
                }
            }
       ...
        }
        return -1, nil, errcall, err
    }
}

内部方法accept代码如下:

src/internal/poll/sock_cloexec.go

func accept(s int) (int, syscall.Sockaddr, string, error) {
    ns, sa, err := Accept4Func(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
    
    switch err {
    case nil:
        return ns, sa, "", nil
    ...
    }

    ...
}

Accept4Func同样是一个系统调用方法:var Accept4Func func(int, int) (int, syscall.Sockaddr, error) = syscall.Accept4,参数s是socket的fd,SOCK_NONBLOCK|SOCK_CLOEXEC则是设置新连接socket的属性。连接成功返回新连接socket的fd和主机地址信息。

我们再返回到poll.FD的Accept方法,accept返回后,如果没有错误就返回新连接socket的fd和主机地址信息。如果错误是EAGAIN(socket被设置为非阻塞模式,在这个socket上的系统调用都会立即返回而不会阻塞线程,例如此处的accept调用,即使没有新的连接,也会立即返回,但是错误信息会被设置为EAGAIN),并且fd.pd.pollable为true时,阻塞当前goroutine进行等待,直到有新的可读消息(此处为有新连接)时continue,再次调用accept进行接收连接。

这里提前简单说一下pollDesc(即FD中的pd),它是IO多路复用(如epoll、kqueue、CompletionPort等)在go语言中的集成,fd.pd.waitRead 即是等待io消息的到来。后续将有单独章节介绍epoll在go语言网络库中的使用。

type FD struct {
    ...
    // I/O poller.
    pd pollDesc
    ...
}

poll.FD的Accept方法返回到netFD的accept方法中,接着调用了newFD创建了netFD,newFD方法在上一章已经介绍,不再赘述。

到目前为止,整个调用链路基本讲完了,我们现在通过下面这张图回顾一下:

  1. TCPListener的accept方法调用netFD的accept方法,返回成功后,调用newTCPConn构建连接对象,并设置连接的keepalive属性
  2. netFD的accept方法调用poll.FD的Accept方法,返回成功后,调用newFD创建新socket的netFD对象
  3. poll.FD的Accept方法进行accept系统调用,如果有新连接建立成功则返回新连接socket的fd,如果遇到EAGAIN错误,则阻塞当前goroutine进行IO消息等待。

4. newTCPConn

src/net/tcp_sock.go

func newTCPConn(fd *netFD) *TCPConn {
    c := &TCPConn{conn{fd}}
    setNoDelay(c.fd, true)
    return c
}

conn是对接口类型Conn的实现,conn的唯一属性则是我们前面一直提到的netFD,conn的核心方法都是对netFD方法的包装:

src/net/net.go

type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

type conn struct {
    fd *netFD
}

TCPConn继承自conn,它比较独特的一个方法就是ReadFrom,用来从一个Reader中读取数据并写入到TCPConn的socket上:

src/net/tcpsock.go

type TCPConn struct {
    conn
}

// ReadFrom implements the io.ReaderFrom ReadFrom method.
func (c *TCPConn) ReadFrom(r io.Reader) (int64, error) {
    ...
    n, err := c.readFrom(r)
    ...
    return n, err
}

src/net/tcpsock_posix.go

func (c *TCPConn) readFrom(r io.Reader) (int64, error) {
    if n, err, handled := splice(c.fd, r); handled {
        return n, err
    }
    if n, err, handled := sendFile(c.fd, r); handled {
        return n, err
    }
    return genericReadFrom(c, r)
}

可以看到readFrom进行了两种读取并写入的尝试,这两种方式都是为了减少用户空间到内核空间的数据拷贝:

  1. splice方式,通过建立一个临时的pipe,将输入splice至pipe,再将pipe splice至输出。这里要求Reader必须是tcp或者unix连接
  2. send file方式,通过sendFile系统调用,将Reader中数据高效地传输到socket上。这里要求Reader必须是文件
  3. genericReadFrom,回归到最原始的数据拷贝方式

如果我们需要向socket写入数据并且数据源实现了Reader接口的话,我们可以选择使用ReadFrom方法来提高性能。

5. 小结

今天通过跟踪TCPListener的Accept方法,了解了server侧接收到新连接的过程。总结为以下几个点:

  1. TCPConn继承自conn,conn对netFD进行包装并实现了Conn接口,netFD对poll.FD进行包装,poll.FD对系统fd进行包装
  2. keepalive设置
  3. 避免用户空间到内核空间的数据拷贝的两种方式:splice和sendfile

下一章我们将对TCPConn的Read方法进行跟踪,来了解数据读取的过程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容