TCP/IP (一)

UDP协议

UDP协议端格式:16位源端口号 16位目的端口号 16UDP长度 16位UDP校验和+携带的数据,16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度,也就是64k大小的数据,当大小超过64k的大小,必须在应用层进行分包,多次发送,在接收端手动拼接,如果校验和出错,直接被丢弃。

对于UDP的报头而言,OS定制的协议报头不过是一种结构化的数据(就是结构体),因为内核是C语言写的

struct udp_hdr{
    uint16_t src_port;
    uint16_t dst_port;
    uint16_t length;
    uint16_t check;
};

UDP的特点:

1.无连接:知道对方的IP地址和端口就直接进行传输,不进行连接;

2.不可靠:没有确认机制,没有重传机制,如果因为各种故障而没有发给对方,UDP协议层也不会给应用层进行反馈。

3.面向数据报:应用层交给UDP多长的报文,UDP原样发送,既不拆分,也不合并,我发10次,你必须接受10次,我发送一次,你也只能接受一次,不能分为多次接收。

对于网络IO接口而言,其实并不是真的接受和发送端口,而是拷贝对应缓冲区的内容,应用层把数据交给传输层之后,返回,接下来由OS进行操作,所以缓存区存在的价值就是给应用层节省了IO的时间,可以处理其他事情。 UDP是全双工的,双方通信互不干扰。

基于UDP的应用层协议:

NFS(网络文件系统) TFTP(简单文件传输协议)  DHCP(动态主机配置协议)  BOOTP(启动协议)  

TCP协议

tcp的特点

1.面向连接:知道对方的端口以及ip,进行绑定连接。

2.可靠性:相对于UDP而言,tcp有确认应答机制(ACK),以及丢包重传机制,所以是可靠的。

3.面向字节流:是字节流的数据。

可靠与不可靠主要是看做了多少手段来保证这个过程是可靠的,要保证可靠性是要付出代价的,要做很多的工作。

TCP报头

TCP全称:传输控制协议,如下是TCP协议报头格式

认识TCP报头字段 

1.16位源端口和16位目的端口保证了源应用,目标应用。

2.32位序号以及32位确认序号保证了通信的确认应答机制,去重,按序到达策略等,因为TCP是全双工的,所以既要序号也要确认序号。

3.16位窗口的大小,流量控制,用来交换通信双方接受缓冲区现在的大小,现在可以接收多少的数据,不经过处理的话是64kb的大小,但是选项中会存在扩大窗口大小的参数可以设置,通常是左移M位来表示扩大后的窗口的大小,但是扩大之后,相应的接收缓冲区的大小也得扩大。TCP的双方是有自己的接收和发送缓冲区,本质是将我的发送缓冲区的数据拷贝到对方的接收缓冲区。

4.16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 含TCP数据部分。

5.16位紧急指针,指的是当其中保留6位的标志位URG设置为1的时候,紧急指针存储的是距离报头的偏移量,表示的是紧急处理的信息的位置,通常只有1kb的大小。

6.保留六位:

URG:紧急指针是否有效

ACK:确认号是否有效

PSH:提示接收端应用程序应该立刻从TCP端把数据缓冲区的内容读走

RST:要求对方重新建立连接,我们把携带RST标识位称为复位报文段,一般是断电重启之后,一端认为连接还在,另外一段的连接断开。此时双方认为连接状态不一致的时候,会发送这个报文。双方连接不一致的时候,用来重置连接。

SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段。

FIN:通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段。

这六个标记位存在的意义是服务器会收到大量的报文,所以收到的报文需要有不同的类型,因为报文有不同的类型,所以需要有不同的处理策略

