Linux网络编程---传输层

一、重谈端口号

1、在 TCP/IP 协议中 , " IP", " 源端口号 ", " 目的 IP", " 目的端口号 ", " 协议号 " 这样一个五元组来标识一个通信 ( 可以通过netstat -n查看 );
2、端口号的划分
0 - 1023: 知名端口号 , HTTP, FTP, SSH 等这些广为使用的应用层协议 , 他们的端口号都是固定的 .

 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.

一个进程可以绑定多个端口号,但是一个端口号不能绑定多个进程。

二、UDP协议

UDP的协议格式

回答问题,A: 1、报头和有效载荷如何分离?

定长报头8字节

2、有效载荷应该交付给哪一个上层协议(对应的协议字段,方案)。

16位目的端口号,交付给上一层协议。

B: 认识报头

C: 学习该协议周边的问题。

三、TCP协议

 TCP协议格式

前二十个字节为标准报头。

1、报头和有效载荷如何分离,如何交付给上层?

通过端口号交付给上层。4位的首部长度报头和有效载荷分离,4位的首部长度,计算的时候,有基本的大小单位:4字节。标准报头可表示为0101

2、16位窗口大小

client和server基于tcp协议进行通信的时候,互发消息的时候,发送的是完整的tcp报文,即,一定携带完整的报头。

避免大面积丢包,对端的缓存区来不及接收,就会丢失,所以TCP增加了流量控制的概念。

TCP凭什么保证可靠性,最基本的一个特点:确认应答机制。

确认应答是什么,对于发送方来讲,发送速度由对方的接收缓冲区中剩余空间的大小决定!客户端是通过服务器发来的确认报文来知道对方的接收缓冲区的剩余大小,16位窗口大小就是填缓冲区剩余大小的。填写的是自己的接收缓冲区的剩余空间的大小。来保证我们的流量控制。

3、序号和确认序号

我收到了应答,确认了我最近发送的消息对方收到了;没有应答的数据,我们无法保证可靠性。所以最新的一条消息是没,有应答的。即我们无法保证发出去的消息是100%被接收到的。我们这个世界上并不存在百分百可靠的协议,话虽这么说,但是在局部上收到应答的消息就是可靠的。也就是最新消息之前的收到应答的消息是可靠的。

tcp最基本最原始的通信过程,我们需要保证两个方向上的可靠性

应答如果丢了,客户端就认为我发给服务器的消息没有被收到,一段时间如果没有收到应答,客户端认为数据丢失了。进行超时重传。tcp会把中间的应答和tcp数据,合成一个报文。这种属于捎带应答。

并行发送,客户端按顺序发送,服务器不会按顺序接收,这种情况称为数据包乱序问题。造成乱序本身就是不可靠的一种。

这就需要用到报头中的序号字段:它保证了数据的按序到达。那么序号是什么呢?

TCP为每个字节都变了号,这个编号为序号。

确认序号为收到的报文序号 + 1,确认序号的意义表示确认序号之前的报文已经收到了,下一次发送,请从确认序号开始发送。所以应答允许有少量的丢失。通过序号排序重组。

为什么要有序号和确认序号,既是应答又携带了数据捎带应答,双方的地位是均等的。

4、六个标记位

tcp通信的时候,需要建立连接,进行征程的数据通信,tcp还要断开连接都要发送完整的TCP报文。  所以TCP收到的报文一定是有各种类型的,不同的类型,决定了服务器要做不同的动作!服务端或者接收方是通过六个标记位来区分TCP的各种类型的。这就是标记位的作用。

ACK: 确认序号是否有效。置为1代表有效。

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

FIN: 通知对方,本端要关闭了。

PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。流量控制与生产消费模型里的同步相似。

RST: 对方要求重新建立连接;我们把携带RST标识的称为复位报文段。TCP虽然保证可靠性,但是TCP允许连接建立失败。我们维护连接也是有成本的

客户端在发送出第三个ACK报文的时候,客户端就认为三次握手完成了。连接就建立好了。因为最后一个ACK是没有应答的。不能保证最后一个ACK一定是能被服务端收到的。这就可能导致客户端和服务器对连接的建立认知不一致。客户端认为连接建立好了,就发送数据,可是服务器认为没有建立连接,所以接受数据后,回复应答,把RST标志位设置。再次让客户端请求连接。

URG: 紧急指针是否有效,允许某些数据可以被应用层高优先级处理。16位紧急指针,是紧急数据在数据中的偏移量。紧急数据一般只能一个报文携带一个字节的数据。在发送是可以使用send函数里的MSG_OOB选项发送带外数据也就是紧急数据。

什么情况下我们会使用URG?

通过紧急数据对服务器的状态进行检测。要求服务器可以读取紧急数据,并且可以返回服务器的工作状态。

5、超时重传机制

第一种情况:

