golang tcp_nodelay延迟参数的趣事

前言:

tcp_nodelay是tcp协议里nagle算法和delay ack的控制开关,nagle是什么? 简单说通过缓冲区合并包来减少包的数量,来提高网络的利用率。

该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=6311

nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

在开启nagle算法下的流程,数据包会先放到socket写缓冲区,只有当缓冲区的数据大于mss,上一个ack到达,或者 触发定时器才会发送。 下面是wiki里找到的关于nagle的伪代码:

// xiaorui.cc

if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

tcp的delay ack又是什么? 当server收到一个包准备返回一个ack,但因为一个tcp最少占用40个字节,为了提高带宽利用率,可以跟数据一起返回,或者等其他ack返回。如果没人跟他一起返回,那么会依赖定时器发送。

到这里可以得知,nagle和delayed ack是牺牲网络的及时性来提高网络的利用率,但现在市面上的网络框架默认多是开启nodelay的。但我这边想测测在nodelay=false的情况下,移动端是个什么情况。

Golang tcp_NoDelay问题

golang在net库默认是开启nodelay=true的,就是关闭nagle和时延ack算法。nagle缺点就是高延迟,但他所谓的优点是提高网络利用率。但根据我的大量测试,关闭nodelay跟开启nodelay的网络质量是一样的。很奇怪。。。

写了个demo进行测试,抓包发现数据并没有合并发送。

// xiaorui.cc

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    target := "172.17.37.143:8000"
    raddr, err := net.ResolveTCPAddr("tcp", target)
    if err != nil {
        log.Fatal(err)
    }
    // Establish a connection with the server.
    conn, err := net.DialTCP("tcp", nil, raddr)
    if err != nil {
        log.Fatal(err)
    }

    // 关闭nodelay
    conn.SetNoDelay(false)

    for i := 1; i <= 5; i++ {
        _, err = conn.Write([]byte("xiaorui.cc#"))
        if err != nil {
            log.Fatal(err)
        }
    }

    time.Sleep(1 * time.Second)

    for i := 1; i <= 5; i++ {
        _, err = conn.Write([]byte("xiaorui.cc#"))
        if err != nil {
            log.Fatal(err)
        }
    }

    time.Sleep(100 * time.Second)
}
// xiaorui.cc

#######
T 172.17.37.143:39196 -> 172.17.37.143:8000 [AP]
  xiaorui.cc#
##
T 172.17.37.143:39196 -> 172.17.37.143:8000 [AP]
  xiaorui.cc#
##
T 172.17.37.143:39196 -> 172.17.37.143:8000 [AP]
  xiaorui.cc#
##
T 172.17.37.143:39196 -> 172.17.37.143:8000 [AP]
  xiaorui.cc#
...

用strace追看下client端的系统调用,setsockopt是用来操作tcp属性的系统调用,我们会发现他调用了两遍,第一遍是go std net默认会开启nodelay,第二遍是我们手动触发关闭nodelay。

// xiaorui.cc

[pid 25542] setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
[pid 25542] setsockopt(3, SOL_TCP, TCP_NODELAY, [0], 4) = 0
[pid 25542] write(3, "xiaorui.cc#", 11) = 11
[pid 25542] write(3, "xiaorui.cc#", 11) = 11
[pid 25542] write(3, "xiaorui.cc#", 11) = 11
[pid 25542] write(3, "xiaorui.cc#", 11) = 11
[pid 25542] write(3, "xiaorui.cc#", 11) = 11

那么可能是server端代码没有配置关闭nodelay引起的问题? net.Conn本身没有提供tcp_nodelay的方法,需要转换到net.TCPConn来操作。

// xiaorui.cc

package main

import (
	"fmt"
	"net"
	"strings"
)

func connHandler(c net.Conn) {
	if c == nil {
		return
	}
	buf := make([]byte, 4096)
	for {
		cnt, err := c.Read(buf)
		if err != nil || cnt == 0 {
			c.Close()
			break
		}
		inStr := strings.TrimSpace(string(buf[0:cnt]))
		fmt.Printf("msg: %s\n", inStr)
	}

	fmt.Printf("Connection from %v closed. \n", c.RemoteAddr())
}

func setNoDelay(conn net.Conn) error {
	switch conn := conn.(type) {
	case *net.TCPConn:
		var err error
		if err = conn.SetNoDelay(false); err != nil {
			return err
		}
		return err

	default:
		return fmt.Errorf("unknown connection type %T", conn)
	}
}

func main() {
	server, err := net.Listen("tcp", ":8000")
	if err != nil {
		fmt.Printf("Fail to start server, %s\n", err)
	}

	for {
		conn, err := server.Accept()
		setNoDelay(conn)

		if err != nil {
			fmt.Printf("Fail to connect, %s\n", err)
			break
		}
		go connHandler(conn)
	}
}

但依旧有问题,还是没看延迟的效果.... 下面是server端的setsockopt系统调用。

// xiaorui.cc

[pid 25778] setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0
[pid 25778] setsockopt(4, SOL_TCP, TCP_NODELAY, [0], 4) = 0

总结:

估计是由于操作系统的原因,导致测试不到tcp延迟的效果。懒得测试了。


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc