golang源码解读之 net.Dial

1、调用 例子

conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
	// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...

源码dial.go文件中没有实际发起连接,主要是对一些参数进行预处理,比如:解析网络类型、从addr解析ip地址,而实际发起连接的函数在tcpsock_posix.go、udpsock_posix.go。
2、net.Dial的源码解读

// net.Dial函数解读
//  实际是对Dialer.Dial的一个封装, 封装后,可以直接调用Dial拨号,而不需要再去定义一个Dialer结构体对象,利用对象拨号,省去了定义结构体对象,封装时用的同名方法,便于记忆
func Dial(network, address string) (Conn, error) {
	var d Dialer      // 定义了一个 Dialer结构体对象,使用该对象的Dial方法去拨号,所以net.Dial 实际是对Dialer.Dial的一个封装
	return d.Dial(network, address)
}

3、Dialer.DialContext 解读
d.DialContext()可以传入一个context,如果context的生命周期在connect完成之前结束,那么会立即返回错误。如果context在连接建立完成之后结束,则不会影响连接。另外如果addr是一组ip地址的话,会把当前剩下的所有时间均分到每个ip上去尝试连接。只要有一个成功,就会立即返回成功的连接并取消其他尝试

// Dialer.DialContext 解读
// DialContext是结构体Dialer对象的原始拨号方法,入参三个(ctx上下文用于设置拨号对象的上下文截止时间、)
// DialContext使用提供的上下文连接到指定网络上的地址, 上下文必须非0且不能过期
// 主机的每个ip链接时间 是timeout/n,n是多少个ip,
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
	// 1、首先判断参数,上下文为空报错
	if ctx == nil {
		panic("nil context")
	}
	// 2、获取dialer和上下文的最早的deadline, (ctx上下文用于设置拨号对象的上下文截止时间、time.now()用于计算设置timeout字段时候的截止时间)
	deadline := d.deadline(ctx, time.Now())
	if !deadline.IsZero() {
		// 当计算出了截止时间:如果上下文没有截止时间、或者最终截止时间早于上下文截止时间
		if d, ok := ctx.Deadline(); !ok || deadline.Before(d) {
			// 就将最终截止时间 设置成 上下文的截止时间处理, 还会返回一个cancel函数,在最后调用
			subCtx, cancel := context.WithDeadline(ctx, deadline)
			defer cancel()
			// 构建上下文:返参 subCtx就是父上下文的一个副本,现在赋值回去
			ctx = subCtx
		}
	}
	// 3、拨号对象 调用取消通道读值,如果有取消值
	if oldCancel := d.Cancel; oldCancel != nil {
		// 就对上下文取消操作:WithCancel 返回具有新cancel通道的父级的副本
		subCtx, cancel := context.WithCancel(ctx)
		defer cancel()   // 一旦在此上下文中运行的操作完成,就调用cancel

		// 开启协程判断 拨号对象的取消通道读值,和上下文完成的工作应取消时,Done返回一个关闭的通道读值,任何一个先完成,都会进行取消操作
		go func() {
			select {
			case <-oldCancel:
				cancel()
			case <-subCtx.Done():
			}
		}()
		// 将副本 赋值给 父上下文
		ctx = subCtx
	}

	// 4、在解析期间对nettrace(如果有的话)进行阴影处理,这样就不会为DNS查找触发连接事件
	// 调用	import "internal/nettrace",TraceKey{}是一个上下文得键key, 关联值应该是*Trace结构, 取出值后类型断言成*nettrace.Trace类型
	resolveCtx := ctx
	if trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace); trace != nil {
		shadow := *trace
		// 设置在拨号之前之后都不调用该上下文的值
		shadow.ConnectStart = nil
		shadow.ConnectDone = nil
		// 返回父级的副本,提供的键必须可比较、用户需要自己定义类型
		resolveCtx = context.WithValue(resolveCtx, nettrace.TraceKey{}, &shadow)
	}

	// 5、 设置解析器, 并根据解析器返回地址列表,
	// d.resolver()方法:如果设置了解析器就返回,没设置就返回默认解析器
	// resolveAddrList() 方法: 调用parseNetwork解析出string的网络协议,根据不同网络类型分别调用ResolveUnixAddr,和internetAddrList方法解析出地址(文字IP地址或DNS名称).
	addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)
	if err != nil {
		return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
	}

	// 6、定义一个带有配置的 系统拨号对象(配置由参数传递)
	sd := &sysDialer{
		Dialer:  *d,
		network: network,
		address: address,
	}

	var primaries, fallbacks addrList
	// 7、如果tcp拨号对象有设置 快速回退等待时长,则将地址列表分为两类(每个地址有一个布尔标签。第一个地址和任何具有匹配标签的地址将作为主地址返回,而具有相反标签的地址将作为回退返回)
	// dualStack()是d.FallbackDelay >= 0时的封装, 返回bool, FallbackDelay是快速回退等待的时间长
	if d.dualStack() && network == "tcp" {
		primaries, fallbacks = addrs.partition(isIPv4)     // 两类地址
	} else {
		// 如果没有设置,则直接将地址列表传给自定义primaries初选变量
		primaries = addrs
	}

	var c Conn
	// 8、如果 回退地址有值,传入回退地址表然后使用系统拨号对象开始拨号: dialParallel(),与dialSerial的两个副本进行竞争,返回第一个建立的连接并关闭其他连接
	if len(fallbacks) > 0 {
		c, err = sd.dialParallel(ctx, primaries, fallbacks)
	} else {
		// 9、否则如果没有设置,dialSerial()方法 按顺序连接到地址列表,返回第一个成功连接或第一个错误
		c, err = sd.dialSerial(ctx, primaries)
	}
	if err != nil {
		return nil, err
	}

	// 10、Conn转TCPConn,如果拨号且处于连接状态:调用setKeepAlive()将 包装网络调用、标记网络文件描述符参数可访问、得到一个新的系统错误
	if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
		// tc.fd,连接里的唯一字段:网络文件描述符结构体

		setKeepAlive(tc.fd, true)
		// setKeepAlive() 是SetsockoptInt(用int参数包装setsockopt网络调用)、KeepAlive将其参数fd 标记为当前可访问、
		//  wrapSyscallError(接受一个错误和一个syscall名称,返回一个新的SyscallError,其中包含给定的系统调用名和错误详细信息)

		// 将连接时长传给ka, 如果没设置则默认值15s
		ka := d.KeepAlive
		if d.KeepAlive == 0 {
			ka = defaultTCPKeepAlive
		}

		// 根据 网络文件描述符 设置保持连接时间
		setKeepAlivePeriod(tc.fd, ka)
		// 测试
		testHookSetKeepAlive(ka)
	}
	return c, nil
}

