赞
踩
UDP是无连接面向报文协议,不保证可靠性尽最大努力交付(UDP对应用层交付的报文,不拆分也不合并,保留其边界,添加首部后就交付IP层)。
UDP没有拥塞控制,其首部只有8个字节,支持一对一、一对多、多对一、多对多的交互通信,因此网络出现的拥塞不会使源主机的发送速率较低。适用于源主机以恒定的速率发送数据,并且允许在网络发生拥塞时丢失一些数据,但却不允许数据有太大延迟(如IP电话、实时视频)。
UDP有两个字段:数据字段和首部字段。首部字段共8字节,源端口号、目的端口号、UDP用户数据报长度(最小为8,仅首部)、校验和(检测UDP用户数据报在传输是否有错)各部分2个字节。
伪首部(12个字节)临时添加在UDP数据报前,得到临时UDP数据报只用于计算校验和,既不向下传也不向上递交。
TCP是面向连接可靠交付的字节流协议。只允许点对点通信。应用程序在使用TCP协议之前,必须先建立TCP连接,再传送数据,完毕后必须释放建立的TCP连接。
面向字节流。“流是指流入到进程或从进程流出的字节序列”。发送端对一数据发送的次数和接受端接受的次数可以不等(你发10次,我可以分3次收,也可以等着一次性收完)。
(1)源端口和目的端口,各占2个字节。
(2)序号,4字节。TCP连接(建立时设置起始序号)中传送的字节流中的每一个字节都按顺序编号。
(3)确认号,4字节。期望收到对方下一个报文段的第一个数据字节的序号。
(4)数据偏移,4位。TCP报文段的数据起始处距离TCP报文段的起始的距离(即TCP首部长度,最大40字节)。
(5)保留,6位。今后使用,先为全0。
(6)紧急URG,为1时,此数据为紧急数据,插入本报文段数据最前面,与紧急指针字段配合使用。
(7)确认ACK。推送PSH。复位RST。同步SYN。终止FIN。
(8)窗口,2字节。窗口值值是接受方让发送方设置其发送窗口的依据。
(9)检验和,2字节。校验时同UDP一样首部和数据再加上伪首部计算校验和。
(10)紧急指针,2字节。
(11)选项,长度可变,最长为40字节。不使用选项时,TCP的首部长度是20字节。填充字段,使TCP首部是4字节的整数倍。
A 的 TCP 向 B 发出连接请求报文段,其首部中的 同步位 SYN = 1,并选择序号 seq = x,表明传送 数据时的第一个数据字节的序号是 x 。
B 的 TCP 收到连接请求报文段后,如同意,则发回确认。B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x + 1,自己选择的序号 seq = y。
A 收到此报文段后向 B 给出确认,其 ACK = 1,确认号 ack = y +1。A 的 TCP 通知上层应用进程,连接已经建立。
B 的 TCP 收到主机 A 的确认后,也通知其上层 应用进程:TCP 连接已经建立。
假设A发出第一个连接请求报文段,在某个网络结点长时间的滞留了,以至延误到A-B连接释放以后的某个时间才到B。本来这是一个早已失效的请求报文段,但B不知道,就误认为A再次发出一个新的连接请求,于是就向A发出确认,同意连接,并一直等待A发来数据。这样B的很多资源就白白浪费了。
数据传输结束后,通信的双方都可释放连接。现在 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的 FIN = 1,其序号 seq = u,等待 B 的确认。
B 发出确认,确认号 ack = u +1,而这个报文段自己的序号 seq = v。TCP 服务器进程通知高层应用进程。从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态。B 若发送数据,A 仍要接收。
若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。
A 收到连接释放报文段后,必须发出确认。
在确认报文段中 ACK = 1,确认号 ack = w + 1,自己的序号 seq = u + 1。
第一,为了保证 A 发送的最后一个 ACK 报文段能够到达 B。如果B没有收到A的确认报文会超时重传FIN+ACK报文段,而A就能在2MSL(最长报文段寿命时间)时间内收到这个重传的FIN+ACK报文段。如果A在TIME-WAIT状态不等待一段时间,那么就无法收到B的超时重传FIN+ACK报文段(不知道B没收到),进而不会向B发送ACK确认报文段,B就L无法CLOSED(傻等负心人)。
第二,防止 “已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL(最长报文段寿命时间),就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
当本端关闭了连接,恰好也同时收到了对方的FIN报文,此时可以把自己的FIN和给对方的确认ACK合在一起发送(中间两次合并),就成了3次。
当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。
从程序的角度,可以使用API来控制实现半连接状态。
- #include <sys/socket.h>
- int shutdown(int sockfd, int how);
- sockfd: 需要关闭的socket的描述符
- how: 允许为shutdown操作选择以下几种方式:
- SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
- 该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
- SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
- SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
在TCP网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测查询链接状态。常用的解决方法就是在程序中加入心跳机制。
Heart-Beat线程
这个是最常用的简单方法。在接收和发送数据时个人设计一个守护进程(线程),定时发送Heart-Beat包,客户端/服务器收到该小包后,立刻返回相应的包即可检测对方是否实时在线。
该方法的好处是通用,但缺点就是会改变现有的通讯协议!大家一般都是使用业务层心跳来处理,主要是灵活可控。
UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测,还是在业务层以心跳包做检测比较好,也方便控制。
SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个探测分节。对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟 15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为EHOSTUNREACH。
根据上面的介绍我们可以知道对端以一种非优雅的方式断开连接的时候,我们可以设置SO_KEEPALIVE属性使得我们在2小时以后发现对方的TCP连接是否依然存在。
- keepAlive = 1;
- setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
如果我们不能接受如此之长的等待时间,从TCP-Keepalive-HOWTO上可以知道一共有两种方式可以设置,一种是修改内核关于网络方面的 配置参数,另外一种就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三个选项。
- int keepIdle = 1000;
- int keepInterval = 10;
- int keepCount = 10;
-
- Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle));
- Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
- Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
SO_KEEPALIVE设置空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测。对于判断网络断开时间太长,对于需要及时响应的程序不太适应。
当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口!关联了完成端口的socket可能会忽略掉该套接字选项。
做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
itcast$ ./server
bind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
itcast$ netstat -apn |grep 6666
tcp 1 0 192.168.1.11:38103 192.168.1.11:6666 CLOSE_WAIT 3525/client
tcp 0 0 192.168.1.11:6666 192.168.1.11:38103 FIN_WAIT2 -
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。
现在用Ctrl-C把client也终止掉,再观察现象:
itcast$ netstat -apn |grep 6666
tcp 0 0 192.168.1.11:6666 192.168.1.11:38104 TIME_WAIT -
itcast$ ./server
bind error: Address already in use
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
- int opt = 1;
- setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
注意:程序中设置某个端口重新使用,在这之前的其他网络程序将不能使用这个端口
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。