附录
说明
最近在钻研golang网络库的实现,顺便写了些测试代码,还真发现了一些有趣的东西。不知道该说是golang实现简洁呢还是该喷呢?
测试
read成功然后超时测试
服务器程序和客户端程序互相配合,服务器端对新来的连接(创建了新协程来处理该连接)设置超时时间10s,在超时时间内,客户端发送了数据,然后客户端停止发送数据(但不断开连接),服务器端在某个时刻出现超时。
为了更直观地查看到服务器内部运行时信息,稍微hack了下go1.5的代码,加了一些打印语句,以下是服务器终端输出信息:
accept connection: &{conn:{fd:0xc8200580e0}}
sys_accept returns EAGAIN, sleep~
set deadline 1441023477949855606 for mode 233
// 服务器端新连接第一次收到数据
Now: 1441023470, hello
// 超时定时器到来,等待读的协程被唤醒,检查出现了
// error=2(timeout)错误并返回给应用程序,应用程序打印
// 错误输出为i/o timeout
go routine 0xc820001500 is woken up because of read timeout at time 1441023477954550830
go routine 0xc820001500 is woken up because of read timeout
go routine 0xc820001500 is woken up and got error: 2
Now: 1441023477, error:read tcp 127.0.0.1:8080->127.0.0.1:58005: i/o timeout
这个过程用图表示如下:
上图中read1成功,在等待read2时之前设置的定时器在read1和read2之间被触发。结果会导致read2出现超时失败。
这与我之前期望的行为不太一致:本期望read1成功后与之相关的定时器会被删除,这样如果我们在read2之前不再次设置socket超时时间,read2就会一直等待。
read前已经超时测试
服务器程序和客户端程序互相配合,服务器端对新来的连接(创建了新协程来处理该连接)设置超时时间1s,在超时时间后,客户端发送了数据,服务器端在读之前就已经超时,而且接下来每次读都会超时,无法接收到客户端的数据。 为了更直观地查看到服务器内部运行时信息,稍微hack了下go1.5的代码,加了一些打印语句,以下是服务器终端输出信息:
accept connection: &{conn:{fd:0xc820078000}}
set deadline 1441062762123875542 for mode 233
go routine 0xc820064300 is woken up because of read timeout at time 1441062762128411796
go routine 0xc820064300 is woken up and got error: 2
Now: 1441062762, error:read tcp 127.0.0.1:8080->127.0.0.1:58918: i/o timeout
Now: 1441062767, error:read tcp 127.0.0.1:8080->127.0.0.1:58918: i/o timeout
Now: 1441062772, error:read tcp 127.0.0.1:8080->127.0.0.1:58918: i/o timeout
可以看到,在客户端数据发送之前,服务器的连接就已经超时了,接下来的每次读都返回i/o timeout,即使客户端后来发送了数据,服务器端也无法响应。
总结
仔细思考了golang的网络超时实现,发现这好像与我们理解的socket超时不太一样。 golang实现的超时比较简单粗暴:在应用程序调用设置超时接口起开始启动一个定时器,一旦定时器到时,就将该socket设置为错误,接下来所有的读写都会失败。没有作更多精细化处理。
后记
看了个帖子,讨论了golang的网络超时的设计思想:不再设置socket层面的超时(因为该socket超时好像是给同步调用使用的)。而是设置一次请求的超时时间:所使用的SetReadDeadline()也就是这个意思。 引用一篇帖子内容:
具体的原因那个CL有说,简单的说,就是,SetTimeout(time.Second)的话,如果网络上的 数据一个字节一个字节的来的话,Read100字节的实际等待时间是100秒,这太容易导致误解。 为了让用户明确表明等待时间到底是指每次Read syscall的还是整个Read()调用的,所以改成了让用户提供一个绝对的时间。 如果任意一次Read syscall返回的时候超过了这个时间点,那么就算超时。 这样做的好处是,你可以在更高层次上设置Timeout,比如读取一个http请求的。deadline而不用管它的实现到底调用了多少次Read()。这样从上而下设置超时的方式都统一一点。
思考
使用操作系统的设置socket超时接口会有什么副作用?
setsockopt()设置SO_RCVTIMEO是针对blocking socket的,而golang则对所有的socket都使用了non-blocking方式,可参考nginx实现,也是non-blocking,使用红黑树来管理所有的socket超时;
传统的超时涵义是设置操作系统的SO_RCVTIMEO选项,这样每次应用程序的一次Read假如触发了操作系统的多次read syscall,那这个超时就可能会被放大很多倍,可参考上面的引用;