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。