计算机网络(传输层)

发布于:2024-05-19 ⋅ 阅读:(120) ⋅ 点赞:(0)

应用层的数据 并不是直接发送到网络而是交给了传输层,比如通过write拷贝到tcp的缓冲区,接下来数据什么时候发、怎么发、出错了怎么办完全由传输层去做决定。 端口号是包含在输层的概念,上层进程绑定某些端口号之后,传输层收到指定端口号的报文,有传输层上交给与该端口号绑定的指定进程。一个进程可以bind多个端口号,但是一个端口号一般只能bind一个进程。

netstat

所谓的udp就在内核里面,内核是用c语言实现的,所谓的报头就是结构体,添加报头就是再定义结构体或位段对象,在里面填数据,或者申请空间对空间对强转,得到报头之后向里面填充字段。所谓封装就是将有效荷载和报头拷贝在一起。解包就是将报头和载荷区分开来,然后根据报头的字段来解析后面的内容。以后网络里面的所有报头理解称为结构体就可以了。定义协议就是结构化数据。应用层上面我们不直接用结构体,在应用层上有一个序列化和反序列化,因为我们应用层的协议是随时随地都可能发生变化的,tcp/ip协议一旦订好了就不太会变化,所0到以在设计上由于应用层一直再变化,所以我们上层为了提供开发效率高的方案,需要自定义协议结合序列化和反序列的方案来实现,而内核的所有协议采用的是结构体字段来构建报头的。

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并; 用UDP传输100个字节的数据:如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节; 而不能循环调用10次recvfrom, 每次接收10个字节。udp比较简单,不用建立连接也就是意味着服务器不用维护连接。

4位首部长度是tcp报头的长度,其中包含了选项,单位是4个字节,取值范围是[0,15]也就是0到60个字节,但是tcp我们无法得知有效载荷的长度,确实无法得知,这是和tcp面向字节流有关。 不可靠的现象:丢包、乱序、重复、校验失败、发送太快、发送太慢、网络出问题。解决丢包问题的前提是我们需要知道我们丢包了。

tcp的应答机制:c要是等待了一定的时间没有收到ack应答 就判定报文丢失,对于ack是不需要进行再次应答的。

 一次发送多条报文,每条报文都要进行ack应答。

为什么32位序号和32位确认序号不能整合在同一个字段内?因为这样做可以让一套s->c的报文可以是s->c的ack(需要确认序号)以及s->c发送数据(需要序号)的结合,叫做捎带应答,可以增加通信的效率。

应用层的工作是将数据通过send接口将数据从应用层拷贝到传输层tcp的发送缓冲区就直接返回了,而从本端缓冲区拷贝到对端缓冲区的过程是由tcp协议来负责的。如果发送数据太快了,对端接收缓冲区当被填充满了之后后面的报文都会被丢失掉,这样会造成资源的浪费,虽然有应答机制不会丢包,但是浪费了很多不必要的资源。16位窗口大小会填上剩余空间的大小,本端接收到之后可以决定后续还可以向对端发送多少数据,这叫做流量控制,当缓冲区空间剩余很大时,流量控制也是可以提速信息传输的效率的。校验和如果校验失败,该报头和以及有效荷载全部都会被丢弃,这叫做真正意义的丢包。

每一个标志位只占用一个比特位, ACK是确认应答报文,当接收到该报文的时候,请多多关注一下我的确认序号,当然该报文可能会携带数据。SYN是一个连接请求的报文,3次握手,1、SYN;2、SYN+ACK;3、ACK。FIN:是一个连接断开的请求报文,4次握手。

PSH :提示接收端应用程序尽快从TCP缓冲区把数据读走。

RST:是对连接进行重置,双方建立连接的时候可能因为一些原因连接建立不成功,或者连接建立成功之后因为一些原理双方连接不一致的情况(比如说有一端网络断了,导致连接失败,但是另外一端认为连接依旧保持正常),这时候接收到该标志位报文的一端,需要将老的程序断掉了,然后重新建立连接。

