0
点赞
收藏
分享

微信扫一扫

【校招 --阶段二 网络基础】传输层及UDPTCP

彩虹_bd07 2022-03-17 阅读 69

一、再谈端口

端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在这里插入图片描述
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看);
在这里插入图片描述
在这里插入图片描述
端口号范围划分

  • 0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的.
  • 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.

认识知名端口号
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:

执行下面的命令, 可以看到知名端口号

netstat(面试高频考点)

netstat是一个用来查看网络状态的重要工具.

语法:netstat [选项]
功能:查看网络状态

pidof

在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id

长短连接

在这里插入图片描述
当数据交换传输的是网页,假如一个网页包含很多数据,一次response不能将全部数据传输过去,但是需要将很多数据传输过去,这就需要很多次response。对于短链接一次请求响应就断开连接,所以就要建立很多次连接,但是建立连接是需要成本的,所有短链接效率特别底下。相反对于长链接,再一次连接你response很多次,可以把这些数据在一次连接中传输过去。

二、UDP协议

UDP协议格式


对于udp协议,它的报头为8字节的定长报头,所以在分离报头和有效载荷可以更容易分离,然后向上交付。

有效载荷长度=16位UDP长度-UDP报头长度(8字节)
UDP怎么保证UDP报文完整性->16位UDP长度

UDP的缓冲区-

  • UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
  • UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致(发送的可能是有序的,但是因为网络有的延迟先走的后到这也是有可能的); 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

UDP的socket既能读, 也能写, 这个概念叫做 全双工
UDP的特点

面向数据报

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
用UDP传输100个字节的数据:

基于UDP的应用层协议

三、TCP协议(可靠性,效率)

可靠性:

效率:

问题:

  1. 为什么会具有可靠性问题?
    网络传输线路更长,网络延迟

  2. 不可靠都有哪些问题?
    丢包,乱序,数据包错误、接受缓冲区

TCP协议格式

在这里插入图片描述

怎么将有效载荷和报头分离?
标准的TCP协议宽度位4字节,因为TCP报头的标准长度位20字节,所以先读取20字节当拿到4位首部长度,而首部长度*4就是报头长度,所以根据首部长度就能分离报头。TCP报头是浮动,标准的TCP报头是20字节。
TCP报头和UDP报头相比少了一个报文长度,为什么呢?
因为TCP协议不需要按数据块交付给上层,当收到数据放到缓冲区中它是面向字节流的,只要按照顺序放好就行,上层怎么读是上层协议的事情。

考点:
1. 为什么TCP报文会有序号和确认应答序号为什么会有两个序号?
序号是为了TCP报文按序到达,确认应答序号是为了发送方确认对方收到了,TCP通信是全双工通信可能。
2. 怎么解决丢包问题
确认应答机制

确认应答(ACK)机制(可靠性)

在这里插入图片描述

TCP将每个字节的数据都进行了编号. 即为序列号.
在这里插入图片描述

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.ACK只有不需要再次ACK

超时重传机制

在这里插入图片描述

但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
在这里插入图片描述
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.

那么, 如果超时的时间如何确定

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

连接管理机制

在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接服务

三次握手:

在这里插入图片描述

5次握手,7次握手都可以建立连接但是3次握手是最小成本建立连接,因为每次连接都需要成本的。

四次挥手:

四次挥手状态转换图
在这里插入图片描述

四次挥手状态是怎么转换的
当client发送FIN后状态变为FIN_WAIT_1,server收到并立马发送ACK,server状态变为CLOSE_WAIT,client收到来自server的ACK后状态由FIN_WAIT_1变FIN_WAIT_2.这就认为client到server的通信信道关闭了(client的应用层不在给server发送数据了client->server的应用层的通信是close的)。这还需要将server到client得通信信道关闭,所以server接着给client发送FIN,server状态变为LAST_ACK,client收到FIN状态由FIN_WAIT_2变为FIN_WAIT.接着client发送ACK到server,server状态LAST_ACK变为CLOSED将连接关闭,再接着client状态由FIN_WAIT变为CLOSED