第二种情况:

主机对于发出去的报文,是否丢失,无法判定!必须通过规定,表征是否重传。第二种情况服务端会收到重复报文,我们需要对重复报文进行去重。

问题来了超时时间如何设置呢?

最理想的情况下 , 找到一个最小的时间 , 保证 " 确认应答一定能在这个时间内返回 ".
但是这个时间的长短 , 随着网络环境的不同 , 是有差异的 .
如果超时时间设的太长 , 会影响整体的重传效率 ;
如果超时时间设的太短 , 有可能会频繁发送重复的包 ;

这个时间间隔应该是动态的和网络状态相关。 

Linux (BSD Unix Windows 也是如此 ), 超时以 500ms 为一个单位进行控制 , 每次判定超时重发的超时时间都是500ms 的整数倍 .
如果重发一次之后 , 仍然得不到应答 , 等待 2*500ms 后再进行重传 .
如果仍然得不到应答 , 等待 4*500ms 进行重传 . 依次类推 , 以指数形式递增 .
累计到一定的重传次数 , TCP 认为网络或者对端主机出现异常 , 强制关闭连接 .  

6、连接管理机制

三次握手是各自的操作系统自主完成的。

tcp通信,是基于连接的。建立和断开,三次握手和四次挥手。

为什么要进行三次握手和四次挥手?

 三次握手: 可靠的验证全双工通信通畅!服务端和客户端必须保证一次发和一次收。第三次ACK是因为验证服务器的第一次发是否可以被客户端收到。

单次握手会发生SYN洪水问题。服务端维护连接时候会消耗系统资源。导致连接资源被使用完毕。浪费极大。

两次握手:优先让服务器做出建立连接的动作。但是客户端错误,没有第三次ACK。会导致我们的异常连接就只能在服务器被管理。也会消耗系统资源。成本被嫁接到了服务端。

三次握手:连接失败的成,本是在客户端上。因为服务端没有收到第三次ack的时候,服务端就知道连接没有成功。在服务端上的半连接就会释放掉从而减轻服务器操作系统的负担。

奇数次握手,可以确保一般情况握手失败的连接成本是嫁接到客户端上的。

三次握手也可以看作四次握手。

断开连接: 断开连接的本质是没有数据给对方发送了。发送数据时双方都可能发,所以必须断开两次。

客户端TIME_WAIT到CLOSED状态必须等待一段时间

7、TCP三次握手各个阶段的状态

  • 连接建立成功和上层有没有accept没有关系。三次握手时双方操作系统自动完成的!
  • listen的第二个参数? 当backlog为1时。服务端在没有Accept时,服务器需要对已经建立的连接进行管理,先描述再组织,这里是通过队列来管理的,listen的第二个参数backlog+1标识管理底层已经建立好的连接队列的最大长度。我们把这个队列称为全连接队列。2个成员第三个不能进队列。所以状态为SYN_RECV状态。把第三次ACK服务器自动丢弃。无法从SYN_RECV状态编程ESTABLISHED状态。
  • server端,不会长时间维护syn_recv,被建立连接的一方,一直处于syn_recv的时候,这种连接被称为半连接。也有一个队列,称为半连接队列,半连接队列的节点,不会长时间维护。
  • client和server连接建立不一致
  • 半连接队列里的节点在在收到第三个ACK后才会被挂到全连接队列里。
  • listen的第二个参数的常见的面试题,为什么不能太长,为什么不能没有,答:太长了导致连接来不及处理,不断有新的连接到来,在服务器非常忙的时候只会占用服务器的资源,反而拖慢了上层的处理速度,会导致操作系统资源的浪费。为什么不能没有呢?如果没有会导致服务器太过于空闲,因为服务器在空闲的时候,没有全连接队列还得等待客户端的连接。如果有,就会直接处理全连接队列里的连接。

8、TCP四次挥手的状态

FIN_WAIT1  FIN_WAIT2 CLOSE_WAIT TIME_WAIT

  •   TIME_WAIT:主动断开连接的一方,在4次挥手完成之后,要进入time_wait状态,等待若干时长,之后,自动释放。
  • 如果主动断开连接的一方是server,server器就会进入timewait状态,连接没有被彻底断开,ip和port正在被使用。所以新启进程就不能绑定相同的端口,服务器无法立即重新启动,我们需要设置socket选项使用系统调用接口
  • TIME_WAIT等多长时间为什么?
    TCP 协议规定 , 主动关闭连接的一方要处于 TIME_ WAIT 状态 , 等待两个 MSL(maximum segment lifetime)的时间后才能回到CLOSED 状态 .
    我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方 , TIME_WAIT 期间仍然不能再次监听同样的server 端口 ;
    MSL RFC1122 中规定为两分钟 , 但是各操作系统的实现不同 , Centos7上默认配置的值是60s;可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看 msl 的值 ;
  • 为什么要等待?尽量保证挥手是成功的,如果最后一次ACK丢失,没有TIME_WAIT,服务器不会断开了。但是有time_wait的话客户端还可以收到FIN,补发ACK。让通信双方历史数据得以消散。让我们断开连接,4次挥手,提高容错性。

