一.TCP简介
TCP提供一种面向连接的,可靠的字节流服务。面对连接意味着两个使用TCP的应用,在彼此交换数据之前必须先建立一个连接。TCP通过以下方式提供可靠性:
1. 应用数据被分割成TCP认为最适合发送的数据块,由TCP传递给IP的信息单位成为报文段。
2. 当TCP发出一个段后,它就启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
3. 当TCP收到TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,而是延迟发送的。
4. TCP将保持对它首部和数据的校验和,这是一个端到端的校验和,目的是检测数据在传输在传送过程中的任何变化。如果收到段得检验和有错误,TCP将丢弃这个报文段和不确认收到报文段。
5. TCP将对收到的数据进行重新排序,将收到的数据以正确的方式交给应用层。
6. TCP的接收端必须丢弃重复的数据
7. TCP还能提供流量控制,TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。
二. TCP状态迁移路线图
下面的文章按照client/server两条路线讲述TCP状态迁移路线图。
1. 连接建立
1) Client
当Client端调用socket函数调用时,相当于Client端产生了一个处于Closed状态的套接字。
Client端又调用connect函数调用,系统为Client随机分配一个端口,连同传入connect中的参数(Server的IP和端口),这就形成了一个连接四元组,connect调用让Client端的socket处于SYN_SENT状态
当Server返回确认,并发送SYN,Client返回确认及SYN后,套接字处于ESTABLISHED阶段,此时双方的连接已经可以进行读写操作
2)Server
当Server端调用socket函数调用时,相当于Server端产生了一个处于Closed状态的监听套接字
Server端调用bind操作,将监听套接字与指定的地址和端口关联,然后又调用listen函数,系统会为其分配未完成队列和完成队列,此时的监听套接字可以接受Client的连接,监听套接字状态处于LISTEN状态。
当Server端调用accept操作时,会从完成队列中取出一个已经完成的client连接,同时在server这段会产生一个会话套接字,用于和client端套接字的通信,这个会话套接字的状态是ESTABLISH。
2. 连接关闭
与连接建立分为server/client不同,连接关闭并没有绝对的server/client之分,连接关闭分为主动关闭和被动关闭。Server和client都可以担任这两个角色中的任意一个。如client可以关闭它与server的连接,同样的server一样也可以关闭一些长时间无读写事件发生的连接。既然这么说了,下面就会分成两个部分:client主动关闭,server主动关闭。
Client主动关闭,Server被动关闭:
Client主动关闭,Server被动关闭的情况还是蛮多的,比如说短连接中,当一次会话结束时,client就可以关闭它与server之间的连接。我们这边直接说close而非shutdown。
当client想要关闭它与server之间的连接,首先client这边会首先调用close函数,client端会发送一个FIN到server端,client端处于FIN_WAIT1状态。当server端返回给client ACK后,client处于FIN_WAIT2状态,server处于CLOSE_WAIT状态。
当server端检测到client端的关闭操作(read返回为0),server端也需要调用close操作,server端会向client端发送一个FIN。此时server的状态为LAST_ACK,当client收到来自server的FIN后,client端的套接字处于TIME_WAIT状态,它会向server端再发送一个ack确认,此时server端收到ack确认后,此套接字处于CLOSED状态。
Server端主动关闭的流程与Client端关闭类似,就不再多讲,下面还需要关注的是TIME_WAIT这个状态,分别按照client/server两个部分讲述。
首先说一下TCP/IP详解中描述的关于TIME_WAIT的描述及其存在的必要性:
主动关闭的socket当收到对端的FIN操作后,该socket就会处于TIME_WAIT状态,处于TIME_WAIT状态的socket会存活2MSL(Max Segment Lifetime),之所以存活这么长时间是有理由的:
一方面是可靠的实现TCP全双工连接的终止,也就是当最后的ACK丢失后,被动关闭端会重发FIN,因此主动关闭端需要维持状态信息,以允许它重新发送最终的ACK。
另一方面TCP在2MSL等待期间,定义这个连接(4元组)不能再使用,任何迟到的报文都会丢弃。设想如果没有2MSL的限制,恰好新到的连接正好满足原先的4元组,这时候连接就可能接收到网络上的延迟报文就可能干扰最新建立的连接。
当Client主动关闭时,正常情况下client的socket会经历TIME_WAIT的状态
连接在关闭的时候,双方都会有状态的迁移,这就要求我们的程序中一定要有比较完全的
3. Server端的监听套接字与会话套接字的不同:
当server端的socket调用bind和listen之后,监听套接字的状态就会变为LISTEN状态,监听套接字的工作决定了它只是监听连接。
当server端调用accept,相当于从套接字的完成队列中取出一个client的连接,可以确定四元组,即:client的IP和port;server的IP和port,双方各有一个套接字进行交互,这里需要说一下server端的socket,我称server端accept后产生的套接字为会话套接字,这个套接字一出生的状态就是ESTABLISH
由server端得四元组我们可以看出,一个server从理论上可以接收的连接数量取决于文件描述符的个数
4. 套接字关闭
TIME_WAIT状态的目的是为了防止最后a发出的ack丢失,让b处于LAST_ACK超时重发FIN
![](http://img.blog.163.com/photo/XFNevno1zI6TqasoTBNYNg==/3998914994128745470.jpg)
所以说,主动发起关闭连接的一方会进入time_wait状态,这个时候,进程所占用的端口号不能被释放。除非在你的程序中用setsockopt设置端口可重用(SOCK_REUSE)的选项,但这不是所有操作系统都支持的
解决TIME_WAIT的办法主要有以下几种:
1、修改LINGER值,缩短关闭时间
LINGER lingerStruct;
lingerStruct.l_onoff = 1;
lingerStruct.l_linger = 0;
setsockopt(m_socket,SOL_SOCKET,SO_LINGER,(char *)&lingerStruct,sizeof(lingerStruct));
不过这种办法不是很安全的,不过现在网络都很好啦,不会有问题的。
2、修改注册表
[HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters]
"TcpTimedWaitDelay"=dword:00000005
这个值好像是300秒到30秒之间,改成30秒后你会发现TIME_WAIT很快就会消失了。
3、禁用LINGER
//如果你使用的是Socket API,可以这样
BOOL bDontLinger=FALSE;
setsockopt(m_socket,SOL_SOCKET,SO_DONTLINGER,(LPCTSTR)&bDontLinger,sizeof(BOOL));
closesocket(s);
//如果你使用的是CAsyncSocket,需要响应的修改,例如禁用LINGER可以这样
BOOL bDontLinger=FALSE;
m_socket->SetSockOpt(SO_DONTLINGER,(const char *)&bDontLinger,sizeof(bDontLinger),SOL_SOCKET);
m_socket->Close();
4、客户端可以不BIND(),这样,即使断开连接后再次连接,SOCKET将使用不同的端口(1025-5000),
等几分钟后,原有的端口就会自动关闭。
关闭BITCOMET后系统出现的几个TCP状态
![](http://img.blog.163.com/photo/xmSWtahOMKwoXzTuJeWpBw==/3677189095748834455.jpg)
![](http://img.blog.163.com/photo/NscYWk5l12d4AEn-QLkPNA==/3677189095748834456.jpg)