兼济天下则达 独善其身则穷
在用epoll提供telnet服务中说到,使用epoll提供telnet服务,用telnet登录后,然后输入命令,一切都很正常。只是由于一些原因,只能用nc命令,如echo stats | nc 192.168.168.128 9999
,这个命令如果在这台机器上运行,还是会返回正确的结果,然而当这个命令在其它机器上运行时,则有时候会返回结果,有时又不返回。于是用样的命令去测试memcache,不论是在本机上还是其它机器上,都可以返回结果。暂时想不到memcache是如何做到的,于是先用echo stats | nc -i 1 192.168.168.128 9999
来应付这个问题。可是这还不是最佳的解决办法,既然memcache能够做到,那么一定有解决的办法。
可喜的是,memcache是开源的,可以去看看它是如何解决这个问题的。打开memcached.c,文件不是一般的长,竟然有5000多行,慢慢找才知道memcache使用libevent,于是去了解libevent相关的知识,按照libevent官网和自带的示例程序,自己写了一个小例子运行,发现还是存在相同的问题。于是自己写了一个非常简单的单连接程序来取代epoll那一部分,可是还是存在相同的问题。问过同学之后,说是通过打印日志和tcpdump来查看网络包就一定可以找到。自己之前也用tcpdump查看过,客户端的确是有发数据的,服务端也收到了,可是为什么不显示收到的命令呢?依然没有解决我的问题,看来只有去《UNIX网络编程》中寻找问题的解答。
说实话,还真没有认认真真的看过《UNIX网络编程》这本书,因为之前对于网络编程总觉得异常繁琐,要调用socket,bind,listen,accept等函数才能建立一个链接,还要去填写sockaddr_in这个结构体,可是现在是工作,而且c就是这么底层,而网络编程也是很底层的,所以只好从头开始看起。此时对于TCP状态转换图才逐渐有些了解,没有想到啊,毕业5个多月后才知道这些,似乎有些晚了。难怪当初面试腾讯时,会被人鄙视说,许多工作的人都还能记得住这些转换图,你一个在校生怎么会不知道这些呢?其实当时很想和他说,这些都是工程实践很强的东西,没经常在这方面打滚,肯定不知道的,工作的人经常要接触这方面的东西,自然会记得住了。也罢,至少现在知道还不算晚。
看过书后,才知道有shutdown这样的东西,虽然以前看《UNIX环境编程》时,也看到过shutdown这个函数,可是没有写过实践代码,看过之后就忘记了。shudown的函数原型如下
#include int shutdown(int sockfd, int hoot);
返回:0--成功, -1--失败
该函数的行为依赖于howto参数的值。其中SHUT_WR选项,这个的意思是关闭连接写的这一半,当前留在套接口发送缓冲区的数据将被发送掉,后跟TCP的正常连接终止序列。用上这个,还是不行。
继续往下看,才知道有SO_LINGER这个选项,它使用linger结构体,如下:
struct linger { int l_onoff; /* 0 = off, nozero = on <em>/ int l_linger; /</em> linger time */ }lin;
当l_onoff是1,且l_linger是0时,close某个连接时,TCP将会丢弃保留在套接口发送缓冲区的任何数据并发送一个RST给对端。于是我回头看代码,建立连接这一段,我用的是公司封装过的函数,发现代码中将l_onoff设置为1,l_linger设置为0,于是我认为是找到了解决的办法,将l_onoff改为0就可以了。试过之后,发现客户端还是收不到数据,看来还存在其它问题。
书上有一个poll函数的例子,于是认真看看。先胡扯一下,网上都说,当epoll工作在水平触发模式时,则是一个高效的poll,可是他们都不给出一个epoll使用水平触发的例子,只是在那里将epoll工作在边沿触发模式的程序抄来抄去的,非常令人失望。而《UNIX环境编程》也只是给出poll的介绍,没有给出例子,看了之后还是很无厘头。还好俊杰同学留下这本书,真得感谢他。用上这个例子,测试之后,发现客户端是可以收到数据的,将这段代码去代替epoll那部分代码,发现依然可以收到,于是问题的范围就缩小了。
打印日志,终于找到问题的所在。首先说明一点,在边沿触发模式下,程序一直在while循环中read客户端发过来的命令,当返回-1,且errno设置为EAGAIN;而当读到结束符,也就是客户端发送FIN包过来时,read返回0。看代码中的处理,原来在代码中,是检查最后read的结果,而不是检查最后读到的字节总数,如果返回0的话,则关闭socket,不再去处理命令,那这里有什么问题呢?通过打印日志,发现如果将命令在与服务端同一台机器上执行,则会输出
n 6 nread -1
n 0 read 0
因为是在同一台机器上,所以两个包是单独处理的,这样就会给客户端返回命令后再关闭连接。而如果不是在同一台机器上,则输出就会发生变化。有时它会收到
n 6 read 0
此时已经从客户端读到命令的,同时也读到了结束符,在我的程序中就会直接关闭连接,这样就不会去处理命令,发送数据给客户端。而有时它会收到
n 6 nread -1
n 0 read 0
这时它就可以正常处理。这就可以解释为什么之前客户端有时可以收到反射,有时又收不到。而当在nc命令中加上-i 1后,客户端的FIN包将会在命令发送大约一秒钟后再发送,服务端就可以正常处理。
通过解决这个问题,了解许多网络的知识,受益良多。如果对于网络的知识不是很了解,调试的过程总不能得心应手,找不到合适的地方输出日志。如果当初能想到输出n和nread的值,也许就可以很快的找到问题的症结,只是没有想到这里。《UNIX网络编程》果然是本好书,在实践过程中来看这本书,才发现其中闪烁的真知见灼,都是来自于实践,有些怀念Richard Stevens了。另外tcpdump确实是个好东西,对于查看网络包,非常方便。
最终的代码见这里用epoll提供telnet服务的代码续