TCP建立和释放原理和需要注意的问题

来源:互联网 发布:c语言编写的聊天程序 编辑:程序博客网 时间:2024/06/18 07:13

I.TCP报文:

各个段位说明:

    源端口和目的端口:  各占 2 字节.端口是传输层与应用层的服务接口.传输层的复用和分用功能都要通过端口才能实现

    序号: 占 4 字节.TCP 连接中传送的数据流中的每一个字节都编上一个序号.序号字段的值则指的是本报文段所发送的数据的第一个字节的序号

    确认号:  占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号

    数据偏移/首部长度: 占 4 ,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远.

    保留:  占 6 ,保留为今后使用,但目前应置为 0

    紧急URG: 当 URG=1 ,表明紧急指针字段有效.它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)

    确认ACK: 只有当 ACK=1 时确认号字段才有效. ACK=0 ,确认号无效

    PSH(PuSH):  接收 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付

    RST (ReSeT):  当 RST=1 ,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接

    同步 SYN: 同步 SYN = 1 表示这是一个连接请求或连接接受报文

    终止 FIN: 用来释放一个连接.FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接

    检验和:  占 2 字节.检验和字段检验的范围包括首部和数据这两部分.在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部

    紧急指针:  占 16 ,指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)

    选项: 长度可变.TCP 最初只规定了一种选项,即最大报文段长度 MSS.MSS 告诉对方 TCP:“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节. [MSS(Maximum Segment Size) TCP 报文段中的数据字段的最大长度. MSS加上 TCP 首部才等于整个的 TCP 报文段]

    填充: 这是为了使整个首部长度是 4 字节的整数倍

    其他选项:

        窗口扩大:  占 3 字节,其中有一个字节表示移位值 S.新的窗口值等于TCP 首部中的窗口位数增大到(16 + S),相当于把窗口值向左移动 S 位后获得实际的窗口大小

        时间戳:  占10 字节,其中最主要的字段时间戳值字段(4字节)和时间戳回送回答字段(4字节)

        选择确认:  接收方收到了和前面的字节流不连续的两2字节.如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。

II. TCP建立和释放连接的状态图:

tcp状态:

LISTEN:侦听来自远方的TCP端口的连接请求

SYN-SENT:再发送连接请求后等待匹配的连接请求

SYN-RECEIVED:再收到和发送一个连接请求后等待对方对连接请求的确认

ESTABLISHED:代表一个打开的连接

FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认

FIN-WAIT-2:从远程TCP等待连接中断请求

CLOSE-WAIT:等待从本地用户发来的连接中断请求

CLOSING:等待远程TCP对连接中断的确认

LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认

TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认

CLOSED:没有任何连接状态

III. TCP建立之三次握手(都要得到对方确认服务器开销昂贵):

Client socket ConnectTCP层,启用同步位SYN字节来建立连接, Seq是报文数据部分第一字节的序号,AckSeq是对方的下一个报文;主动连接端会从Close->SYN_SENT->ESTABLISHED状态,接收端会从LISTEN->SYN_RCVD->ESTABLISHED状态;之所以要三次握手是双方都要发出并得到对方的回复确认才建立连接(后一次是因为服务器的开销是宝贵的,要再次确认下,避免和已经有网络问题的客户端建立连接)

  1. C->S: SYN = 1,  Seq = x
  2. S->C: SYN = 1, Seq = y, AckSeq = x + 1
  3. C-S: ACK = 1, Seq = x + 1, AckSeq = y + 1

ClientConnect函数返回成功,可以Send,Recv数据了;服务器的Accept成功,可以Recv,Send数据了。

server端的socket调用bindlisten之后,监听套接字的状态就会变为LISTEN状态,监听套接字的工作决定了它只是监听连接。server端调用accept,相当于从套接字的完成队列中取出一个client的连接,此时可以确定四元组,即:clientIPportserverIPport,双方各有一个套接字进行交互,这里需要说一下server端的socket,我称serveraccept后产生的套接字为会话套接字,这个套接字一出生的状态就是ESTABLISHserver端得四元组我们可以看出,一个server从理论上可以接收的连接数量取决于文件描述符的个数。

 

IV. TCP释放之四次挥手(全双工的只能先关闭主动到被动端的,然后关闭被动端到主动端的):

记得被动关闭端Close_Wait状态后需要关闭主动端的socket.主动关闭端收到FIN报文后进入Time_Wait状态确保发送关闭了且避免重新建立的连接使用了收发缓存导致问题,如果主动关闭端是服务器,那么需要setsockopt(x,x,SO_REUSEADDR,x,x).

FIN是终结标志位,ACK是确认标志位,Seq是报文序列号,AckSeqAck序号。

客户端和服务器端,都有权利进行close,主动close的将会进入FIN_WAIT_1不再发送数据, FIN_WAIT_2, Time_Wait不再接收数据,Close状态。

被动的会进入Close_Wait不再发送数据,过一段时间后需要应用层调用close也发送FIN给主动端, 自己进入Lask_Ack不再接收数据, Close状态。

 主动端Socket close后,且被动端也Socket close后,Tcp层:

  1. 主动方调用close FIN = 1, Seq =x,主动方不再向被动方发送数据了,但是连接还没有完全断开,剩下的数据还可以发送只是序列号不变发送TCP队列里面的数据。
  2. 被动方TcpAck很快:ACK = 1, Seq = y, AckSeq = x+1; 主动方收到ACK进入FIN_WAIT_2完成断开,不能再发送数据了,但是可以接收数据,被动方进入CLOSE_WAIT
  3. 被动方recv返回SOCKET_ERROR需要应用层进行close,FIN = 1, Seq = z, Ack Seq= x + 1 发送给主动端,主动端进入TIME_WAIT状态,被动端进入LAST_ACK
  4. 主动方收到FIN,Tcp层确认关闭:ACK = 1, Seq = x + 1, AckSeq = 1003, 被动端收到ACK进入CLOSED状态;主动端还不会关闭会TIME_WAIT 2MSL时间。

 

V. 注意:

  1. CLOSE_WAIT状态

CLOSE_WAIT是被动关闭端收到FIN后进入的状态,需要应用层收到SOCKET_ERROR后也调用close()才能顺利的进入LAST_ACK进而CLOSE

处理方法:无论是服务端还是客户端程序员都要保证recvSOCKET_ERROR后进行socket close()

// 你的程序正在recv,不管是服务端还是客户端,都应该收到SOCKET_ERROR时候,都要关闭socket

int nRet = recv(s,....);

if (nRet == SOCKET_ERROR) // SOCKET_ERROR == 0

{

// closesocket(s);

return FALSE;

}

很多人就是忘记了那句closesocket,这种代码太常见了。

 

  1. TIME_WAIT状态

主动关闭方进入Time_Wait状态,2Max Segment LifeTime时间2-4分钟,在这期间不能重新绑定端口建立连接:

需要TIME_WAIT的原因:

  1. 保证最后的Ack发送成功了,会启动计时器可能收到未收到报文会重发。
  2. 锁定IP+端口一段时间,避免又绑定了再使用该发送接受缓存,导致连接混乱。

处理方法:client程序可connect时候随机分配一个其它端口,server 程序总是应该在调用 bind() 之前设置setsockoptSO_REUSEADDR 套接字选项还有更多SO_LINGERSO_KEEPALIVE设置选项

比如:

int opt =  1;

 if ( setsockopt(sListen, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)) < 0 )

 {

  printf("setsockopt Failed.\n");

  return false;

 }

so_linger会降低服务器性能

so_linger用来保证对方收到了close时发出的消息,即,至少需要对方通过发送ACK且到达本机。

怎么保证呢?等待!close会阻塞住进程,直到确认对方收到了消息再返回。然而,网络环境又得复杂的,如果对方总是不响应怎么办?所以还需要l_linger这个超时时间,控制close阻塞进程的最长时间。

注意,务必慎用so_linger,它会在不经意间降低你程序中代码的执行速度(close的阻塞)。

 

  1. 网络大端顺序
大端小端转换,网络是字节大端存储顺序:
windows 中有 htons  ntohs  htonl  ntohl 等一套函数,分别用来完成 2 个字节和 4 个字节的转换。

 

拓展:

1)Linux Closeshutdown的区别:
linux多线程下关闭socketcloseshutdown都一样;多进程下,close只是关闭引用当引用计数为0时才真正关闭,shutdown却会直接关闭掉。
2)查看系统TCP连接资源命令
netstat 
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
3)Socket门面设计模式:
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
在创建 Socket 实例的构造函数正确返回之前,将要进行 TCP 的三次握手协议,TCP 握手协议完成后,Socket 实例对象将创建完成,否则将抛出 IOException 错误。与之对应的服务端将创建一个 ServerSocket 实例。注意这时服务端与之对应的 Socket 实例并没有完成创建,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。


1 0
原创粉丝点击