Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Golang:为了实现 UDPping 而产生的问题与解惑记录 #19

Open
wisecsj opened this issue Mar 9, 2019 · 4 comments
Open

Golang:为了实现 UDPping 而产生的问题与解惑记录 #19

wisecsj opened this issue Mar 9, 2019 · 4 comments

Comments

@wisecsj
Copy link
Owner

wisecsj commented Mar 9, 2019

UDPping作业来自于《计算机网络-自顶向下方法(原书第6版)》

具体作业要求可见:https://github.com/wisecsj/Computer-Networking-A-Top-Down-Approach-NOTES

大致来讲,其实本质就是一个udp server和client进行通信,然后server随机丢包来模拟ping包的丢失

由于我一开始并不知道SetReadDeadline method的存在(还好不知道,不然应该没有这篇blog了,hh)。后面也会有对这个method的理解笔记。

那么不用SetReadDeadline,需要考虑的是什么呢?

addr, _ := net.ResolveUDPAddr("udp", "localhost:8000")
c, err := net.DialUDP("udp", nil, addr)

因为我们需要计算丢包率,所以需要识别一个包的接收是否timeout了.显然想到了利用time.After + select 来实现。

我的第一版实现:https://gist.github.com/wisecsj/cde649a32001a9186586a899002b944e

package main

import (
	"fmt"
	"log"
	"net"
	"net/http"
	_ "net/http/pprof"
	"time"
)

// Client launch the client
func Client() {
	addr, _ := net.ResolveUDPAddr("udp", "localhost:8000")
	c, err := net.DialUDP("udp", nil, addr)
	if err != nil {
		panic("Dial failed ...")
	}
	// RTT := make([]int, 10)
	maxRTT := 0.0
	minRTT := 0.0
	const total int = 10
	// packet超时时间
	const mtime = time.Second * 3
	lossNum := 0
	for i := 0; i < total; i++ {
		ch := make(chan bool)
		start := time.Now()
		msg := fmt.Sprintf("Ping %d %s", i, start)
		c.Write([]byte(msg))
		buf := make([]byte, 1024)
		// c.Read(buf)
		// log.Printf("%s", buf)
		go func() {
			c.Read(buf)
			ch <- true

		}()

		select {
		case <-ch:
		case <-time.After(mtime):
			lossNum++
			log.Printf("Faild: Ping %d,cost %f s", i, float64(time.Since(start)/time.Second))
			continue
		}
		duration := time.Since(start)
		d := float64(duration / time.Millisecond)
		if d > maxRTT {
			maxRTT = d
		}
		if d < minRTT {
			minRTT = d
		}
		log.Printf("sucess:ping %d;RTT:%f ms", i, d)
	}

}

@wisecsj
Copy link
Owner Author

wisecsj commented Mar 9, 2019

贴一下运行截图:

图片

总结来说就是:当server端开始出现第一次丢包后,client端的输出表明后面的包无论server丢弃与否,都走的超时逻辑代码

接下来来一一解决这份代码的问题

@wisecsj
Copy link
Owner Author

wisecsj commented Mar 9, 2019

一. net.conn没有显式关闭

这一点对于这个程序来说没什么影响。但是如果是对于一直运行的server的话,就会造成资源泄露。

而且golang也提供了defer,所以在创建conn的语句后加上 defer c.Close() 即可

@wisecsj
Copy link
Owner Author

wisecsj commented Mar 10, 2019

二. goroutine泄露

这个程序运行错误的罪魁祸首就是它了。

首先得了解一下 goroutine+closure 的组合。这份代码里实际上就是这样一个模式:匿名goroutine里闭包了channel类型的ch。可以认为闭包是一个结构体,一个字段指向执行函数,一个字段指向变量ch(且不再改变)。

当我们没有给c设置deadline的时候,c.Read是会一直阻塞住的,直到接收到一个完整的datagram。那么当 server 端第一次丢弃udp包后,对应的接收 goroutine A 的c.Read就会一直阻塞,直到超时,然后执行下一次循环。于是乎,又新启了一个goroutine B 进行 c.Read ,并且阻塞在read waiter queue中,排在前一个goroutine A之后。

导致的结果就是,本该由B接收的udp包,被A给读取了。而goroutine A形成的闭包里的ch早已不是如今select里的ch了,因为我用的是 ch := make(chan bool) ,从而导致goroutine A 接收到了包,但是由于channel已经没有了接收者而永远阻塞住了。之后的循环以此往复,全部阻塞住了,最终导致了上面的运行图。


所以当我们编写goroutine的时候,一定要考虑好goroutine的退出条件是否可以达到,避免造成 goroutine 泄露。

因为一开始没有头绪,还尝试了通过 pprof 来分析的方法。但是由于我这个程序循环了十次,时间太短了吧,所以得到的 cpu profile sample 数量都是 0.

@wisecsj
Copy link
Owner Author

wisecsj commented Mar 10, 2019

修改后的版本:https://gist.github.com/wisecsj/7fb5148c40f539ba0b1725c9adbb30d5

运行截图:

图片


SetDeadline 函数说明:

可能很多人奇怪为什么它的参数类型是time.Time, 而不是 time.Duration。其实一开始它函数名为 SetTimeout ,参数也的确是 Duration。

但是后来修改过来了,具体原因可以到官方 golang issues 里去看。

我的理解是:当参数指定的time.Time到了,那么当前与特定net.conn挂钩的read和write操作都会返回错误(net.Error,并且Timeout()为true)。但是不影响之后的读写操作,所以如果需要实现Timeout的功能,需要循环设置。

@wisecsj wisecsj pinned this issue Mar 10, 2019
@wisecsj wisecsj unpinned this issue May 12, 2019
@wisecsj wisecsj pinned this issue May 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant