一.TCP
TCP/IP协议栈中传输层的典型协议:TCP(Transmission Control Protocol ):传输控制协议
特性: 面向连接,可靠传输,提供字节流服务
协议字段信息:
- 16位源端口号,16位目的端口号:负责端与端之间的数据传输
- 32位序号/确认序号: 进行协议栈中的包序管理+其它功能
- 4位首部长度: 单位:4个字节 TCP头部最大长度:15(8+4+2+1)*4 = 60字节 TCP头部最小长度:20字节
- 6位保留
- 6位标志位:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从缓冲区将数据读走
- RST:对方请求重新建立连接 把携带RST标识的称为 复位报文段
- SYN:请求建立连接 把携带SYN标识的称为 同步报文段
- FIN:通知对方,本方要关闭了 把携带FIN标识的称为 结束报文段
- 16位窗口大小: 作用+功能
- 16位检验和:检验数据一致性
- 16位紧急指针:带外数据(标识哪部分是紧急指针)
- 49字节选项数据:用到的时候才有,暂时忽略
二.TCP网络通信
服务端: 1.创建套接字 2.绑定地址信息 3. 开始监听 4.获取新连接 5. 使用获取的新连接与指定客户端进行通信 6.关闭套接字连接
客户端: 1. 创建套接字 2. 向服务端发起连接请求 3.收发数据 4 关闭套接字连接
创建套接字:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域 : AF_INET ipv4
type:套接字类型
SOCK_STREAM:流式套接字 对应的协议:TCP
SOCK_AGRAM:数据报套接字 对应的协议:UDP
protocol:
0:套接字类型默认协议
IPPROTO_TCP : 6
IPPROTO_UDP : 17
返回值:文件描述符:套接字操作句柄
绑定地址信息:
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
向服务端发起三次连接请求:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
开始监听:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog:指定未完成连接队列的最大长度
在TCP建立连接阶段,内核维护着两个队列:
未完成队列 这是客户端发送SYN过来,服务器回应SYN+ACK之后,服务器当前处于SYN_RECV状态,此时的连接在未完成队列中。
完成队列 客户端回应ACK之后,两边都处于ESTABLISHED状态,此时连接从未完成队列移到完成队列中,服务器调用accept,就从完成队列移除并返回给程序。
假如指定了一个很小的backlog,比如1,那么完成队列很容易就满,满了以后客户端连接进来会怎么样呢?从上面可知,客户端connect还是成功返回,但是服务器这个连接进不了完成队列,一段时间后被内核释放了,服务器就没有办法通过accept得到连接。
这时就出现这样的现象:
客户端连接成功并得到socket fd,但服务器没有相应的fd。
客户端执行read,服务器停止了,客户端的read没有办法返回0,还是一直阻塞着。
所以,在编写服务器代码时要注意:
backlog要指定合适的值,Linux一般是128,通常用这个就可以了。
尽早accept新的连接,防止完成队列满了,新连接没办法被accept。
获取已完成新连接:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
发送和接收数据:
#include <sys/socket.h>
int send(int sockfd, const void *msg, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
关闭套接字:
int close(int sockfd);
三.面向连接
三次握手:
-
向服务端发送SYN包连接请求,进入SYN_SENT状态
-
服务端收到客户端发送的SYN包连接请求后,向客户端回复SYN+ACK包,进入SYN_REVD状态
-
客户端收到服务端回复的SYN+ACK包后,向服务端回复ACK包,进入ESTABLISHED状态
-
服务端收到客户端回复的ACK包后,同样进入ESTABLISHED状态
四次挥手:
-
主动关闭方不再发送信息,则向被动关闭方发送FIN包请求断开连接,进入FIN_WAIT1状态
-
被动关闭方收到收到主动关闭方发送的FIN包请求断开连接后,向主动关闭方回复ACK包,进入CLOSE_WAIT状态
-
主动关闭方收到被动关闭方的ACK包回复后,进入FIN_WAIT2状态
-
等被动关闭方不再发送信息,则向主动关闭方发送FIN包请求断开连接,进入LAST_ACK状态
-
主动关闭方收到被动关闭方发送的FIN包请求断开连接后,向被动关闭方回复ACK包,进入TIME_WAIT状态,再等待2个MSL(报文的最大生存周期)后,进入CLOSED状态.
-
被动关闭方收到主动关闭方回复的ACK包后,进入CLOSED状态
补充:
MSL(报文最大生存周期): 在Windows中时间为2分钟,Linux系统中时间为1分钟,UNinx系统中时间为30秒.
shutdown()函数与close()函数的区别:
shutdown()函数可以选择关闭全双工连接的读通道或者写通道,如果两个通道同时关闭,则这个连接不能再继续通信。
close()函数会同时关闭全双工连接的读写通道,除了关闭连接外,还会释放套接字占用的文件描述符。而shutdown()只会关闭连接,但是不会释放占用的文件描述符。所以即使使用了SHUT_RDWR类型调用shutdown()关闭连接,也仍然要调用close()来释放连接占用的文件描述符。
相关问题:
1.握手为什么是三次而不是三次或者四次呢?
TCP是面向连接的,通信两端都必须确认对端具有数据收发能力.两次不安全,四次没必要.
客户端主动发送SYN包建立连接,服务端收到客户端发送的SYN包则回复SYN+ACK包.客户端收到ACK包时,就可以证明自己具有收据收发能力.但是服务端此时并没有确认是否有数据收发能力.此时客户端回复ACK给服务端,服务端收到ACK时,则表示自己也具有数据收发能力.
2.挥手为什么是四次而不是三次呢?
收到FIN包只能表示对方不再发送数据,不表示对方不再接受数据.因此被动关闭方收到断开连接请求并进行回复后,还可以继续发送数据.只有确认自己也不再发送数据的时候,才会给对方发送FIN包断开连接请求,主动关闭方收到FIN包后再回复ACK.因此挥手是四次.
3.TIME_WAIT有什么用?不要TIME_WAIT可以吗?
-
如果没有TIME_WAIT,就会直接CLOSED.而CLOSED最终会释放socket,则不再占用地址和端口信息.
若是立即启动一个客户端,使用相同的地址和端口信息:
假如上一次挥手时最后一次的ACK回复丢失,则服务端处于LAST_ACK状态.(超时重传FIN包)
-
客户端发送建立连接请求时发送的SYN包会被服务端认为状态错误,重置连接.
-
客户端有可能收到服务端重发的FIN包
因此使用相同地址启动客户端,有可能会对新连接造成影响.
-
-
等待的作用:
-
对重传的FIN包进行处理
-
等待重传的数据不会对新连接造成影响
-
-
等待多长时间比较合适?
2个MSL(报文的最大生存周期), MSL:一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。
-
TIME_WAIT用来保护客户端的(服务端通常就算重启也必须使用相同的地址信息)
解决方案: 可以调整TIME_WAIT等待时间,也可以设置套接字选项(地址重用)
设置套接字选项:地址重用
int setsocketopt(int sockfd,int level,int optname,const void *optval,socklen_t len);
sockfd:套接字描述符
level:SOL_SOCKET
optname:SO_REUSEADDR
optval:int opt = 1 开启地址重用选项
optlen:sizeof(opt)
4.一台主机出现了大量的CLOSE_WAIT状态的socket是什么原因?
CLOSE_WAIT是被动关闭方收到FIN包请求进行回复后出现的状态,会在调用close或者shutdown之后进入下一个状态LAST_ACK,因此一旦一台主机出现了大量的CLOSE_WAIT状态的socket则表示你的程序中可能在对方关闭连接时没有关闭套接字释放资源.这是一种编程上存在的错误,连接断开但是没有关闭套接字,释放资源.
5.一台主机上出现大量的TIME_WAIT状态的socket是什么原因?
TIME_WAIT是主动关闭方出现的状态,因此一台主机上出现大量的TIME_WAIT,意味着这台主机上进行了大量的套接字主动关闭,常见于爬虫服务器.
TCP协议栈中的保活机制(心跳包):
默认情况下,通信双方(7200s=2个小时)没有数据往来,则每隔一段时间(75s)向对方发送一个保活探测数据包,要求对方得到响应.
若是得到响应,则认为连接正常.
若是连续9次没有得到响应,则认为连接断开,则将socket状态置为CLOSE_WAIT
连接断开对于编程的影响:
连接断开,则recv返回0,不再阻塞.连接断开,如果继续send就会触发异常(SIGPIPE),导致进程退出.
当recv返回0的时候则认为连接断开,可以关闭套接字.如果连接断开不想因为send发送数据而导致进程退出,可以自定义SIGPIPE信号处理函数.
四.可靠传输
-
面向连接(大前提):确认通信双方都具有数据收发能力.
-
确认应答机制:发送的每一条数据都要求接收方进行确认回复,收到确认则认为数据安全到达
-
超时重传机制:等待超时后都没有收到数据的确认回复,则认为数据丢失,对这条数据进行重传
-
包序管理:基于协议字段中的序号/确认序号实现(三次握手阶段,双方会告诉对方自己的起始序号是多少)
-
数据不一定会按序到达,但是接收方会根据序号进行数据排序,保证有序交付
-
确认序号:接收方告诉发送方,确认序号之前的数据都已经收到了,下次就从这个确认序号开始发送数据
-
序号:告诉对方本条数据的起始序号是多少
-
-
数据一致性校验:基于协议字段中的检验和字段,不一致则丢弃要求重传.
如何避免丢包?
流量控制:TCP支持根据接收端的处理能力,来决定发送端的发送速度.
问题:避免因为发送方发送数据过快过多,导致接收方缓冲区溢出,数据无处存放直接丢弃的丢包.
滑动窗口机制: 接收方每接收到一条数据,就会通过协议字段中的窗口大小字段告诉发送方最多继续可以发送多少数据(如果为0表示不再发送数据).通常协议字段中的窗口大小字段中的值,不会大于当前接收缓存区中的剩余空间大小.
流量控制的实现:接收方通过每次收到的数据后的确认回复向对端传递窗口大小,限制对方最多能发送的数据,避免发送的数据过多,导致接收缓存区慢溢后数据丢失.
对于发送方来说:后沿的移动取决于是否收到ACK确认回复,前沿的移动取决于对端回复的窗口大小.
对于接收方来说:后沿的移动取决于是否收到数据,前沿的移动取决于接收方的接收缓存区剩余空间.
窗口只是序号的一种虚拟概念.
滑动窗口中的协议:
-
停等协议:每一条数据收到回复后才会发送下一条,应用于网络状况极差的情况
-
回退N步协议:从丢包位置开始往后的数据重传进行传输,专用于网络一般的情况
-
选择重传协议:哪个包丢就重传哪一个,用于网络较好的情况
拥塞控制:
问题:避免发送数据时,一开始不知道网络的状况,万一网不好,发的越多丢的就越多.
解决方法:一种慢启动块增长(指级数的增长)的形式进行传输(慢启动就是为了在起始阶段进行网络探测)
实现:发送方维护一个拥塞窗口(当前发送数据大小的限制量).
每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送的窗口.
如何提高性能(挽回更多的性能损失)?
快速重传机制:
问题:任意一条数据丢失,发送方都会等待一个超时后才会进行重传,效率较低.
解决方法:因此 接收方若是在前边数据没有收到的情况下,收到了后边的数据,则认为前边的数据有可能丢失,则直接要求发送方对前面数据进行重传.
将前边数据的重传请求连续发送三次,发送方收到三次连续的重传请求,则对数据进行重传.
连续三次重传请求才会进行重传的原因:为了避免数据并没有丢失,只是延迟到达.
捎带应答机制:接收方会为每一次收到的数据进行确认回复,但是每一条确认回复都只是报头中的一些信息(有可能每一次回复都只是一个单纯的TCP报头的传输),如果在进行确认的时候,刚好有数据要发送给对方,那就顺便把确认回复信息和要发送的数据集成为一条数据进行发送.
延迟应答机制: 当接收方为每一次收到的数据进行确认回复的时候,大概率这条数据还在缓冲区中,没有被取出,则窗口会变小,传输吞吐量降低.
解决思想:收到数据之后,稍微延迟一小会(时间不超过500ms),有可能这一小会上层就取出了数据,这时候窗口大小保持不变,保持传输吞吐量.
收到数据之后,并不会立即确认回复,因为立即确认回复,则会造成窗口变小,网路吞吐量降低,传输速度降低,延迟一会的目的是为了保证这段时间有可能用户将数据取出,则尽可能保证窗口的大小.
五.提供字节流服务
对于TCP来说,每当创建一个socket的时候,就会同时在内核中创建一个接收缓冲区和发送缓冲区。
接收数据:对于接收到的数据,并非像UDP一样一条一条往上交付,而是先将接收到的数据放入缓冲区,再根据上层所需要的长度,从接收缓冲区中取出相应的数据交付。 发送数据:对于发送的数据并不会直接发送,而是先存入发送缓冲区。如果数据过大,则会被拆分成多个TCP数据包发送。而如果数据过小,则会在发送缓冲区中等待,直到大小合适后再从发送缓冲区中取出数据发送(延迟发送可关闭)。
优点:这种传输方式比较灵活,对于多个小数据,会合并为一条大的数据一次性发送过去,这样就大大的减少了IO的次数。接收方也更加灵活,他可以任意取出想要的数据,不会像UDP一样必须交付一条完整的报文。
缺点:因为数据会在缓冲区中进行合并或者拆分,这就导致了数据直接的边界无法控制,所以TCP交付的这条数据可能并非一条完整的数据,而是半条或者多条数据,所以可能会导致上层会将多条数据按照一条来处理。这也就是TCP粘包问题。
TCP粘包问题:即TCP可能将多条数据按照一条处理(UDP不会有这种问题,UDP在首部中定义了数据报长度,确保每次只交付一条完整的数据)
解决方案:需要我们自己进行边界的管理。
-
每条数据之间以特殊字符进行间隔(如果数据中有该字符可能要转义处理)
-
数据定长传输,不够则补位(数据如果过短,则会传递大量无用的补位数据)
-
应用层协议头部定义数据长度(这里可以参考http协议和udp协议的做法)
http:头部以\r\n\r\n表示结束,并且在头部的Content-Length确定正文长度。 udp:传输层就解决了,在头部中就已经定义数据报长度。