DialContext最终调用的是dialParallel和dialSerial,先看dialParallel,该函数将v4地址和v6地址分开,先尝试v4地址组,在dialer.fallbackDelay 时间后开始尝试v6地址组,每一组都是调用dialSerial(),让两组竞争:

4、dialParallel()源码解读

// dialParallel 竞赛dialSerial的两个副本,给第一个先机。它返回第一个建立的连接并关闭其他连接。 否则,它将从第一个主地址返回一个错误。
func (sd *sysDialer) dialParallel(ctx context.Context, primaries, fallbacks addrList) (Conn, error) {
	// 如果回退地址为空,还是调用dialSerial()按顺序连接地址
	if len(fallbacks) == 0 {
		return sd.dialSerial(ctx, primaries)
	}

	returned := make(chan struct{})
	defer close(returned)

	// 拨号结果对象
	type dialResult struct {
		Conn
		error
		primary bool
		done    bool
	}
	results := make(chan dialResult) // unbuffered

	// 跟踪处理函数,根据传入的primary决定使用哪个地址拨号
	// ras: remote addresses
	startRacer := func(ctx context.Context, primary bool) {
		ras := primaries
		if !primary {
			ras = fallbacks
		}
		// 无论哪列地址,都调用按顺序拨号,写入结果通道
		c, err := sd.dialSerial(ctx, ras)
		select {
		case results <- dialResult{Conn: c, error: err, primary: primary, done: true}:
		case <-returned:
			// 当返回通道能读出数据,且有连接对象返回,关闭连接
			if c != nil {
				c.Close()
			}
		}
	}

	var primary, fallback dialResult

	// Start the main racer.

	// 使用传入的上下文 新建一个具有新完成通道的父级(ctx)的副本
	primaryCtx, primaryCancel := context.WithCancel(ctx)
	defer primaryCancel()
	// 协程,调用处理函数,根据具有取消通道的上下文,此时primary为真,则不使用会提add,使用第一个add
	go startRacer(primaryCtx, true)

	// 启动计时器,回退时间为周期,使用完成后defer关闭这个计时器
	fallbackTimer := time.NewTimer(sd.fallbackDelay())
	defer fallbackTimer.Stop()

	for {
		select {
		// ipv6延迟时间到,开始尝试ipv6地址组
		case <-fallbackTimer.C:
			fallbackCtx, fallbackCancel := context.WithCancel(ctx)
			defer fallbackCancel()
			go startRacer(fallbackCtx, false)

		// 当至少有一组已经建立连接,此时会写出数据
		case res := <-results:
			if res.error == nil {
				return res.Conn, nil
			}
			if res.primary {
				primary = res
			} else {
				fallback = res
			}
			// 但是如果同时建立了连接,就需要抛弃并返回
			if primary.done && fallback.done {
				return nil, primary.error
			}
			if res.primary && fallbackTimer.Stop() {
				//如果我们能够停止计时器,这意味着它正在运行(尚未启动回退),但是我们在主路径上遇到了一个错误,所以立即启动回退
				fallbackTimer.Reset(0)
			}
		}
	}
}