9、流量控制 

第一次连接的时候,怎么保证发送数据量是合理的。不要理解三次握手,只是三次握手,双方也交换了报文!已经协商了对方的接收能力。

第三次握手的时候,我们可以携带数据!

  

接收端如何把窗口大小告诉发送端呢 ? 回忆我们的 TCP 首部中 , 有一个 16 位窗口字段 , 就是存放了窗口大小信息 ;
那么问题来了 , 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么 ?
实际上 , TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是窗口字段的值左移 M ;

流量控制是属于可靠性。也提高了传输效率,不会造成报文的丢失和重复。 

10、滑动窗口

已经发出去,但是暂时没有收到应答的报文,要被tcp暂时保存起来,已经发出去,但是暂时没有收到应答,可能会在发送方存在多个。 已经发出去,但是暂时没有收到应答多个报文,会被保存到发送缓冲区

因为有滑动窗口的区域,我们才可以一次向对方发送大量的tcp报文。

发送缓冲区可以被分为已发送已确认这部分数据可被覆盖,就叫做从tcp缓冲区中移除它了;已发送未确认可以发/已经发,但是未收到应答的区域,这部分区域称为滑动窗口,滑动窗口是发送缓冲区的一部分,这部分区域的范围是对方接收窗口(目前);待发送。

这里区域划分是通过双指针来维护的,win_start,win_end;窗口滑动,本质就是指针右移,所以它是通过指针/下标进行划分的。

问题1:如果丢包了怎么理解滑动窗口?

确认序号的定义:确认序号是x,x之前的报文我们全部收到了。那么问题来了如果我们是中间的报文丢了确认报文ACK,后面的报文收到了ACK

win_start直接增加到5001,5001丢了win_start增加到4001,如果2001是数据丢了,我们的3001的ACK报文回复报文填写2001,保证了滑动窗口,线性的连续的向后更新,不会出现跳跃的情况。之后我们需要补发报文

上面的策略称为快重传,已经有了快重传,为什么还要有超时重传呢?快重传是有条件的,需要接收到三个相同的ACK,才会重传。超时重传是兜底的,而快重传是保证效率的。接近末期时没有三个ACK了,就需要用到超时重传。

问题2:滑动窗口能不能向左移动,向右移动,大小会变化吗,怎么变化,会为0吗?

不可以向左移动。

向右移动:

滑动窗口不能超过对方的接收缓冲区的剩余空间的大小。窗口的大小会皆顾对方的接收缓冲区的大小。滑动窗口是动态变化的!

        相应的会有三种情况。可以范围扩大也可以范围缩小

        变大:左右都移动,范围扩大

        变小:右边不变,左向右移动

        不变:

        int start = 根据确认序号设置,start = 确认序号

        int end = 确认序号 + 窗口大小。重点是理解上述的三种情况,窗口的大小由对方的接收缓冲区的大小确定。流量控制是通过滑动窗口实现的。

会变为0,说明对方的接收缓冲区已经被打满了。     

问题3:滑动窗口,会在发送缓冲区中越界吗?

        TCP采用了类似环状队列的算法。

11、延迟应答

发送方一次发送更多的数据,发送的效率就越高。

发送方一次发送更多的数据,对方告诉我他能接收更多的数据。

如果接收方,给发送方通告一个更大的窗口大小(TCP大小)
如何让接收方给对方通告一个更大的窗口呢?

收到报文不着急应答。等一等,上层较为充分的时间,来取走数据。就可以通告一个最大的窗口。目的是为了提高发送效率。

比较推荐的做法,每次都尽快通过read,recv尽快的把数据全部从内核中拿上来。

12、捎带应答,本质还是要提高效率。

13、拥塞控制:

TCP几乎所有的策略,起作用的都是两端机器上的!TCP还替我们考虑了网络。

如果发送数据,出现问题,不仅仅是对方主机出现问题。也可能是网络出现了问题!

       1、如果通信的时候,出现了少量的丢包?

        常规情况,对方来不及接收

        2、如果通信的时候,出现了大量的丢包?

        大量丢包,网络出现了问题,硬件设备出问题,数据量太大,引起阻塞。

如果通信双方出现了大量的数据丢包问题,tcp会判断网络出问题了(网络拥塞了)。大量的数据都超时了。我们的发送方,应该怎么办?我们不能立即对报文进行超时重发。为什么?这样做会加重网络的拥塞。

我们用TCP协议是实现了多主机面对网络出现拥塞时的 "共识",因为我们的网络资源也是共享资源。