URG:告诉接收端    我们报文携带的有效载荷里面是有部分紧急数据的,需要插队提前处理,并告知我的16位紧急指针的位置是有效的(说明平常这个字段是无效的)。只有具有定位功能的数据,在宏观上我们都可以叫做指针。16位紧急指针是紧急数据在有效载荷部分的偏移量。常规数据入队列,紧急数据直接插队通过特殊接口交给上层。注意紧急数据只有一个字节的大小,可以在正常传递数据的途中新开辟一条通信链路(用的还是之前的连接)但可以只发一个字节的数据,通过状态码的方式给对方发送控制指令来控制数据传输的相关细节。所以紧急指针可以叫做带外数据。 

标志位的本质是报文的类型,因为不同类型的报文我们对应不同的动作。 

应答机制:

tcp的可靠性在于当我们给对端发数据的时候,如果我们接收到了ack应答,说明上次数据已经成功发送,同时如果超时没有收到ack,也就是我们判断上次数发送失败这也是可靠性的体现。在通过send/write函数接口将数据从应用层拷贝到tcp的发送缓冲区的时候,tcp并不需要关注这些数据是说明,只把它当成二进制数据流。tcp发送缓冲区可以看成一个char sendBuffer[],这并不是说我们要把数据当成字符来看,而是说将数据以8个比特位也就是1个字节为一个最基本的单位进行传输,如果主机a将数据d发送给主机b,但是主机a没有收到该数据d的ack,主机a对该数据d进行超时重传,那么主机b就会收到重复的数据d,所以主机b还要根据序号进行报文去重。由于超时重传的存在,主机a是不能发送完数据d之后就将数据d从tcp的发送缓冲区给清掉的,还需要维护一段时间,直到收到数据d的ack或者进行超时重传。超时重传的时间是和当前的网络环境息息相关的,所以只能动态计算的。

三次连接:

建立连接的过程我们要进行三次握手。服务端accept并没有参与三次握手,而是一直在阻塞等待直到三次握手成功。客户端的connect也是需要在三次连接建立成功之后才可以返回。如果三次连接建立的过程中第三次连接发送失败,那么客户端会认为三次连接建立成功、而服务端认为三次连接未重新建立,这时候双方不一致,客户端接下来会给服务端发送数据(因为它认为三次连接已经建立好了),此时服务端如果收到该数据报文之后就明白客户端误认为连接建立成功了,服务端就可以给客户端发送连接重置RST,这是客户端可以把连接关掉进行重新连接。所以三次连接是在赌,赌第三次ACK是否会被收到。但这是小概率事件,大多数情况,客户端发送第三次ACK,服务端不给客户端发送RST,就说明当前连接成功建立了。虽然存在失败的机率,但是大不了重新连接就是的了。如果两次握手的话对于tcp来说,存在重打的漏洞,让服务器非常容易受到AYN洪水攻击。如果是四次握手或者说是偶数次握手,由于连接有一定概率异常,尤其是服务器面多多个客户端,连接异常的情况一定是时常发生,承担最后一次握手连接异常的成本会嫁接到服务器端,那服务器端面多多个客户端,那么会导致服务器维护这种异常连接称为必然,不利于服务器。三次握手的过程是由双方操作系统自动完成的。

四次挥手的原因是因为需要征得客户端和服务器双发方的同意,双方都认为互不来往了这才叫做连接断开,四次挥手是可靠的建立这种共识的最小成本。客户端在四次回收完成之后会进入到time_wait状态一端时间,才会close是为了保证它发送的最后一次ack尽量要被服务器收到。如果客户端close fd但是服务器不close fd,客户端会进入到FIN_WAIT_2状态,等待服务器发送FIN或者发送数据,但是如果这段时间服务器一直不给客户端发送数据,客户端也不会等你太久就直接close掉了,但是服务器会比较长的时间处于CLOSE_W AIT的状态,此时客户端已经close了,所以服务器维系这种状态不仅浪费资源而且没有实际意义,所以服务器在通信的时候一定不要忘记关闭fd。一段时间之后当服务器在想客户端FIN的时候,由于客户端早已经关闭了,一定不会对该FIN进行ACK,所以服务器会重发几次FIN然后没有收到ACK自己就close掉了。

服务端和客户端任意一端,做为主动断开的一端会维护一端时间的TIME_WAIT临时状态,如果是客户端主动发起的FIN,那么客户端会维持一端使劲啊的TIME_WAIT状态,而客户端在接收到第四次回收ACK之后可以立马close。