5、sd.dialSerial源码解读

// 按顺序连接到地址列表,返回第一个成功连接或第一个错误
func (sd *sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) {
	var firstErr error // 链接的第一个错误

	// 遍历 地址
	for i, ra := range ras {
		// 检测上下文是否取消,取消则返回
		select {
		case <-ctx.Done():
			return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
		default:
		}

		// 上下文截止时间处理
		dialCtx := ctx
		// 前 i 个IP地址的连接失败,然后将剩下的时间均分到剩余的IP地址
		if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
			// 取出上下文的deadline, 当多个地址挂起时,partialDeadline()根据当前时间、截止时间、剩余地址数 返回用于单个地址的截止日期
			partialDeadline, err := partialDeadline(time.Now(), deadline, len(ras)-i)
			if err != nil {
				// Ran out of time.
				if firstErr == nil {
					firstErr = &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: err}
				}
				break
			}
			// 如果取出单个地址截止时间早于 求得的截止时间, 则将单个地址取得的截止时间设置成 新的上下文的截止时间
			if partialDeadline.Before(deadline) {
				var cancel context.CancelFunc
				dialCtx, cancel = context.WithDeadline(ctx, partialDeadline)
				defer cancel()
			}
		}
		// 使用新截止时间的上下文和 地址,建立单个连接,并返回
		c, err := sd.dialSingle(dialCtx, ra)
		if err == nil {
			return c, nil
		}
		if firstErr == nil {
			firstErr = err
		}
	}

	if firstErr == nil {
		firstErr = &OpError{Op: "dial", Net: sd.network, Source: nil, Addr: nil, Err: errMissingAddress}
	}
	return nil, firstErr
}

6、处理deadlien 的方法 partialDeadline源码

// 返回在多个地址挂起时用于单个地址的截止日期
// 使用 : partialDeadline(time.Now(), deadline, len(ras)-i)
func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) {
	// 如果时间是0,则返回
	if deadline.IsZero() {
		return deadline, nil
	}

	// 和当前时间的间距, 需要大于当前时间
	timeRemaining := deadline.Sub(now)
	if timeRemaining <= 0 {
		return time.Time{}, errTimeout
	}

	// timeout 为每个 剩余地址分配相等的时间
	timeout := timeRemaining / time.Duration(addrsRemaining)

	// 如果每个地址的时间太短,则从列表的末尾窃取
	const saneMinimum = 2 * time.Second
	if timeout < saneMinimum {
		if timeRemaining < saneMinimum {
			timeout = timeRemaining  // 时间太短, 每个地址的截止时间为 剩余的时间
		} else {
			timeout = saneMinimum	// 否则,就是最短时间2s
		}
	}
	// 截止时间就是计算出的 此时+timeout
	return now.Add(timeout), nil
}

7、上下文context设置截止时间的方法 WithDeadline

// 如果父上下文的截止时间小于d, 则此时还是等于父上下文
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

8、上下文context 结构体源码解读