解包分用:

        tcp协议是有标准长度的,20字节,首先读取20字节。将这个数据转化成一个结构化的数据,这个结构化的数据一般都是OS内部自己提供的,立马提取标准报头的4位首部长度。计算4位首部长度中的大小可以得到后续报头的剩余大小,[0000,1111]->[0,15]--->*4 [0,60] ----->20字节的头部长度--->[20,60],这里计算的就是选项长度。只要把tcp的报头部分读完,到下一个tcp报头之前的长度就是有效载荷的长度,对于tcp的报头而言,同样的在系统中也是一个结构体。

struct tcp_hdr{
    uint16_t src_port;
    uint16_t dst_port;
    uint32_t sep;
    uint32_t ack_seq;
    uint32_t header_length;
    ....

};

确认应答机制

对于TCP连接而言,只有收到了应答,历史消息我才能100%确认对方收到了--确认应答才算可靠。每次服务端接受到消息之后,都会发送确认应答,确认序号是序列号+1,表示的是前面的序列号所收到的消息都是准确无误的。

超时重传机制

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B; 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发,当然也有可能是主机B的ACK丢了,在主机A等待一段时间之后,同样也会超时重传,主机B会收到很多重复的数据,根据序号辨别重复数据,进行去重操作。

对于丢包问题而言,需要把发送到的数据拷贝到发送缓冲区,发送缓冲区类似于一个数组,数组的下标就表示序号,那如何判断丢包了呢?对于发送方而言,有没有真正的丢包,发送方也不知道,定的策略是超时了,就判断丢包。超时的时间主要是由网络情况决定的。

对于发送而言,发送出去的数据,不会在发送缓冲区立马被移除,而是必须在滑动窗口处暂存,直到收到对方的应答才开始滑动。

连接

三次握手:三次握手是连接建立的机制,三次握手之后也不一定成功,当客户端发送最后一次应答的时候,到服务端认为建立连接之间是有时间间隔的,可能ACK丢失了,也无法建立连接。

对于服务端而言,首先是socket---->bind---->listen---->accept。其中在accept的客户端地址端口阻塞式等待客户端连接。

这里发送的SYN,其实是标志位SYN置为1的TCP报头,服务端就会认为这是个连接请求,从而进行处理。在代码中的体现就是connect()操作,同时在最开始进行SYN的时候,就开始交换双方各自的接收缓冲区大小即16位窗口大小被设置。

半连接:当服务端收到对端的SYN消息,并且给对端发送ACK+SYN的时候,此时服务端这边的状态是SYN-RECV,处于半连接状态,如果此时收到对端的ACK则成功建立连接,否则会不断发送请求直至成功。如果此时伪造大量的IP地址对服务器进行半连接,此时伪造的SYN会占用未连接队列,影响正常的SYN,导致目标系统运行缓慢,网络堵塞甚至是系统瘫痪。

为什么需要进行三次握手:首先一次握手容易造成SYN洪水的问题,对于服务端的OS而言,每有一个客户端进行连接的时候,就需要对这个连接进行管理,而维护一个连接是有成本的,需要先描述在组织,就会在服务端构建对应的结构管理这个连接。所以当客服端疯狂的发送SYN连接请求的时候,就容易把服务端的资源占完,即SYN洪水。采用的解决方案是SYN Cookie。

对于两次握手跟上诉的情况类似,并且不能保证通信是全双工的。

三次握手:用最小的成本验证全双工通信是畅通的,并且三次握手可以有效防止历史连接初始化了连接。

客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
(1) 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
(2) 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
(3) 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接;
(4) 如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。

四次握手可以是可以,但是浪费资源,对于偶数次连接,如果最后一次连接没有成功,服务器会挂满大量的半连接,更多次握手浪费资源。

TCP为什么要连接?因为要保证面向连接的可靠性,tcp不直接保证可靠性,在实际进行TCP连接的时候,服务端会建立结构体,对连接进行管理,是保证可靠传输的基础,三次握手是建立在连接结构体的基础上的,连接结构体里有关于是否是新建连接,那些报文丢失需要重传,那些报文需要超时重传,这些特征都维护在连接结构体里面的,流量控制,超时重传等机制也是连接结构体决定的。