之前当我们启动服务器并关闭的时候是不能立马再次启动服务器的,必须要还端口号才能立马重新启动,这是因为服务器做为主动发起FIN的一发会保持一端时间的TIME_WAIT的状态,在这个状态下服务器的端口号和ip地址是依旧被使用的过程,虽然此时服务器的进程的以及被终止掉了的。解决方法:

    void Socket()
    {
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        // 设置地址是复用的
        int opt = 1;
        setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
        //int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);对sockfd该套接字,在level层(tcp层、ip层、还是那一层),optname通常会写成SO_REUSEADDRip地址复用,SO_REUSEPORT端口号复用,opt为1设置进去,说明optname设置为真。
    }

 为什么我们要TIME_WAIT状态,TIME_WAIT要等待2MSL的时间,MSL是TCP报文的最大生存时间,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失,保证第四次回收这个报文可靠到达。

流量控制:第一次发送报文的时候我们是不知道对端接收缓冲区的大小,如何保证第一次发送数据不是太大也不是太小。 双发在进行tcp通信的时候并不是双方进行tcp报文第一次交换的时候,双方在进行三次握手的时候就至少进行过一次报文的交换的,客户端在进行连接建立请求的时候,除了SYN标志位被置为1,客户端也一定要将16位的窗口大小通告给服务器,服务器就知道客户端的承载能力,而第二次握手的时候客户端就可以得知服务器的承载能力。所以一开始进行通信的时候我们就不用担心发快或者发慢的问题。如果我们通过16位窗口得知对端承载能力已经到了极限的时候,我们就需要停止发送了,但是接下来我该如何决定下一次发送消息的时间呢,这时发送方会进行定期的窗口探测(也就是发送tcp报头不携带数据再通过对方的ACK来获知对方的当前的承载能力),对端也可以给我主动发送窗口更新的通知。

  滑动窗口: 

tcp通信是发送报文再给一个应答的过程,在发送报文和确认应答的过程中,可以是上面左边的工作模式,但是也可以是右边的工作模式,一次发送多个报文,对方再给客户端进行多并发的响应。这样就把报文的发送的时间进行重叠了,从而提高了发送的效率。

 滑动窗口是不能向左的,只能向右滑动。滑动窗口是可以变大的(也就是让windend变大一点容纳更多的发送区间),同样也可以变小(winend不动,winstart向右移动)。滑动窗口可以变成0,意思是对方不能再接收数据呢,这时候就进入到了窗口探测和窗口更新阶段了。滑动窗口的大小目前是根据对方的接收能力也就是响应报头中wind的大小。tcp为了可靠性必须要保证按序应答,应答序号seq就是说明seq之前的报文已经接受了,下次请从seq开始发送,也就是让winstart=seq,应答的win就是让winend=winstart+win,从而实现窗口的向右移动。

比如如果我们发送了1-5000个字节的数据,分成5个报文,每个报文的数据大小是1000个字节。当第一个包的ACK如果丢失了,由两种情况,情况1:第一个包已被服务器成功接收,但是其ACK再传输过程中丢失了,2000、3000、4000、5000这四个报文对应的ACK里面的序号里面2001、3001等,这个序号的意思不是说1000-2000或者2000-3000的报文我收到了,而是说2000之前的报文或者3000之前的报文我收到了,当我没有收到1001但是收到2001的ACK序号,我知道1-1000的包对方一定收到了,只不过是其ACK丢失了。情况2:如果是数据真的丢失了,加入丢的是1000-2000的数据报文,那么接收方虽然收到了其余4000个字节的数据包,但是接受方发给客户端的ACK序号只能写1001也就,因为确认序号是告诉我们该序号之前的数据都接收到了,所以客户端接收的多条ACK的确认序号是重复的,这时候客户直到是存在丢包情况了,这时候滑动窗口左端winstart=1000然后也不会向右移动疑问没有收到1000-2000这个数据包的ACK,滑动窗口这时候可以理解称为在进行等待,等待进行超时重传。快从传是当客户端收到3个确认应答的确认序号是相同的时候会进行重传,当时当我们传递报文只有一两个的时候,只能进行上述的超时重传。所以超时重传和快从传并不矛盾,快重传是一个强调效率的方式,而超时重传是对快重传进行兜底。