// 上下文包含截止日期、取消信号和其他值
// 多个goroutine可以同时调用Context的方法
type Context interface {
	// 返回代表此上下文完成的工作应取消的时间,没有设置返回false
	Deadline() (deadline time.Time, ok bool)

	// 当代表此上下文完成的工作应取消时,Done返回一个关闭的通道。
	// 如果无法取消此上下文,则Done可能返回nil。
	// cancel函数返回后,Done通道的关闭可能会异步进行
	// WithCancel安排在调用cancel时关闭Done
	// WithTimeout安排在超时结束时关闭Done
	Done() <-chan struct{}

	// 关闭了Done,Err将返回一个非nil错误
	Err() error

	// 仅对传输进程和API边界的请求范围的数据使用上下文值,而不是将可选参数传递给函数
	// 希望在上下文中存储值的函数通常在全局变量中分配一个键,然后将该键用作context.with值和Context.Value.
	// 客户端使用user.NewContext以及user.FromContext,NewContext返回一个携带值u的新上下文,
	// 其实是return context.WithValue(ctx, userKey, u)函数的封装; FromContext返回存储在ctx中的用户值,其实是u, ok := ctx.Value(userKey).(*User) 的封装
	Value(key interface{}) interface{}
}


type CancelFunc func()

9、其中的 拨号对象 Dialer 解读

// 拨号对象解读
// 包含一些连接的选项,零值代表不带该选项
type Dialer struct {
	// 是拨号将等待的最大时间量
	// 如果没有设置,则使用操作系统的较早超时,tcp是3min左右
	Timeout time.Duration

	// 截止日期 是拨号失败的绝对时间点
	Deadline time.Time

	// 拨号时使用的本地地址,为0是自动选择本地地址
	// 真正dial时的本地地址,兼容各种类型(TCP、UDP...),如果为nil,则系统自动选择一个地址
	LocalAddr Addr       // Addr网络端点地址,是接口

	// 以前支持rfc6555快速回退支持,也被称为“快乐眼球”,如果IPv6被错误配置和挂起,IPv4将很快被尝试
	// 双协议栈,即是否同时支持ipv4和ipv6.当network值为tcp时,dial函数会向host主机的v4和v6地址都发起连接
	// 已弃用:默认情况下启用快速回退
	DualStack bool

	// 当DualStack为真,ipv6会延后于ipv4发起,此字段即为延迟时间,默认为300ms
	FallbackDelay time.Duration

	// 指定活动网络连接的keep-alive探测之间的间隔,如果协议和操作系统支持,那默认15s
	KeepAlive time.Duration

	// 解析器
	Resolver *Resolver

	// 可选通道,其关闭指示应取消拨号
	// 这个取消不推荐使用,改用了DialContext
	Cancel <-chan struct{}

	// 如果有传这个控制函数,创建网络连接后但在实际拨号之前调用它,传递给控制方法的网络和地址参数不一定是传递给拨号的参数。
	Control func(network, address string, c syscall.RawConn) error
}

10、拨号对象设置 截止日期的方法 (d *Dialer) deadline

// 拨号对象设置 截止日期的方法
// 截止日期返回以下最早日期:now+Timeout、d.Deadline、上下文的deadline, 如果没有设置则是0
func (d *Dialer) deadline(ctx context.Context, now time.Time) (earliest time.Time) {
	// 1、如果有设置超时,则将设置的超时传给最早结束时间
	if d.Timeout != 0 { // including negative, for historical reasons
		earliest = now.Add(d.Timeout)
	}
	// 2、如果获取到上下文的截止时间,则取两者最早的哪个时间
	if d, ok := ctx.Deadline(); ok {
		earliest = minNonzeroTime(earliest, d)
	}
	// 3、最后对比结构体对象的字段;如果设置了字段-截止时间,则取截止时间和上述的对比结果时间
	return minNonzeroTime(earliest, d.Deadline)
}

11、自己在包内定义 最早时间对比方法

func minNonzeroTime(a, b time.Time) time.Time {
	// 如果有一方时间未设置,则舍弃
	if a.IsZero() {
		return b
	}
	// 调用time.Before运算():该方法主要是在不同条件时,对入参时间的的s数对比,或者ns数对比
	if b.IsZero() || a.Before(b) {
		return a
	}
	return b
}

目前,dial.go源码解读相关就是这些。源码也是调用了内置库的一些其他的处理函数,考虑到很多情况的判断,比如参数的是否零值、不同传值需要调用的处理函数。
总之,dial只是做了一些预设置,并且返回了一个连接对象。
在网络network上连接地址address,并返回一个Conn接口。可用的网络类型有:
“tcp”、“tcp4”、“tcp6”、“udp”、“udp4”、“udp6”、“ip”、“ip4”、“ip6”、“unix”、“unixgram”、“unixpacket”
对TCP和UDP网络,地址格式是host:port或[host]:port,参见函数JoinHostPort和SplitHostPort。


版权声明:本文为yyj18085644原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。