四次挥手:当客户端或者服务端不想通信之后(注意这里的通信发送数据指的是不发用户的数据,这里是通过上层调用close(fd)实现的,并不代表底层没有报文的交互),只需要发送FIN=1的报头即可,当收到之后,进行ACK应答,之后,服务端给客户端发送FIN=1的报头,此时服务端进入CLOSE_WAIT状态,在客户端收到之后,给服务端发送确认应答,此时客户端会进入TIME_WAIT状态,此时一般是等待2MSL的时间(防止ACK没被收到以及等网络中的数据被真正消散),刚好够消息一来一发的时间,收到ACK报文的服务端进入CLOSED状态。之后客户端进入CLOSED状态。从上述描述可以知道,连接结构体中存在着STATUS状态变量,用来记录当前状态。

为什么要进行四次挥手呢?因为双方通信是全双工的,不像建立连接的过程,接收方收到连接请求后可以立即发送SYN进行连接,在断开连接时需要释放资源,接受方在收到发送方的释放连接请求后,还需要一段时间来处理未完成的发送请求,这里需要两次确认发送方的请求:第一次是未处理完,我还不能释放,但是收到了你的请求,告诉发送方一声,等等我;第二次确认表示已经处理完请求了,我也可以进行释放了。因此需要四次。

四次挥手动作完成之后,为什么断开连接的一方需要维持TIME-WAIT状态:需要保证最后一个ACK尽量可能的被对方接收到,并且在双方断开连接的时候,网络中还有滞留的报文,保证滞留报文进行消散。

在平时启用服务的时候,ctrl+c终止掉服务端的应用之后,再次重启该端口的该服务的时候,会bind_error,是因为此时是有服务端发起的断开请求,此时服务端正处在TIME-WAIT状态,该端口仍然是被占用的,所以需要在bind端口之前,设置端口可以在TIME-WAIT状态下被复用

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

对于断开连接而言,双方都可以主动选择断开连接,主动断开连接的一方,最终状态都是TIME_WAIt状态,被动断开连接的一方,两次挥手完成之后,就会进入CLOSE_WAIT状态。对于服务端而言,如果不调用close()关闭服务,客户端发起了断开请求之后,就会一致处在CLOSE_WAIT状态,如果服务器出现了大量的close_wait状态,1.服务器有bug,没有做close文件描述符的操作;2.服务器有压力,可能一直在推送消息给client端,导致来不及close。

为什么客户端要进入等待超时状态:因为最后一次ACK包之后,如果ACK包丢失,就会导致服务端一直停留在最后确认状态。无法断开连接就会占据资源。

滑动窗口

滑动窗口也为数据发送的效率提供了帮助,滑动窗口本质是在对方能够接受的前提条件下,尽可能增加发送效率,滑动窗口就是发送缓冲区的一个子区域,这个子区域的范围表示可以不收到对方的确认,可以发送的数据范围。

对于发送缓冲区而言,一共有四个部分:

 如何看待滑动窗口的问题,滑动窗口就是一个缓冲区的子数组,所谓的滑动窗口移动就是数组下标的移动,滑动窗口的大小设定是跟对方的接受能力有关,滑动窗口可能会变大或者变小的,无论怎么滑动,都要保证对方能够正常接受。

 滑动窗口可能向右滑动,也可能保持不动,当对端不从接受缓冲区拿数据的时候,wid_end就不会移动,只有win_start移动。滑动窗口的大小不是固定的,会根据对面缓冲区剩余的大小,进行调整窗口大小。对于win_start=ACK_SEQ,win_end=win_start+tcp_win,其中ACK_SEQ就是对端的ACK序号。收到应答确认的时候,如果不是最左侧发送的报文的确认,而是中间的,此时就可能出现两种情况,数据没丢,只是应答丢了,此时根据确认序号的定义可以知道ACK seq=X+1,表示的是X+1之前的所有的数据全部都收到了,所以此时不需要重传,表示之前的只是应答丢失了。如果最前面的数据丢失了,后面的数据收到了,也只会应答丢失数据前的序号,当连续收到了3个以上的连续相同的序号之后,就会触发重传机制。滑动窗口不一定必须滑动,可以保持不动,也可以变成0,对于窗口的移动其实就是数组下标的移动,滑动窗口本质上就是一个环形结构。

