getty 开发日志

692 阅读7分钟
原文链接: alexstocks.github.io

getty 开发日志


written by Alex Stocks on 2018/03/19

0 说明


getty是一个go语言实现的网络层引擎,可以处理TCP/dup/websocket三种网络协议。

2016年6月我在上海做一个即时通讯项目时,接口层的底层网络驱动是当时的同事sanbit写的,原始网络层实现了TCP Server,其命名规范学习了著名的netty。当时这个引擎比较简洁,随着我对这个项目的改进这个网络层引擎也就随之进化了(添加了TCP Client、抽象出了 TCP connection 和 TCP session),至2016年8月份(又添加了websocket)其与原始实现已经大异其趣了,征得原作者和相关领导同意后就放到了github上。

将近两年的时间我不间断地对其进行改进,年齿渐增但记忆速衰,觉得有必要记录下一些开发过程中遇到的问题以及解决方法,以备将来回忆之参考。

1 UDP connection


2018年3月5日 起给 getty 添加了UDP支持。

1.1 UDP connect


UDP自身分为unconnected UDP和connected UDP两种,connected UDP的底层原理见下图。

当一端的UDP endpoint调用connect之后,os就会在内部的routing table上把udp socket和另一个endpoint的地址关联起来,在发起connect的udp endpoint端建立起一个单向的连接四元组:发出的datagram packet只能发往这个endpoint(不管sendto的时候是否指定了地址)且只能接收这个endpoint发来的udp datagram packet(如图???发来的包会被OS丢弃)。

UDP endpoint发起connect后,OS并不会进行TCP式的三次握手,操作系统共仅仅记录下UDP socket的peer udp endpoint 地址后就理解返回,仅仅会核查对端地址是否存在网络中。

至于另一个udp endpoint是否为connected udp则无关紧要,所以称udp connection是单向的连接。如果connect的对端不存在或者对端端口没有进程监听,则发包后对端会返回ICMP “port unreachable” 错误。

如果一个POSIX系统的进程发起UDP write时没有指定peer UDP address,则会收到ENOTCONN错误,而非EDESTADDRREQ。

一般发起connect的为 UDP client,典型的场景是DNS系统,DNS client根据/etc/resolv.conf里面指定的DNS server进行connect动作。

至于 UDP server 发起connect的情形有 TFTP,UDP client 和 UDP server 需要进行长时间的通信, client 和 server 都需要调用 connect 成为 connected UDP。

如果一个 connected UDP 需要更换 peer endpoint address,只需要重新 connect 即可。

1.2 connected UDP 的性能


connected UDP 的优势详见参考文档1。假设有两个 datagram 需要发送,unconnected UDP 的进行 write 时发送过程如下:

* Connect the socket
* Output the first datagram
* Unconnect the socket
* Connect the socket
* Output the second datagram
* Unconnect the socket

每发送一个包都需要进行 connect,操作系统到 routine table cache 中判断本次目的地地址是否与上次一致,如果不一致还需要修改 routine table。

connected UDP 的两次发送过程如下:

* Connect the socket
* Output first datagram
* Output second datagram

这个 case 下,内核只在第一次设定下虚拟链接的 peer address,后面进行连续发送即可。所以 connected UDP 的发送过程减少了 1/3 的等待时间。

2017年5月7日 我曾用 python 程序 对二者之间的性能做过测试,如果 client 和 server 都部署在本机,测试结果显示发送 100 000 量的 UDP datagram packet 时,connected UDP 比 unconnected UDP 少用了 2 / 13 的时间。

这个测试的另一个结论是:不管是 connected UDP 还是 unconnected UDP,如果启用了 SetTimeout,则会增大发送延迟。

1.3 Go UDP


Go 语言 UDP 编程也对 connected UDP 和 unconnected UDP 进行了明确区分,参考文档2 详细地列明了如何使用相关 API,根据这篇文档个人也写一个 程序 测试这些 API,测试结论如下:

