一、TCP协议
1.网络模型
1)七层网络模型
层数
|
名字
|
主要功能
|
对应的典型设备
|
传输单位
|
7
|
应用层
|
提供应用程序间通信
|
计算机:应用程序,如FTP、SMTP、HTTP等
|
程序级数据
|
6
|
表示层
|
处理数据格式、数据加密等
|
计算机:编码方式,如图像编解码、URL字段传输编码等
|
程序级数据
|
5
|
会话层
|
建立、维护和管理会话
|
计算机:建立会话,如session认证、断点续传
|
程序级数据
|
4
|
传输层
|
建立主机端到端连接
|
计算机:进程和端口
|
数据段(segment)
|
3
|
网络层
|
寻址和路由选择
|
网络:路由器、防火墙、多层交换机
|
数据包(packet)
|
2
|
数据链路层
|
提供介质访问、链路管理等
|
网络:网卡、网桥、交换机
|
帧(frame)
|
1
|
物理层
|
比特流传输
|
网络:中继器、集线器、网线和HUB
|
比特(bit)
|
- 这个七层网络模型在数据的传输过程中还会对数据进行封装,应用层封装之后形成应用层协议数据单元(Protocal Data Unit,PDU),在接收端会进行解封装
层次
|
封装头
|
应用层
|
Application Header,AH
|
表示层
|
Presentation Header,PH
|
会话层
|
Session Header,SH
|
传输层
|
Transport Header,TH
|
网络层
|
Network Header,NH
|
数据链路层
|
Data Link Header,DH & Data Link Termination,DT
|
物理层
|
|
2)五层网络模型
- 大学教科书常用
层次
|
功能
|
应用层
|
http,smtp,ftp
|
传输层
|
负责主机间不同进程的通信TCP,UDP等
|
网络层
|
负责分组交换网中不同主机间的通信
|
数据链路层
|
负责将网络层的IP数据报组装成帧
|
物理层
|
透明的传输bit流
|
3)四层网络模型
- TCP/IP分层模型(TCP/IP Layer Model),使用最为广泛,又叫因特网分层模型(Internet LayerModel)或因特网参考模型(Internet Reference Model)
层数
|
名字
|
应用到的协议
|
4
|
应用层
|
DNS,FINGER,WHOIS,FTP,HTTP,GOPHER,TELNET,IRC,SMTP,USERNET等
|
3
|
传输层
|
TCP,UDP
|
2
|
网间层
|
TCMP,IP
|
1
|
网络接口
|
ARP/RARP等
|
- 该分层并不包含数据链路层和物理层,必须与其他协议协同工作
1.网络接口层包括用于协作IP数据在已有网络介质上传输的协议。TCP/IP协议自身并不定义与数据链路层和物理层相对应的功能,本层则定义这些
2.网间层对应于OSI七层网络协议的网络层。本层包含IP协议,RIP(Routing Information Protocol,路由信息协议)协议,负责数据的包装、寻址和路由、ICMP(Internet Control Message Protocol,网间控制报文协议)协议用来提供网络诊断信息
3.传输层对应于OSI七层网络协议的传输层,他提供两种端到端协议TCP和UDP
4.应用层对应于OSI七层网络协议的
应用层和表示层,
- 总结(应该记忆的内容):
1.TCP协议在第四层传输层
2.IP协议在第三层网络层
3.ARP协议在第二层数据链路层
4.数据链路层的数据叫帧(Frame),网络层的数据叫包(Packet),传输层的数据叫段(Segment)
5.所有数据首先会被打包到TCP的segment中,然后TCP的segment会打包到IP的packet中,然后再打包到以太网的Frame中。传输到对端后再解封装
2.TCP头部 P191
![](https://i-blog.csdnimg.cn/blog_migrate/ea9edeb86c814997a10252431de0fd98.png)
- 16位源端口号:
- 16位目的端口号:递交给哪个上层协议或者应用程序(目的端口)
- 32位序号:建立TCP连接时序号值被初始化为某个随机值ISN(随机序号值),后续的TCP报文段中的序号值被设置为ISN加上该报文段所携带的数据的第一个字节在整个字节流中的偏移
- 32位确认序号:用作对另一方发送来的TCP报文的相应,其值是接收到的TCP报文段的序号值加1
- 4位头部长度:标识该头部有多少32bit,最大为15*4 = 60Byte
- 保留6位
- URG:紧急指针是否有效
- ACK:是否是确认报文
- PSH:
- RST:要求对方重建连接,复位报文
- SYN:请求建立连接,同步报文
- FIN:通知本端要关闭连接,结束报文
- 16位窗口大小:本端的TCP缓冲区还能容纳多少字节
- 16位校验和:采用CRC循环冗余校验,这个校验不仅校验头部,也会校验数据部分,是TCP可靠传输的保障
- 16位紧急指针
总结:
1.TCP包没有IP地址,那是网络层上事,但是有源和目的端口
2.一个TCP需要五个元组来表示是同一个连接:src_ip,src_port,dst_ip,dst_port和协议
3.32位序号是包的序号,用来解决网络包乱序的问题
4.ACK用于接收确认,解决丢包问题
5.窗口就是著名的滑动窗口,用于解决流控问题
6.6个bit的TCP Flag如上,确认包的类型,用于操控TCP状态机
3.TCP状态流转
P193
- TCP建立连接需要进行三次握手,而TCP终止连接则需要四次握手
1)建立连接
![](https://i-blog.csdnimg.cn/blog_migrate/91a0eaccc5864e712758eb7ce9d6e377.png)
//一开始客户端和服务器端都处在closed状态,其中服务器端处在LISTEN监听状态
1.第一次握手:建立连接时,客户端发送SYN(同步报文)包(
SYN = 1,seq = x)到服务器,并且进入
SYN-SEND状态,等待服务器确认
2.第二次握手:服务器收到SYN包,必须对该包进行回应(
发送ACK = 1包且ack = x+1),同时自己也发送一个SYN信息(
SYN = 1,seq = y),即发送一个SYN+ACK包,此时服务器进入
SYN-RECV状态
3.第三次握手:客户端接收到服务器的SYN+ACK包,想服务器发送确认包ACK(
ACK = 1,seq = x+1,ack = y+1),此包发送完毕后客户端进入
ESTABLISHED状态,服务器接收到之后也进入
ESTABLISHED状态
- 在建立连接的时候,通信的双方要互相确认最大报文长度(Maximum Segement Size,MSS),以便通信。
- 为什么需要三次握手:对于建立连接的三次握手,主要是要初始化Sequence Number的初始值。通信的双方要互相通知对方自己的初始化Sequence Number值,所以这个过程也叫SYN同步。也就是上图中的x和y,这个序号要作为以后通信使用的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序(TCP会使用这个序号来拼接数据)
2)结束连接
- TCP有一个特别的概念叫做半关闭。TCP的连接是全双工的,即可以同时发送和接收。因此在关闭连接的时候,必须关闭收和发两个方向上的连接。
![](https://i-blog.csdnimg.cn/blog_migrate/1e3e639a1c3b5d60bb04326e7f3dd54a.png)
- 客户机给服务器送一个结束报文(FIN = 1,seq = u),然后服务器返回给客户端一个确认报文(ACK = 1,ack = u+1,seq = v),至此客户端发送到服务器端的数据通道关闭了;然后服务器端先传完未传输完成的数据,传输完成后,服务器给客户端也发送一个结束报文(FIN = 1,ACK = 1,ack = u+1,seq = w,注意这里用的是序号w),客户端返回给服务器一个确认报文(ACK = 1,ack = w+1,seq = u+1)。至此四次握手结束,连接关闭
- 为什么要进行四次握手:因为TCP是全双工的,所以发送方和接收方都需要FIN和ACK,一方发送完数据之后,才会发送自己的FIN。不过有一方是被动的,所以看上去就成为了所谓的四次挥手。
- TCP状态转换图如上所示,各个状态的意思如下:
1.CLOSED:表示初始状态
2.LISTEN:表示服务器端的某个socket处于监听状态,可以接受连接
3.SYN_SENT:在服务器端监听(LISTEN)后,
客户端socket执行CONNECT连接时,客户端发送SYN报文,此时客户端就进入SYN_SENT状态,等待服务器的确认
4.SYN_RCVD:表示服务端接收到了SYN报文。通常,这是服务器端在三次握手过程中间一个中间状态,很短暂。在这种状态下,一旦接收到了客户端的ACK报文就会进入到
ESTABLISHED状态
5.ESTABLISHED:表示连接已经建立
6.FIN_WAIT_1:在已经建立连接的情况下,一方主动关闭连接,向对方发送了FIN报文,此时该socket即进入到FIN_WAIT_1状态,当接收到对方回应的ACK报文之后,就会进入FIN_WAIT_2状态,也是一个很短暂的状态。
7.FIN_WAIT_2:处于FIN_WAIT_2状态下的socket表示半连接。即有一方要求关闭连接,并且发送了FIN接收到了ACK关闭了己方的发送,但对方还有数据没发送完成,所以让关闭发起方稍作等待,发送完数据再关闭另一半连接。在这个状态下,主动关闭方还能够接收数据,但是没有办法发送数据了。
8.TIME_WAIT:表示关闭的发起方收到了对方的FIN报文,并且发送了ACK报文。等待2MSL之后即可回到CLOSED可用状态。
MSL(Maximum Segement Lifetime):报文最大生存时间。 等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包
如果在FIN_WAIT_1状态下,收到了对方同时带有FIN和ACK标识的报文时,可以直接进入TIME_WAIT状态,而无须经过FIN_WAIT_2状态
9.CLOSING:双方同时发送FIN报文,在接收到ACK报文之前就接收到了单独的FIN报文,就会进入CLOSING状态,这是一个比较少见的状态,表示双方都正在关闭
10.CLOSE_WAIT:被动关闭方接收到对方的FIN报文,返回一个ACK报文之后,就会进入CLOSE_WAIT状态,这个状态是处理剩下没有发送完成的数据,如果不再有数据需要处理,那么就发送FIN给对方,转到下一个状态
11.LAST_ACK:被动关闭方在发送完FIN报文后,最后等待对方的ACK报文
12.CLOESD:当收到ACK报文后,即可进入到CLOSED报文可用状态
4.TCP超时重传
P196
- TCP数据传输的可能情况:
1.正常情况:数据顺利到达对端,对端发出ACK,己方顺利收到ACK
2.异常一:数据包中途丢失
3.异常二:数据包顺利到达,但对端异常未相应,没有发出ACK
4.异常三:数据包顺利到达对端,对端也发出了ACK,但是ACK报文中途丢失
//出现这些异常情况,TCP就会超时重传。TCP对每个报文会设置一个计时器,只有计时器设置的时间到了还没有收到确认,就要重传这一段报文。
- 超时重传的关键参数一:
1.RTO(Retransmission Timeout,超时重传时间):指发端发送数据后,等待ACK报文的时间。过了这个时间没有收到ACK报文,就进行重传
//太长,重传慢,效率低性能差
//太短,重传频繁,增加网络负载,导致拥塞
- TCP协议必须适应两个方面是时延差异:
1.达到不同目的端的时延的差异
2.统一连接上的传输时延随业务量负载变化而出现的差异
- TCP采用自适应算法来适应互联网分组传输时延的变化。TCP监视每个连接的性能,由每一个TCP的连接情况推算出合适的RTO值,当连接时延性能变化时,TCP也能相应的自动修改RTO设定,以适应网络变化。
- 超时重传的关键参数二:
1.RTT(Round Trip Time,连接往返时间):
- 由RTT计算RTO的算法如下所示:
1.先采样RTT,记录下最近几次的RTT
2.然后进行平滑计算SRTT:SRTT = alpha*SRTT + (1-alpha)*RTT;其中alpha取值为0.8~0.9
3.计算RTO:RTO = min(UBOUND,max(LBOUND,(beta*SRTT)));其中
UBOUND为最大的timeout值,LBOUND为最小的timeout值,beta取值为1.3~2.0
//在没有重传发生的时候的算法
- 如果发生重传就使用Karn算法:
1.一旦发生重传:
新的重传时间 = gama*(旧的重传时间)
2.没有发生重传的时候:再按照上面的方法根据报文的往返时延更新平均往返时延RTT
5.TCP滑动窗口
P200
- TCP滑动窗口主要有两个作用:一是提供TCP的可靠性,二是提供TCP的流控特性
- 体现了TCP面向字节流的设计思路
- 见2.TCP头部中,窗口字段是一个16bit的字段,它代表窗口的字节容量,为65535个字节。
- TCP选项字段还包含了窗口扩大因子,能够把窗口扩大到32bit
- TCP发送方缓存中的数据可以分成四类:
1.已经发送并且得到对端ACK
2.已经发送但是没有得到对端ACK
3.未发送但对端允许发送
4.未发送且对端不允许发送
//其中2,3称为TCP的发送窗口
//当接收到新的ACK对于发送窗口中后续字节的确认是,窗口会进行滑动
- TCP接收方的接收缓存可以分成三类:
1.已接收
2.未接收准备接收
3.未接收并不准备接收
//不存在已接收未回复ACK这种,因为回复必然马上发生
//其中未接收准备接收称为接收窗口
- TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”
- 接收窗口的大小取决于应用、系统和硬件的限制。各自的发送窗口的大小则取决于对端的接收窗口的大小,应该和接收窗口相同。
- 滑动窗口实现面向流的可靠性来源于“确认重传机制”。TCP滑动窗口的可靠性也是建立在确认重传上的。
1.发送窗口只有在收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界
2.接收窗口只有在前面所有的段都确认的情况下才会移动左边界,在前面还有字节未接收但收到后面字节的情况下,窗口也不会移动,
并不对后续字节进行确认。以此确保对端会对这些数据重传
- TCP的流控特性:应用根据自身的处理能力变化,通过本端TCP接收窗口大小控制来对对端的发送窗口进行流量控制。
6.TCP拥塞控制
P202
- 网络的资源:计算机网络中的带宽、交换节点中的缓存和处理机等
- 拥塞:对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就可能变坏
- TCP的拥塞控制由四个核心算法构成:慢开始、拥塞避免、快速重传和快速回复
1)慢开始和拥塞避免
- 发送方维持一个叫做拥塞窗口cwnd的状态变量
- 设置一个门限叫慢开始门限ssthresh,
- TCP拥塞控制四个核心算法:
1.慢开始
2.拥塞避免
3.快速重传
4.快速恢复
//慢开始
//在每收到一个对新报文的确认之后,就把拥塞窗口的大小翻倍,直到cwnd > ssthresh为止
//cwnd < ssthresh:使用慢开始算法
//cwnd > ssthresh:使用拥塞避免算法
//cwnd == ssthresh:两者使用任意
//拥塞避免算法
//让拥塞窗口缓慢增长,每经过一个往返时间RTT就把发送方的cwnd加1,而不是加倍
//具体过程
1)TCP连接初始化,把cwnd设置为1(一个MSS大小,实际中该状态变量的大小应该是表示的字节数)
2)执行慢开始算法,cwnd按指数规律增长,直到cwnd = ssthresh为止,开始执行拥塞避免算法,cwnd按照线性增长
3)当网络发生拥塞时,把ssthresh值更新为拥塞前的一般,cwnd重新设置为1,按照步骤二执行
2)快速重传和快速恢复
![](https://i-blog.csdnimg.cn/blog_migrate/008fe138ea845e8632cd71295c220d17.png)
- 快重传:
1.要求接收方:在收到失序的报文段(即编号没有对应上)后就立即发送重复确认,不用等待自己发消息时顺带发送
2.要求发送方:一连收到3个重复确认(第一个对正确报文的确认不算)就应当立即重传对方没有收到的报文段
//见上图
- 快恢复算法:
1.当发送方连续收到三个重复确认时,要执行“乘法减小”算法,把ssthresh门限减小一般。但是cwnd并不重置到1
2.发送发只认为网络可能拥塞,把cwnd设置为ssthresh大小
二、TCP网络编程API
- 网络层的IP地址可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。三元组(IP+协议+端口)就可以标识网络的进程
- 网络中的进程是通过socket来通信的
- socket起源于UNIX,遵循UNIX的基本模式,即:open ——write/read——close
- TCP/IP协议的应用程序通常采用应用程序接口,即UNIX BSD的套接字(socket)来实现网络进程之间的通信,也是一种通用的形式。
- accept发生在三次握手的哪个阶段:
- 连接在三次握手之后建立了TCP连接,从syn队列转移到accept队列,这个时候就可以调用accept函数获得此连接
- socket套接字的交互流程:共12步,与TCP交互流程的图相互对应 P206
![](https://i-blog.csdnimg.cn/blog_migrate/693a4dcf62d81730454eccbff83c0ec0.png)
- 其中服务器的read改为recv()write改为close。客户机的write改为send,read改为close
- 注意客户端的socket是主动打开的,发生在connect这一步,而服务器的socket是被动打开的,发生在接收到客户端的请求之后
- 三次握手全部发生在connect阶段
- 四次挥手发生在close阶段
1.socket函数
- socket函数用于创建一个socket描述符,它唯一标识一个socket
//如果函数调用成功,会返回一个标识这个套接字的文件描述符。
//每个进程空间都有一个套接字描述符表,通过描述符在该表中查询套接字
//参数:
//domain:协议域/协议簇:如AF_INET等
//type:指定socket类型,如SOCK_STREAM等
//protocal:指定协议
int socket(int domain,int type,int protocal);
2.bind函数
- bind函数把一个地址族中的特定地址赋给socket
//调用成功返回0
//
参数:
//sockfd:socket描述符
//addr:要绑定给socket的协议地址的指针
//addrlen:对应的地址的长度
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
3.listen和connect函数
- 服务器在调用socket和bind之后就会调用listen来监听这个socket,会监听指定socket地址
- 客户端这时调用connect发出连接请求,服务器就会收到这个请求
//socket函数创建的socket默认是一个主动类型的,而listen函数将socket转换称为被动类型的,等待客户的连接请求
//参数
//
sockfd:socket描述符
//backlog:相应socket可以排队的最大连接数
int listen(int sockfd,int backlog);
//客户端通过调用connect函数来建立与TCP服务器的连接
//参数
//
sockfd:socket描述符
//addr:要绑定给socket的协议地址的指针
//addrlen:对应的地址的长度
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
4.accept函数
- TCP服务器监听到客户端发送的连接请求之后,就会调用accept函数取接收请求。
//如果socket成功,那么其返回值是由内核自动生成的一个全新的socket描述字,代表与客户端的TCP连接
//参数
//sockfd:socket描述符
//addr:要绑定给socket的协议地址的指针
//addrlen:对应的地址的长度
//注意:accept的第一个参数是服务器的socket描述字,是服务器开始调用socket函数生成的,称为监听socket描述字;而accept函数返回的是一个全新的,描述连接的socket描述字。
//一个服务器通常只创建一个监听socket描述字,它在该服务器的声明周期一直存在。内核为每个由服务器进程接受的客户创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭
int accept(int sockfd,
const struct sockaddr *addr,socklen_t addrlen);
5.read和write函数
- 完成了上述的连接操作建立了连接,就可以进行网络I/O操作。如前所述,socket套接字是类似文件的。
read/write
recv/send
readv/writev
recvmsg/sendmsg
recvfrom/sendto:
在无连接的数据报socket方式下,由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址
//其中最常用
read/write
//参数:
//fd:socket描述字
//buf:缓冲区
//count:缓冲区大小
ssize_t read(int fd,void *buf,size_t count);
//
//参数:
//fd:socket描述字
//buf:缓冲区
//count:缓冲区大小
ssize_t write(int fd,const void *buf,size_t count);
6.close函数
- 关闭相应的socket描述字
#include <unistd.h>
int close(int fd);
三、实现一个TCP server
四、TCP协议选项
- 使用setsockopt来进行设置
1.SO_REUSEADDR
- 这个套接字会通知内核,如果端口忙,但TCP状态位于TIME_WAIT状态,则可以重用端口
2.TCP_NODELAY和TCP_CORK
- 包的"Nagle化" P217
- 这两个选项都禁用了包的Nagle化:TCP_NODELAY不使用Nagle算法,不会将小包拼接成大包再发送。TCP_CORK会尽量每次发送最大的数据量,每次阻塞200ms再发送
3.NO_LINGER
- 控制close的操作。几种选项,关闭、强制关闭和优雅的关闭
4.TCP_DEFER_ACCEPT
- 推迟接受,在收到第一个数据之后才会创建连接
- 在三次握手之后,状态为SYN_RECV而不是ESTABLISHED。
5.SO_KEEPALIVE
- 用于保持连接检测对方主机是否奔溃,避免服务器永远阻塞与TCP连接的输入
- 任一方都没有数据发送的情况下,TCP自动给对方发送一个保持存活探测分节,用于判断对方是否崩溃
6.SO_SNDTIMEO和SO_RCVTIMEO
- 分别设置socket的发送和接收超时时间
7.SO_RCVBUF和SO_SNDBUF
- TCP必须为已经发送的数据保留一个副本,直到它被对端确认为止。UDP不保存应用程序的副本,因此不需要一个真的发送缓冲区
- 每个TCP套接字都有一个发送缓冲区和一个接收缓冲区,可以使用这两个选项来改变默认缓冲区的大小
五、网络字节序与主机序
- 不同的CPU有不同的字节序类型,这些字节序是指整数在内存中的保存的顺序,称为主机序
- little endian:小端模式,将低位字节存放在内存的低地址端。符合人的第一观感,即低位值小放在低地址位
- big endian:大端模式,将低位字节存放在内存的高地址端。直观,地址从左到右由高到低,数值从左到右写出从低位到高位,一一对应不需要考虑对应关系
- java和网络协议都采用大端模式,因此也把big endian称为网络字节序。主机间进行通信必须把数据转换成网络字节序再进行通信
六、封包和解包
- 粘包的情况 P234
- UDP不会出现粘包,因为UDP是个数据包协议,两段数据之间是有界限的
- 粘包的发生情况:
1.在发送端,由于Nagle算法造成的发送端粘包
2.接收端在接收不及时的时候造成接收端粘包。TCP把接收到的数据放在接收缓冲区中,然后通知应用层取数据。如果应用层取数据取的不及时,就会造成TCP缓冲区放了很多段数据,造成粘包
- 对数据进行封包和拆包就能解决粘包的问题
封包就是给一段数据加上包头,数据包分为了包头和包体两部分内容。
包头其实就是一个大小固定的结构体
包头中有一个结构体变量表示包体的长度,根据固定的包头长度以及包头中含有的包体长度的变量值,就能够正确的拆分出一个完整的数据包
- 接收和发送一个字符串、接收和发送一个结构体
1.int等类型在传输过程中,需要在发送端通过
htonl转换成网络字节序,而在接收端需要通过
ntohl函数转换成为主机字节序
2.注意发送和接收方式,通过设置接收和发送的buffer大小可以规定某次接收/发送多少数据
3.注意结构体的打包方式,使用memcpy来进行打包,并且要对结构体内的逐个元素进行打包,对有需要的进行字节序转换
4.sizeof对于字符串数组和字符指针,计算的是其长度。而不是指针所占内存大小