拥塞控制

当客户端向服务端发送大量数据的时候,有1-2个报文丢失,客户端会重发这一两个报文,但是当有大量的数据丢失的时候,不应该超时重传,因为这时候已经是网络的问题了,此时如果再出现大量的报文的话,只会增加网络的故障问题,此时引入了网络拥塞的概念。

拥塞控制主要是跟当前的网络拥堵状况有关,在不清楚当前网络状态下,贸然发送大量的数据可能会增加网络负担。TCP引入了慢启动机制,先发少量的数据,摸清当前的网络拥堵状态,在决定按照多大的速度传输数据。TCP的可靠性不仅仅考虑了双方主机的问题,同时也考虑了网络的问题。

拥塞控制的策略:拥塞窗口是一个变量,首先呈指数增长,紧接着增长到慢启动的阈值之后,呈线性增长,线性增长的目的是为了探测到拥塞窗口的大小,重新计算慢启动的阈值,再从1到新的阈值。所以拥塞窗口其实是一个上下浮动的值。

拥塞控制既是为了可靠性,也是为了传输数据的效率。关于效率问题直接影响的是滑动窗口的大小问题,滑动窗口的大小=min(拥塞窗口,对端窗口大小(对端的接受能力))

延迟应答

有时候服务端或者客户端并不是收到消息之后,立马给出应答,而是会等待一定的时间(必须小于超时重传的时间),因为应用层处理数据的速度很快,会从缓冲区拿走一定的数据,此时接收缓冲区的容量变大,就会增大自己的接受能力,从而增大窗口,以至于增大网络的吞吐量,传输效率增大,目标就是为了保证在网络不拥塞的情况下,提高传输效率。

延迟应答的策略:数量限制:每隔N个包就应答一次;时间限制:超过最大延迟时间就应答一次。

捎带应答

再给你应答的时候,可能会携带着给你的数据,也就是ACK+data一起给你了,也有就是三次挥手的时候ACK+SYN一起给客户端。

面向字节流

创建TCP的socket时,同时在内核中创建发送缓冲区和接受缓冲区,由于缓冲区的存在,TCP程序的读写不需要一一匹配,例如:写100个字节数据的时候,可以调用write写100个字节,也可以调用100次write,每次只写一个字节,read同理。

面向字节流就是应用层需要多大的数据,自己从缓冲区拷贝即可,想怎么读取就怎么读取,TCP无法保证是否是完整的报文,应用层应该去关注是否是完整的报文,需要应用层去维护数据。而面向数据报的就是每次传输的必定是完整的报文,要么读到了就是一个完整的报文,没读取到就是啥也没有,只需要应用层拿去自己做序列和反序列即可。

对于面向字节流的就容易出现粘包(数据包)问题,解决问题的关键是明确两个数据包之间的边界:

对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲 区从头开始按sizeof(Request)依次读取即可;

对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

对于变长的包, 自描述字段还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔 符不和正文冲突即可);

listen的第二个参数

listen的第二个参数是用来为上层维护一个连接队列,不能没有这个连接队列,也不能太长,tcp底层允许最多有backlog+1个完整的连接,后续来的都只能是半连接,此时server端的状态是SYN_RECV,如果没有尽快完成握手的话,会被server关闭。在这里维护一个连接队列的本质是让我们有资源空闲的时候,可以立马使用,提高资源的利用率,不能不排队,也不能让队列太长。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值