TIME_WAIT:主动断开连接地一方,要进行等待?原因在于如果没有TIME_WAITclient就没等当client把最后一次ACK发出去马上CLOSED,但是不能保证server收到ACK,当ACK丢失了,server会超时重传再一次发送FIN请求关闭连接,但是clilent已经CLOSED不能收到FIN,所以连接就无法关闭。所以client就必须等待server关闭链接,当client在等的一段时间内没有收到FIN认为链接已经关闭就自己切换状态。尽量保证最后一个ACK被对方收到,继而释放server的资源。等待历史数据在网络上消散。通常TIME_WAIT:2MSL(MSL最大重传时间)

下图是TCP状态转换的一个汇总:

在这里插入图片描述

理解TIME_WAIT状态

现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是:
在这里插入图片描述
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口.我们用netstat命令查看一下:
在这里插入图片描述

想一想, 为什么是TIME_WAIT的时间是2MSL

解决TIME_WAIT状态引起的bind失败的方法

在这里插入图片描述
在这里插入图片描述

server因为是主动断开连接所以状态是TIME_WAIT
主动断开地一方进入TIME_WAIT状态,链接没有断开,端口还是被占用着所以会引起bind失败。过一段TIME_WAIT时间之后就可以重新连接了。

在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的

端口复用:
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符。当server主动断开连接可以立马又重新链接
在这里插入图片描述

理解 CLOSE_WAIT 状态

以之前写过的 TCP 服务器为例, 我们稍加修改
测试代码:

#include<iostream>
#include<sys/wait.h>
#include<netinet/in.h>
#include<cstdio>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cstdlib>
#include<sys/socket.h>
using namespace std;
class sever{
  private:
    int listen_sock;//监听套接字
    int post;
  public:
    sever(int post_){
      post=post_;
    }
    void Init(){
      listen_sock=socket(AF_INET,SOCK_STREAM,0);
      if(listen_sock<0){
        cout<<"套接字创建失败!"<<endl;
        exit(0);
      }
      struct sockaddr_in addr;
      addr.sin_family=AF_INET;
      addr.sin_port=htons(post);
      addr.sin_addr.s_addr=INADDR_ANY;
      if(bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr))<0){
        cout<<"绑定失败!"<<endl;
        exit(1);
      }
      if(listen(listen_sock,5)<0){
        cout<<"绑定失败!"<<endl;
        exit(2);
      }
    }
      void star(){

    while(1){
        
        struct sockaddr_in in_addr;
        socklen_t len=sizeof(in_addr);
        int sock=accept(listen_sock,(struct sockaddr*)&in_addr,&len);
        if(sock<0){
          cout<<"获取链接失败!"<<endl;
          continue;
        }
        char s1[16];
        
        sprintf(s1,"%d",ntohs(in_addr.sin_port));
        string IP_PORT=inet_ntoa(in_addr.sin_addr);
        IP_PORT+=":";
      
        cout<<"get a new link"<<IP_PORT<<s1<<endl;
        int id=fork();
        if(id==0){
          close(listen_sock);
          service(sock);
        exit(0);
        }
        close(sock);
        
       // waitpid(-1,NULL,0);
      }
      }
       void service(int sock){
         signal(SIGCHLD,SIG_IGN);//线程分离
          char buff[1024*2];
        int s=recv(sock,buff,sizeof(buff),0);
     		if(s>0){ 
          buff[s]=0;
          cout<<buff<<endl;
          string response="HTTP/1.0 200 OK
";
          response+="Content-type: text/html
";
          response+="
";
          response+="
        <!DOCTYPE html>
        <html>
        <head>
        <meta charset="utf-8">
        <title>helloword</title>
        </head>
        <body>
        <h1>hello word</h1>
        </body>
        </html> ";
        send(sock,response.c_str(),response.size(),0); 
        }      while(1){

        }
    //close(sock);  
    }
      ~sever(){
        close(listen_sock);
      }
};
int main(int argc,char*argv[]){
sever s(atoi(argv[1]));
s.Init();
s.star();

}

在这里插入图片描述
此时服务器进入了 CLOSE_WAIT 状态, 结合我们四次挥手的流程图, 可以认为四次挥手没有正确完成.
小结: 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.

滑动窗口

刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.
这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.

在这里插入图片描述
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时
间重叠在一起了).
在这里插入图片描述

在这里插入图片描述
如何理解滑动窗口?
在这里插入图片描述

发送方怎么知道对方地接受缓冲区大小的?
在建立连接时,双放互相发送报文,报头中含有16位地窗口大小,这是接收缓冲区地大小。
在这里插入图片描述

那么如果出现了丢包, 如何进行重传 这里分两种情况讨论.
情况一: 数据包已经抵达, ACK被丢了.
在这里插入图片描述

这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;
情况二: 数据包就直接丢了.

![](https://img-blog.csdnimg.cn/096347fbc8bc43efb0c5f01754446c74.pngx-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6Ieq6aaW55qE5bCP5YG3,size_20,color_FFFFFF,t_70,g_se,x_16)

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,
就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制

在这里插入图片描述
窗口探测和窗口更新通知是通信细节层不关心(recv函数获取对方)。

当接收方缓冲区满了,发送方就要等待在,
这时上层在干什么?上层的行为取决于发送缓冲区大下,如果满了就等,美满就放数据。通信细节上层不关心。

拥塞控制

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

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快.

在这里插入图片描述

少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案

延迟应答

发送方一次发多少数据由接收方的接受缓冲区大小决定,当接收方的接收缓冲区越大,通信效率就越高,一次ACK就把接收方的接收缓冲区大小报给发送方来刷新窗口大小。所以在传输一次数据之后,先不着急ACK就等待上层取走缓冲区数据,这样等待时间在一定程度上越长缓冲区的ACK携带接受放的接受缓冲区的大小就越大,刷新的发送方滑动窗口就越大,通信效率就越高。
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输
效率;

那么所有的包都可以延迟应答么 肯定也不是;

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms

在这里插入图片描述

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说
了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”;
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
在这里插入图片描述

面向字节流

创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次
read一个字节, 重复100次;

粘包问题

那么如何避免粘包问题呢 归根结底就是一句话, 明确两个包之间的边界.
因为HTTP是基于TCP协议,当把拿到TCP报文分离出TCP报头剩下的是HTTP报文,然后上交给应用层,HTTP报头由的HTTP有效载荷长度,可以直接提取到有效载荷,解决粘包问题

思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢

TCP异常情况

进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即
使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ
断线之后, 也会定期尝试重新连接.

基于TCP应用层协议

用UDP实现可靠传输

参考TCP的可靠性机制, 在应用层实现类似的逻辑;

lisen第二个参数

listen第二个参数当底层连接比较多而无法处理的底层连接的数量(全连接队列)
对于服务器, listen 的第二个参数设置为 5, 并且不调用 accept

测试程序:

#include<iostream>
#include<sys/wait.h>
#include<netinet/in.h>
#include<cstdio>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cstdlib>
#include<sys/socket.h>
using namespace std;
class sever{
  private:
    int listen_sock;//监听套接字
    int post;
  public:
    sever(int post_){
      post=post_;
    }
    void Init(){
      listen_sock=socket(AF_INET,SOCK_STREAM,0);
      if(listen_sock<0){
        cout<<"套接字创建失败!"<<endl;
        exit(0);
      }
      struct sockaddr_in addr;
      addr.sin_family=AF_INET;
      addr.sin_port=htons(post);
      addr.sin_addr.s_addr=INADDR_ANY;
      if(bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr))<0){
        cout<<"绑定失败!"<<endl;
        exit(1);
      }
      if(listen(listen_sock,5)<0){
        cout<<"绑定失败!"<<endl;
        exit(2);
      }
    }
 
      ~sever(){
        close(listen_sock);
      }
};
int main(int argc,char*argv[]){
sever s(atoi(argv[1]));
s.Init();
while(1){

}
}

此时启动 8 个客户端同时连接服务器, 用 netstat 查看服务器状态, 一切正常.
但是启动第七个客户端时, 发现服务器对于第七个连接的状态存在问题了
在这里插入图片描述

客户端状态正常, 但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:

  1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
  2. 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)

而全连接队列的长度会受到 listen 第二个参数的影响.
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.

举报

相关推荐

0 条评论