拥塞控制的策略,每台识别到了网络拥塞的机器,都要做拥塞控制。具体的策略如下:

虽然 TCP 有了滑动窗口这个大杀器 , 能够高效可靠的发送大量的数据 . 但是如果在刚开始阶段就发送大量的数据 , 仍 然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵 . 在不清楚当前网络状态下 , 贸然发送大量的数据 , 是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制 , 先发少量的数据 , 探探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据 ;

引入一个概念拥塞窗口:发送开始的时候,定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送的窗口;所以滑动窗口大小 = min(窗口大小对方主机的接收能力,拥塞窗口,考虑的是动态的,网络的接收能力) 滑动窗口,接收窗口,拥塞窗口。三个窗口互相配合,保证我们TCP可靠传输。

所以滑动窗口的大小,可以用下面的算法来计算

int start  = ACK。

int end = ACK + min(接收窗口,拥塞窗口)

拥塞窗口就是主机判断网络健康程度的指标,超过拥塞窗口,会引发网络拥塞,否则不会。因为网络是动态的,拥塞窗口本身不是设定值,它是动态变化的。

慢启动只是指初始时慢,但是增长速度快。网络出现拥塞,发送少量的报文,如果都可以,网络已经趋于健康了。应该迅速尽快恢复正常通信了。前期满,增长速度快。

实际机器发送数据量,会一直指数增长吗?绝对不会。超过窗口大小,就按窗口大小规定滑动窗口的大小了。

为了不增长的那么快 , 因此不能使拥塞窗口单纯的加倍 .
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候 , 不再按照指数方式增长 , 而是按照线性方式增长

TCP 开始启动的时候 , 慢启动阈值等于窗口最大值 ;
在每次超时重发的时候 , 慢启动阈值会变成原来的一半 , 同时拥塞窗口置回 1
慢启动的阈值:最近一次发生网络拥塞的拥塞窗口的大小的一半。
虽然图上是那么画的,但是我们的网络不可能那么的拥塞。对方的接收缓冲区的大小可能不会造成网络拥塞。

14、面向字节流

用户只关心应用层协议。不关心网络通信的细节!
由于缓冲区的存在 , TCP 程序的读和写不需要一一匹配 , 例如 :
 
100 个字节数据时 , 可以调用一次 write 100 个字节 , 也可以调用 100 write, 每次写一个字节 ;
100 个字节数据时 , 也完全不需要考虑写的时候是怎么写的 , 既可以一次 read 100 个字节 , 也可以一次read一个字节 , 重复 100 ;

 15、粘包问题

粘包问题,相对于用户层的概念,解决粘包问题需要定制协议,也就是网络版本计算器的Request和Response类。

首先要明确 , 粘包问题中的 " " , 是指的应用层的数据包 .
TCP 的协议头中 , 没有如同 UDP 一样的 " 报文长度 " 这样的字段 , 但是有一个序号这样的字段 .
站在传输层的角度 , TCP 是一个一个报文过来的 . 按照序号排好序放在缓冲区中 .
站在应用层的角度 , 看到的只是一串连续的字节数据 . 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分 , 是一个完整的应用层数据包.

导致粘包问题的原因是 ,TCP没有任何标识它一个数据包的大小的字段,有的只是首部字节长度。所以数据部分有多长我们就不得而知了,这就需要我们对应用层的数据包进行特殊的处理,使得对方可以清楚的知道每一个数据包的边界。从而继续编写上层的数据包处理逻辑等。

对于定长的包 , 保证每次都按固定大小读取即可 ; 例如上面的 Request 结构 , 是固定大小的 , 那么就从缓冲区从头开始按sizeof(Request) 依次读取即可 ;
对于变长的包 , 可以在包头的位置 , 约定一个包总长度的字段 , 从而就知道了包的结束位置 ;
对于变长的包 , 还可以在包和包之间使用明确的分隔符 ( 应用层协议 , 是程序猿自己来定的 , 只要保证分隔符不和正文冲突即可);

解决粘包问题后,再对数据包反序列化。 

因为UDP有UDP报文长度的字段,所以UDP没有任何的粘包问题。 

15、TCP异常情况 

进程终止:和文件是直接相关的!文件的生命周期是随进程的。所以连接的生命周期也是随进程的。有用户connect,然后OS自动完成的!都是进程结束。进行正常的四次挥手即可。进程终止,连接正常自动断开。

机器重启:先要杀掉所有的进程,所以这和进程终止的处理是相同的。

机器掉电/网线断开:客户端和服务器连接认知不一致问题,服务器向客户发送rst报文。服务器启动保活机制。

理解一下:打通一下文件和socket的关系

这里需要看一下Linux系统内核是怎么搞的。我学完网络再看,再解释。切入点是struct file结构体。这个结构体就是inode所在的结构体。

接下来进入网络层。

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值