* 1 connected UDP 读写方法是 Read 和 Write;
* 2 unconnected UDP 读写方法是 ReadFromUDP 和 WriteToUDP(以及 ReadFrom 和 WriteTo);
* 3 unconnected UDP 可以调用 Read,只是无法获取 peer addr;
* 4 connected UDP 可以调用 ReadFromUDP(填写的地址会被忽略)
* 5 connected UDP 不能调用 WriteToUDP,”即使是相同的目标地址也不可以”,否则会得到错误 “use of WriteTo with pre-connected connection”;
* 6 unconnected UDP 不能调用 Write, “因为不知道目标地址”, error:”write: destination address requiredsmallnestMBP:udp smallnest”;
* 7 connected UDP 可以调用 WriteMsgUDP,但是地址必须为 nil;
* 8 unconnected UDP 可以调用 WriteMsgUDP,但是必须填写 peer endpoint address。

综上结论,读统一使用 ReadFromUDP,写则统一使用 WriteMsgUDP。

1.4 Getty UDP


版本 v0.8.1 Getty 中添加 connected UDP 支持时,其连接函数 dialUDP 这是简单调用了 net.DialUDP 函数,导致昨日(20180318 22:19 pm)测试的时候遇到一个怪现象:把 peer UDP endpoint 关闭,local udp endpoint 进行 connect 时 net.DialUDP 函数返回成功,然后 lsof 命令查验结果时看到确实存在这个单链接:

COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
echo_clie 31729 alex    9u  IPv4 0xa5d288135c97569d      0t0  UDP localhost:63410->localhost:10000

然后当 net.UDPConn 进行 read 动作的时候,会得到错误 “read: connection refused”。

于是模仿C语言中对 TCP client connect 成功与否判断方法,对 dialUDP 改进如下:

* 1 net.DialUDP 成功之后,判断其是否是自连接,是则退出;
* 2 connected UDP 向对端发送一个无用的 datagram packet【”ping”字符串,对端会应其非正确 datagram 而丢弃】,失败则退出;
* 3 connected UDP 发起读操作,如果对端返回 “read: connection refused” 则退出,否则就判断为 connect 成功。

2 Compression


去年给 getty 添加了 TCP/Websocket compression 支持,Websocket 库使用的是 gorilla/websocketGo 官网也推荐这个库,因为自 This package("golang.org/x/net/websocket") currently lacks some features

2.1 TCP compression


最近在对 Websocket compression 进行测试的时候,发现 CPU 很容易就跑到 100%,且程序启动后很快就 panic 退出了。

根据 panic 信息提示查到 gorilla/websocket/conn.go:ReadMsg 函数调用 gorilla/websocket/conn.go:NextReader 后就立即 panic 退出了。panic 的 表层原因 到是很容易查明:

但是为何发生读超时错误则毫无头绪。

2018/03/07 日测试 TCP compression 的时候发现启动 compression 后,程序 CPU 也会很快跑到 100%,进一步追查后发现函数 getty/conn.go:gettyTCPConn::read 里面的 log 有很多 “io timeout” error。当时查到这个错误很疑惑,因为我已经在 TCP read 之前进行了超时设置【SetReadDeadline】,难道启动 compression 会导致超时设置失效使得socket成了非阻塞的socket?

于是在 getty/conn.go:gettyTCPConn::read 中添加了一个逻辑:启用 TCP compression 的时不再设置超时时间【默认情况下tcp connection是永久阻塞的】,CPU 100% 的问题很快就得到了解决。

至于为何 启用 TCP compression 会导致 SetDeadline 失效使得socket成了非阻塞的socket,囿于个人能力和精力,待将来追查出结果后再在此补充之。

2.2 Websocket compression


TCP compression 的问题解决后,个人猜想 Websocket compression 程序遇到的问题或许也跟 启用 TCP compression 会导致 SetDeadline 失效使得socket成了非阻塞的socket 有关。

于是借鉴 TCP 的解决方法,在 getty/conn.go:gettyWSConn::read 直接把超时设置关闭,然后 CPU 100% 被解决,且程序运转正常。

总结


本文总结了 getty 近期开发过程中遇到的一些问题,囿于个人水平只能给出目前自认为最好的解决方法【如何你有更好的实现,请留言】。

随着getty若有新的 improvement 或者新 feature,我会及时补加此文。

此记。

参考文档


扒粪者-于雨氏

于雨氏,2018/03/19,初作此文于帝都海淀西二旗。

Gitalking ...