之前说的数据要支持重传,就必须被暂时保存起来,保存的位置就是滑动窗口中。滑动窗口里面的数据也是客户端并发发送的数据区间。发送缓冲区被设计称为环状结构,这也是历史数据可以被覆盖的原理,winstart和winend也就不存在越界的说法。响应当中的确认序号不是对你收到的报文做确认,而是对收到的报文以及之前的所有报文做确认。滑动窗口内的数据是准备发送的数据或者已经发送的数据但是没有收到对应的应答。

拥塞控制:

当存在少量丢包情况的时候,发送发可以采用重传策略。但是当存在大量丢包的时候,这时候很可能是网路出现了问题(不考虑网络瘫痪,这里仅仅考虑网络拥塞就是网络报文太多了,来不及传输处理),这时就不是客户端和服务器端可以控制解决的问题,那此时客户端应该采用什么样的发送策略呢?如果此时依旧采用快充穿或者超时重传的策略,只会加重网络拥塞的情况。

当发生网络拥塞的时候,发送端要基本得知网络拥塞的情况,必须要进行网络的探测。也就是先尝试发一个包、两个包、四个包等,不断的进行尝试。我们不能一直采用线性增长的方式来探测拥塞窗口的大小,因为后期单次增长的空间太大了以至于会对拥塞窗口的代下掌握不准确,所以前期采用指数增长后期采用线性增长,这中间通过慢启动的阈值来进行划分,慢启动的阈值也是会不断根据上次探测拥塞窗口的上限进行调整的。拥塞窗口的大小不代表客户端发送量的大小,因为还要考虑接收端的能力。

延迟应答:

接收端在收到一个报文之后,在不超过超时重传的时间内多等待一会再进行应答,当然再等待的过程中接收端可能在继续接收数据,所以接收端也可以将多个报文的应答合成一个应答,在等待的期间可以然应用层有更多的时间进行读取,

tcp的工作基本模式主要时1:基于滑动窗口的多数据并发访问;2:每个报文基本上都是捎带应带的报文。校验和保证的交上去的数据一定时没有问题的。序列号是为了抱保证数据按需达到,并且通过该序列号进行去重。只有收到确认应答的报文才能保证百分百的被对方接收到。tcp是面向连接的,在通信的时候必须建立连接,双方在建立连接握手的时候本身就是就是在交换报文的过程,可以在这个期间交换通信的时候需要的属性数据。滑动窗口并发发送数据,是多个数据包的发送在时间上重叠。用户只需要将数据拷贝到tcp的发送缓冲区,接下来这个数据什么时候发、如何发、出错了这么处理这些都是双方tcp协议内核操作系统自主决定的,所以tcp叫做传输控制协议。粘包问题再tcp层不存在,因为再tcp层是没有一个完整数据包的概念,这是应用层需要解决的问题。udp是否存在粘包问题呢?udp包头里面有udp定长报头,在加上udp长度字段,所以当一个udp报文被收取到的时候,udp是知道独立报文长度是多少的。我们只需要将一个完整的请求扔到udp的有效载荷里,在读取得时候是一定可以读到一个完整得请求的,然后直接可以向上交付,udp不用解决粘包问题,只需要收发数据,然后对收到的数据进行反序列化提取字段即可。tcp报头没有有效载荷的长度,因为不需要,这和tcp面向字节流是强相关的,当我们收到tcp的报文的时候,首先这个报文决定不会出错,因为有校验和,而且报文一定是有序的,所以收到报文之后只要将其有效荷载原封不懂得放到tcp的缓冲区里面就可以了,至于报文的边界如何处理由应用层取解决。 

进程一旦崩溃了,该进程曾经创建的连接、打开的文件、申请的空间会被os系统全部回收掉。对于不活跃的连接管理,tcp是很难进行处理的,这时候这些连接的管理需要依赖应用层。 

滑动窗口既然涵盖了大量的字节流数据,对方的接受能力就是我窗口的大小,那为什么要把滑动窗口的一大段数据封装称为多个报文呢?拿什么做为一个报文进行发送呢?这样效率难道不是更高的吗? 


网站公告

今日签到

点亮在社区的每一天
去签到