Socket 编程(二):Dial 函数的底层实现及超时处理

Dial 函数的底层调用

上篇教程中,我们介绍了 Go 语言中可以通过 Dial() 函数建立网络连接。实际上,Dial() 函数是对 dialTCP()dialUDP()dialIP()dialUnix() 的封装,这可以通过追溯 Dial() 函数的源码看到,底层真正建立连接是通过 dialSingle() 函数完成的:

net.DialSingle函数底层实现

dialSingle() 函数通过从传入参数中获取网络协议类型调用对应的连接建立函数并返回连接对象。再往下追溯,可以看到这些底层函数最终都调用了 syscall 包的 Socket() 函数与对应平台操纵系统的 Socket API 交互实现网络连接的建立,针对不同的通信协议,建立不同的连接类型:

syscall.Socket函数底册实现

其中 domain 代表通信域,支持 IPv4、IPv6 和 Unix,对应的常量值分别是 syscall.AF_INETsyscall.AF_INET6syscall.AF_UNIX

注:IPv4 和 IPv6 分别代表 IP 协议网络的第四版和第六版,Unix 指的是类 Unix 操作系统中特有的通信域,在装有此类操作系统的同一台计算机中,应用程序可以基于此域建立 socket 连接。

typ 代表 Socket 的类型,比如 TCP 对应的 Socket 类型常量是 syscall.SOCK_STREAM(面向连接通信),UDP 对应的 Socket 类型常量是 syscall.SOCK_DGRAM(面向无连接通信),此外还支持 syscall.SOCK_RAWsyscall.SOCK_SEQPACKET 两种类型,SOCK_RAW 其实就是原始的 IP 协议包,SOCK_SEQPACKETSOCK_STREAM 类似,都是面向连接的,只不过前者有消息边界,传输的是数据包,而不是字节流。通常,我们使用 SOCK_STREAMSOCK_DGRAM 居多。

最后一个参数 proto 表示通信协议,一般默认为 0,因为该值可以通过前两个参数判断得出,比如,前两个参数值分别为 syscall.AF_INETsyscall.SOCK_DGRAM 的时候,会选择 UDP 作为通信协议,前两个参数值分别为 syscall.AF_INET6syscall.SOCK_STREAM 时,会选择 TCP 作为通信协议。

当然,我们在 Go 语言中编写网络程序时,完全不用关心这些底层的实现细节,只需要调用 Dial 函数并传入对应的参数就可以了。

网络超时处理

网络超时包含在多个环节中,比如连接超时、请求超时和响应超时,我们先来看连接超时。

连接超时

在使用 Dial 函数建立网络连接时,可以使用 net 包提供的 DialTimeout 函数主动传入额外的超时参数来建立连接,该函数原型如下:

func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {
    d := Dialer{Timeout: timeout}
    return d.Dial(network, address)
}

Dial 函数调用一样,只是设置了超时字段而已,如果使用 Dial 函数,默认会通过操作系统提供的机制来处理连接超时,对于 TCP 连接,通常是 3 分钟左右,这对我们的程序来说,可能太长了,这个时候,就可以通过 DialTimeout 来建立连接,以上篇教程编写的示例代码 tcp.go 为例,如果请求国外被封的域名,比如 facebook.com,程序可能长时间没有反应,将建立网络连接的代码调整如下:

// 建立网络连接
conn, err := net.DialTimeout("tcp", service, 3 * time.Second)

再次请求,3 秒后就会返回超时错误退出:

网络连接超时处理

请求和响应超时

使用 DialDialTimeout 函数建立网络连接成功之后,都会返回 net.Conn 对象,然后我们就可以在该对象上进行读写操作实现请求和响应,关于这一部分的超时,可以通过 Conn 提供的以下三个方法来设置:

SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error

我们可以通过 SetDeadline 设置统一的读写超时时间,也可以通过 SetReadDeadlineSetWriteDeadline 分别设置读超时和写超时。注意这三个方法传入的都是绝对时间值,而不是相对时间长度:

// 设置读写超时时间
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
checkError(err)

更多工具函数

此外,net 包中还提供了一系列的工具函数,合理地使用这些函数可以有效提高开发效率,并且更好地保证程序质量。

比如,可以通过 ParseIP 函数验证 IP 地址的有效性:

func net.ParseIP()

通过 IPv4Mask 创建子网掩码:

func IPv4Mask(a, b, c, d byte) IPMask

通过 DefaultMask 获取默认子网掩码:

func (ip IP) DefaultMask() IPMask

以及根据域名查找对应 IP 地址:

func ResolveIPAddr(net, addr string) (*IPAddr, error) 
func LookupHost(name string) (cname string, addrs []string, err error)

等等,更多工具函数请参考 net 包文档

上一篇: Socket 编程(一):Dial 函数及其使用

下一篇: HTTP 编程(一):客户端如何发起 HTTP 请求