【Linux】Linux网络总结图详解

      • 网络是进行分层管理的
      • 应用层
        • HTTP
        • HTPPS
      • 传输层(UDP、TCP)
        • UDP
        • TCP
        • TCP和UDP对比
      • 网络层
        • IP
      • 数据链路层(MAC)/物理层(以太网)
        • 以太网通信(负责网卡之间)

网络是进行分层管理的


应用层

HTTP

HTTP协议格式(请求和响应)

方法说明HTTP协议版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用⒆道协议连接代理1.1
LINK建立和资源之间的联系1.0
UNLINE断开连接关系1.0

其中我们最常用的为GET和POST方法

GET方法是通过url传参的。
POST方法是通过正文传参的。

类别原因短语
1XXInformational(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错

HTTP常见的Header

  • Content-Type:数据类型(text/html等)。
  • Content-Length:正文的长度。
  • Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
  • User-Agent:声明用户的操作系统和浏览器的版本信息。
  • Referer:当前页面是哪个页面跳转过来的。
  • Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
  • Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。

Cookie和Session
什么是Cokkie?
简单来说,Cookie是用来保存个人信息的,它的保存方法有两种,分别为内存级别文件级别
cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。

  • 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
  • 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。

因为单纯的使用Cokkie是不安全的,所以,我们又引入了Session

session的作用
当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端生成一个对应的SessionID,这个SessionID与用户信息是不相关的。系统会将所有登录用户的SessionID值统一维护起来。

此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。

我们使用SessionID的好处,我们的客户端拿到的只有SessionID,而真正的密码账号在我们的服务器中,从此以后,保护用户信息的任务就落在了服务器上。即使这样在HTTP环境下也是不安全的,我们又引入了HTTPS

HTPPS

再使用http进行信号传输的时候,任然有可能会导致我们传输过程中被不法分子将我们的数据截取进行修改或者监听。

对此,我们HTTPS中又新增加了一层协议层SSL/TLS,主要功能就是进行数据加密、身份验证(安全证书)

加密方式

对称加密

采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,
特征:加密和解密所⽤的密钥是相同的(及时特点也是它的缺点)

使用对称加密的时候,很容易让不法分子从传输过程中进行截取,原因:因为密钥是公开的,所有人都是可以看到和使用的。如图:

非对称加密

非对称加密有两个密钥,一个是公钥,一个是私钥。
两个密钥是不同的,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。

但即便如此,不法分子还是可以从中间进行窃取的,虽然他没有私钥,但是它有公钥,如图:

不法分子篡改过程:

非对称加密+对称加密

因为非对称加密算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,⽽使得加密解密速度没有对称加密解密的速度快,所以如果使用两者结合的方法来进行传输,效率将会提高很多


但是这样,仍然存在中间人的问题,对此我们引入了CA证书

服务端在使⽤HTTPS前,需要向CA机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就⾏了,证书就如⾝份证,证明服务端公钥的权威性

解决数据安全问题(引入证书)


CA证书是什么保证数据安全的
1.通过某种公开的散列函数将数据进行运算得到一个散列值,如果中间人修改数据内容,会导致最后算出来的结果与签名中通过加密保存的散列值不同,可以发现已经修改数据内容。

2.如果中间人要修改签名,客户端在进行使用CA公钥进行解密时,由于CA的密钥只有CA持有,所以中间人修改的签名不能通过CA的公钥进行解密,此时也会发现已经修改内容。

传输层(UDP、TCP)

Socket 接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
  • domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)AF_INET6(IPv6)
  • type:如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
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));
    }
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。
void Bind(const uint16_t &port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
    }
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
  • sockfd:需要设置为监听状态的套接字对应的文件描述符。
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
void Listen()
    {
        if (listen(_sock, gbacklog) < 0)//gbacklog = 32
        {
            logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int sock = accept(_sock, (struct sockaddr *)&temp, &len);
        if (sock < 0)
        {
            logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
        }
        else
        {
            *clientip = inet_ntoa(temp.sin_addr);
            *clientport = ntohs(temp.sin_port);
        }
        return sock;
    }
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
int Connect(const std::string &serverip, const uint16_t &serverport)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());

        return connect(_sock, (struct sockaddr *)&server, sizeof(server));
    }

接收和发送方式

ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socklen_t *fromlen); 

ssize_t 相当于 long int,socklen_t 相当于int
参数说明:

  • sockfd:标识一个已连接套接口的描述字。
  • buf:接收数据缓冲区。
  • len:缓冲区长度。
  • flags:调用操作方式。是以下一个或者多个标志的组合体,可通过“ | ”操作符连在一起(通常设置为0)
  • from 是一个指向sockaddr结构体类型的指针;
  • *fromlen表示my_addr结构的长度,可以用sizeof操作符获得。

返回值说明:

  • 返回值:成功则返回接收到的字符数,失败返回-1.
int sendto (socket s, const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen ) ;

参数说明:

  • s:一个用于标识已连接套接口的描述字。
  • buf:包含待发送数据的缓冲区。
  • en:缓冲区中数据的长度。
  • flags:调用执行方式。
  • to 是一个指向sockaddr结构体类型的指针;
  • 参数tolen表示to结构的长度,可以用sizeof操作符获得。

返回值说明:

  • 成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。

  • buf:数据的存储位置,表示将读取到的数据存储到该位置。

  • count:数据的个数,表示从该文件描述符中读取数据的字节数。
    返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。

  • 如果返回值等于0,则表示对端已经把连接关闭了。

  • 如果返回值小于0,则表示读取时遇到了错误。

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

端口号转换接口

#include <arpa/inet.h>

uint32_t htonl( uint32_t hostlong );
uint16_t htons ( uint16_t hostshort );
uint32_t ntohl(uint32_t netlong );
uint16_t ntohs( uint16_t netshort);

IP转换接口

字符串转整数

int inet_aton(const char *cp, struct in_addr *inp);

参数说明:

  • cp:待转换的字符串IP。
  • inp:转换后的整数IP,这是一个输出型参数。

返回值说明:

  • 如果转换成功则返回一个非零值,如果输入的地址不正确则返回零值。
in_addr_t inet_addr(const char *cp);

参数说明:

  • cp:待转换的字符串IP。

返回值说明:

  • 如果输入的地址有效,则返回转换后的整数IP;如果输入的地址无效,则返回INADDR_NONE(通常为-1)。

整数转IP

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明:

  • af:协议家族。
  • src:待转换的整数IP。
  • dst:转换后的字符串IP,这是一个输出型参数。
  • size:用于指明dst中可用的字节数。

返回值说明:

  • 如果转换成功,则返回一个指向dst的非空指针;如果转换失败,则返回NULL。

port端口问题

1、一个进程是否可以bind多个端口号?

可以( 因为一个进程可以打开多个文件描述符,而每个文件描述符都对应一个端口号,所以一个进程可以绑定多个端口号。)

2、一个端口号是否可以被多个进程bind?

不可以(如果一个进程先绑定一个端口号,然后再fork一个子进程,这样的话就实现了多个进程绑定一个端口号,但是不同的进程绑定同一个端口号是不可以的。)

UDP

特点

  • 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
  • 不可靠传输: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量
    (如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节。)

UDP协议

协议的工作只是用来保证发送数据的正确性和统一的

  • 16位源端口号:表示数据从哪里来。
  • 16位目的端口号:表示数据要到哪里去。
  • 16位UDP长度:表示整个数据报(UDP首部+UDP数据)的长度。
  • 16位UDP检验和:如果UDP报文的检验和出错,就会直接将报文丢弃。

其中udp协议中的报头是定长的8个字节,16位udp长度就是整个udp的长度,两者相减就可以得到数据的大小。我们可以根据定长的报头长度来进行封装和解包。

UDP缓冲区

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

特点

  • 有连接
  • 可靠传输
  • 面向字节流

TCP协议

  • 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
  • 32位序号/32位确认号: 用于可靠传输的TCP协议中的重要字段。
  • 4位TCP报头长度: 表示该TCP头部有多少个32位bit(每一位bit代表4字节);
  • 6位标志位:
    • URG: 紧急指针是否有效(内容加急,不通过接收缓存区,直接交付到应用层)
    • ACK: 确认号是否有效(肯定置一有效)
    • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
    • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
    • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
    • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  • 16位窗口大小: 就是接受缓存区剩余空间的大小(传的是发送端的缓存区大小)
  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分
  • 16位紧急指针: 标识哪部分数据是紧急数据

TCP如何解包?

  • 提取20字节
  • 根据标准报头,提取4位首部长度
  • 读取 [提取4位首部长度*4-20] 字节数据,选项
  • 读完了报头,剩下的都是有效载荷

TCP运行过程

服务端的操作
1.调用socket,创建文件描述符
2.调用bind,将当前文件描述符和ip/port进行网络绑定
3.调用listen启动监听模式,时刻注意是否有客户端发来连接请求。
5.调用accept进行接受以及阻塞。

客户端的操作
1.调用socket,创建文件描述符
2.调用connect, 向服务器发起连接请求;、

确认应答机制

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

  • 发数据1-1000,回复数据1001代表着前1000个数据都收到了。

超时重传机制
发生超时重传的机制会有两种情况:
第一种为从A主机发送到B主机长时间没有收到答复时,此事件称为发生了数据丢包

第二种为确认应答丢失时,会进行超时重传(但是一般多数据发送时,应答丢失是可以忽略的)

这种情况是可以忽略的

流量控制
流量控制的本质就是每次客户端和服务端进行确认应答的时候,服务端会进行将自身的剩余缓冲区空间进行一个上报,服务端如何把窗口大小告诉客户端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
Linux中TCP的流量控制通过滑动窗口机制实现,确保发送方不会以过快的速度向接收方发送数据,从而保证数据传输的可靠性和接收方的正常运行。
滑动窗口
滑动窗口的由来是因为每次单支数据一应一答太慢且占用空间,所以,我们又推出了滑动窗口。

双方在进行TCP通信时可以一次向对方发送多条数据,这样可以将等待多个响应的时间重叠起来,进而提高数据通信的效率。

滑动窗口工作流程:

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 下图的窗口大小就是4000个字节(四个段).

  • 发送前四个段的时候, 不需要等待任何ACK, 直接发送;

  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;

  • 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;

  • 只有确认应答过的数据, 才能从缓冲区删掉;窗口越大, 则网络的吞吐率就越高


既然有优点也就会有一定的缺点,因为一次传输的数据多了也会有一定的丢包情况,面对这种情况,我们是这样解决的

情况一: 数据包已经抵达, ACK被丢了

这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。

情况二: 数据包就直接丢了

  • 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001″一样;
  • 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 – 2000 重新发送;
  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 – 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中。
  • 这种机制被称为 “高速重发控制”(也叫 “快重传”)

拥塞控制
它的引入主要原因是,虽然我们使用滑动窗口进行了数据包的管理,但是还是有一些不足之处的,例如:

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

  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
  • 刚开始拥塞窗口增长速度, 是指数级别的,“慢启动” 只是指初使时慢, 但是增长速度非常快;为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。

此处引入一个叫做慢启动的阈值;

  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长;
  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;

就好像谈恋爱一样,刚开始处于热恋期;感情温度升的非常快,之后感觉升温就慢下来了;最后可能还吵了一架,导致感情瞬间到达极小点,如何男生不断的示好,感情也重新开始升温,可是感情的最后点变低了。(当然也可能直接崩了)

TCP拥塞控制这样的过程, 就好像热恋的感觉。

小结:

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

延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。

  • 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
  • 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。

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

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

捎带应答
例如:我们在进行握手的时候,其实就可以捎带的将16位窗口进行设置。

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

面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区。

  • 调用write时, 数据会先写入发送缓冲区中;
  • 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  • 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
    然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做全双工。
  • 由于缓冲区的存在, TCP程序的读和写不需要一一匹配,。

例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

粘包问题
简单来说,粘包问题就是接收的报文多了后,容易搞混分析错,那么我们如何解决呢?

归根结底就是一句话, 明确两个包之间的边界

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

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

没有,报文是定长的

对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层,就有很明确的数据边界
站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现”半个”的情况。

三次握手

  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示”同意建立连接”; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

四次挥手

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)

  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

    TCP异常问题

  • 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN,它和正常关闭没有什么区别。

  • 机器重启: 它和进程终止的情况相同。

  • 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset, 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放。

  • 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接。

TCP小总结

可靠性:

  • 校验和出错
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 流量控制
  • 拥塞控制

提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答
TCP和UDP对比

TCP 和 UDP 对比
可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 vs 数据报

TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播。

网络层

IP

TCP作为传输层控制协议,IP则提供了一种能力,即将数据从主机跨网络传送到另一台主机的能力。IP不提供任何可靠性机制。可以理解为IP是一个执行者,而TCP是一个决策者。

IP协议报头

  • 4位版本号:指定IP协议的版本,对于IPv4来说就是4
  • 4位首部长度:表示IP报头的长度,以4字节为单位,可以实现报头与报文的分离
  • 8位服务类型:3位优先权字段(已经弃用), 4位TOS字段, 和1位保留字段(必须置为0). 4位TOS分别表示: 最小延时, 最大吞吐量, 最高可靠性, 最小成本. 这四者相互冲突, 只能选择一个,对于ssh/telnet这样的应用程序, 最小延时比较重要; 对于ftp这样的程序, 最大吞吐量比较重要。
  • 16位总长度:IP报文(IP报头+有效载荷)的总长度,用于将各个IP报文进行分离。这个总长度是和UDP报文是相同的,表示整个报文的大小,同时也说明IP报文不是面向字节流的
  • 16位标识:唯一的标识主机发送的报文,如果数据在IP层进行了分片,那么每一个分片对应的id都是相同的。
  • 3位标志字段:第一位保留(保留的意思是现在不用, 但是还没想好说不定以后要用到). 第二位置为1表示禁止分片, 这时候如果报文长度超过MTU, IP模块就会丢弃报文. 第三位表示”更多分片”, 如果分片了的话,最后一个分片置为0, 其他是1. 类似于一个结束标记;
  • 13位分片偏移:分片相对于原始数据开始处的偏移,表示当前分片在原数据中的偏移位置,实际偏移的字节数是这个值× 8 \times 8×8得到的。因此除了最后一个报文之外,其他报文的长度必须是8的整数倍,否则报文就不连续了。
  • 8位生存时间:数据报到达目的地的最大报文跳数. 一般是64. 每次经过一个路由, TTL -= 1, 一直减到0还没到达, 那么就丢弃了. 这个字段主要是用来防止出现路由循环8位协议: 表示上层协议的类型;
  • 16位头部校验和: 使用CRC进行校验, 来鉴别头部是否损坏;
  • 32位源地址和32位目标地址: 表示发送端和接收端;
  • 选项字段(不定长, 最多40字节)。
  • 8位协议:TCP/UDP协议
  • 数据:有效载荷=16位总长度-(4位首部长度)* 4

IP分片
那么如果TCP传给IP的数据太多,导致IP报文太大怎么处理呢?注意IP是面向数据报的,而不是面向字节流的。
这个时候就需要将IP报文进行分片操作了,分片之后由对端的IP层来进行组装。
比如IP的数据是3400个字节,此时就需要对此IP报文进行分片操作:

分片的过程就是将原有的报头提出,作为其中一个子报文的首部,然后将数据分为几个1480部分,并为它们添加报头。
这一过程是IP层做的,与传输层无关,传输层也不需要知道。

分片五大方向

  • 1 分片不是主流
    • 容易出错,TCP协议可以规定不要传太大就不用分片了
  • 2 识别报文和报文的不同
    • a. 16位的标识:不同报文,标识不同。
    • b. 相同报文的分片,标识是相同的。
    • c. 相同的标识放在一起;便于分片组装。
  • 3 具体识别报文是否被分片与是否没有被分片。
    • 三位标志中的 更多分片标识位 + 13位偏移保证的。
  • 4 如何识别分片的开始,中间和结尾三部分???
    • a. 更多分片标志位为0 && 13位偏移为0 = 报文未被分片
    • b. 更多分片标志位为1 && 13位偏移为0 = 报文被分片,此分片为原始报文起始。
    • c. 更多分片标志位为0 && 13位偏移不为0 = 报文被分片,表示最后一个分片。
    • d. 更多分片标志位为1 && 13位偏移不为0 = 报文被分片,表示后面还有分片。
  • 5 异常处理:我们的识别如果组装过程中,任意一个分片丢失,我们都能识别出来。
    • 根据总产度(如果不是起始分片还要减去报头长度) + 13位偏移 = 下一个分片偏移量;两者进行比对。

数据传输过程
数据的分片和组装都是在IP层完成的,上层的传输层和下层的链路层并不关心。

传输层只负责为数据传送提供可靠性保证,比如当数据传送失败后,传输层的TCP协议可以组织进行数据重传。

  • 当TCP将待发送的数据交给IP后,TCP并不关心该数据是否会在IP层进行分片,即TCP并不关心数据具体的发送过程。
  • 当TCP从IP获取到数据后,TCP也不关心该数据是否在IP层经过了组装。

而链路层的MAC帧只负责,将数据从一个节点传送到和自己相连的下一个节点。

  • 当IP将待发送的数据交给MAC帧后,MAC帧并不知道该数据是IP经过分片后的某个分片数据,还是一个没有经过分片的数据,MAC帧只知道它一次最多只能发送MTU大小的数据,如果IP交给MAC帧大于MTU字节的数据,那MAC帧就无法进行发送。
  • 当MAC帧从网络中获取到数据后,MAC帧也不关心这个数据是否需要进行组装,MAC帧只需要将该数据的MAC帧报头去掉后直接上交给上层IP就行了,而至于该数据的组装问题则是IP需要解决的。

因此,数据的分片和组装完全是由IP协议自己完成的,传输层和链路层不必关心也不需要关心。

小结:

  • 分片我们不推荐,因为封装和分片的过程中上层(传输层)不知道。
  • 分片增加了丢包的概率。
  • 解决方法,需要传输层协议规定。

IP网段划分

比如对于192.168.128.10这台主机,它所在的网段号是192.168.128,它的主机号是10。
网络号:保证相互链接的两个网段具有不同的标识。
主机号:同一个网段内,主机之间具有相同的网络号,但是必须有不同的主机号。
同一个网段的两个主机,网段号相同,主机号不同。

通信过程


当一台主机想像另一台主机发消息的时候,会首先查看自己的网络号与当前局域网的网络号是否相同,如果不相同,就会到路由器构成的网络中去查找哪个网络号符合条件,找到符合条件的网络号之后,再到该网络号对应的局域网中根据主机号找到对应的主机。
手动为子网内新增的主机分配节点IP地址时很复杂的事情,有一种技术称为DHCP,能够自动的给子网内新增的主机节点分配IP地址,一般路由器都带有DHCP功能·,因此路由器也可以看做一个DHCP服务器。

IP地址分五大

  • A类 0.0.0.0到127.255.255.255
  • B类 128.0.0.0到191.255.255.255
  • C类 192.0.0.0到223.255.255.255
  • D类 224.0.0.0到239.255.255.255
  • E类 240.0.0.0到247.255.255.255

随着Internet的飞速发展,这种划分方案的局限性很快显现出来,大多数组织都申请B类网络地址, 导致B类地址很快就分配完了, 而A类却浪费了大量地址。

针对这种情况提出了新的划分方案,称为CIDR(Classless Interdomain Routing):

  • 引入一个额外的子网掩码(subnet mask)来区分网络号和主机号;
  • 子网掩码也是一个32位的正整数. 通常用一串 “0” 来结尾;
  • 将IP地址和子网掩码进行 “按位与” 操作, 得到的结果就是网络号;
  • 网络号和主机号的划分与这个IP地址是A类、B类还是C类无关。

总结来说就是:人为定义一个子网掩码,并规定子网掩码从左到右必须是连续的1或者0(通常首位为1),从而根据网段中的主机量来控制网络号和主机号。

  • IP地址与子网掩码做与运算可以得到网络号, 主机号从全0到全1就是子网的地址范围;
  • IP地址和子网掩码还有一种更简洁的表示方法,例如140.252.20.68/24,表示IP地址为140.252.20.68, 子网掩码的高24位是1,也就是255.255.255.0
  • 子网掩码取反就是子网地址的最大号。

特殊的IP地址和私有IP

  • 将IP地址中的主机地址全部设为0, 就成为了网络号, 代表这个局域网;
  • 将IP地址中的主机地址全部设为1, 就成为了广播地址, 用于给同一个链路中相互连接的所有主机发送数据包。
  • 127.*的IP地址用于本机环回(loop back)测试,通常是127.0.0.1。

解决IP地址不足有以下几种方式:

IP地址(IPv4)是一个4字节32位的正整数. 那么一共只有 2的32次方 个IP地址, 大概是43亿左右. 而TCP/IP协议规定, 每个主机都需要有一个IP地址。这么就意味着IP地址就不够用。

解决方法:

  • 动态分配IP地址:当一个设备接入网络之后,再为其分配IP地址,不过对于我们购买的云服务器来说,IP地址基本是一个。
  • NAT技术:能够让不同局域网当中同时存在两个相同的IP地址,NAT技术不仅能解决IP地址不足的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机。
  • IPv6:IPv6用16字节128位来表示一个IP地址,能够大大缓解IP地址不足的问题。但IPv6并不是IPv4的简单升级版,它们是互不相干的两个协议,彼此并不兼容,因此目前IPv6还没有普及。
  • IP地址的复用(即公有IP和私有IP),下面来详细介绍。

在整个的大网络中,我们会发现,不同的局域网中可以有相同IP的主机。同时我们会发现一个路由器有两份IP地址。这是因为路由器是横跨两个网段的,它可以是该局域网的一部分(网络号和局域网一致),也可以作为一台主机和外面的其他路由器共同构成一个局域网(网络号与更大的局域网一致)。

路由

  • 在复杂的网络结构中, 找出一条通往终点的路线;
  • 路由的过程就是一跳一跳的“问路”的过程;

数据在路由的过程中,实际就是一跳一跳(Hop by Hop)“问路”的过程。所谓“一跳”就是数据链路层中的一个区间,具体在以太网中指从源MAC地址到目的MAC地址之间的帧传输区间。

IP数据包的传输过程也和问路一样

  • 当IP数据包, 到达路由器时, 路由器会先查看目的IP;
  • 路由器决定这个数据包是能直接发送给目标主机, 还是需要发送给下一个路由器;
  • 依次反复, 一直到达目标IP地址。

数据链路层(MAC)/物理层(以太网)

数据链路层要解决的问题

  • IP拥有将数据跨网络从一台主机送到另一台主机的能力,但IP并不能保证每次都能够将数据可靠的送到对端主机,因此IP需要上层TCP为其提供可靠性保证,比如数据丢包后TCP可以让IP重新发送数据,最终在TCP提供的可靠性机制下IP就能够保证将数据可靠的发送到对端主机。
  • TCP除了对下层IP提供可靠性机制之外,TCP对上还提供进程到进程的服务,我们在进行socket编程时,本质就是在使用TCP或UDP为我们提供的进程到进程的服务。
  • 但数据在网络传输时需要一跳一跳的从一台主机跳到另一台主机,最终才能将数据转发到目标主机,因此要将数据发送到目标主机的前提是,需要先将数据转发给与当前主机直接相连的下一跳主机,而两台主机直接相连也就意味着这两台主机属于同一网段,因此将数据转发到下一跳主机实际是属于局域网通信范畴的,而这实际就是链路层需要解决的问题。
  • 也就是说,网络层IP提供的是跨网络发送数据的能力,传输层TCP是为数据发送提供可靠性保证的,而链路层解决的则是两台相连主机之间的通信问题。

既让数据链路层解决的问题是局域网通信问题,那么局域网通信方式一共有三种,其中我们主要介绍以太网;三种方式分别为:以太网、令牌环网、无线LAN/WAN

以太网通信(负责网卡之间)
  • 数据在发送之前会先进行数据封装,此时链路层会给数据封装上对应的局域网的报头。
  • 如果数据要进行跨网络传输,那么就需要经过路由器转发。
  • 当数据在路由器进行向上交付时,会将该数据对应的局域网报头去掉。
  • 而当路由器该数据转发给下一跳之前,又会给该数据封装上下一跳网络所对应的局域网报头。

也就是说,网络中的路由器会不断去掉数据旧的局域网报头,并添加上新的局域网报头,因此数据在进行跨网络传输时,就算所需跨越的网络采用的是不同的局域网技术,最终也能够正确实现跨越。

注意,在数据链路层和网络层中间也可能有一些协议,来完成不同的工作。因此数据链路层向上提交的协议不同,添加的报头就不同。
它的总体格式可以分为三种,分别是提交给IP,ARP,RARP。

源地址和目的地址是指网卡的硬件地址(也叫mac地址),长度是48位,是网卡出厂时固化的。

认识MAC和理解他和IP的区别

  • 1 在网络转发的过程中,目的IP不会变。
    • IP地址描述的是路途总体的 起点 和 终点
    • 唐僧从东土大唐去往西天拜佛求经(源IP地址和目的IP地址)
  • 2 MAC帧报头会发生变化。
    • MAC地址描述的是路途上的每一个区间的起点和终点(局域网)
    • 例如:唐僧从女儿国去往狮驼岭(源MAC地址和目的MAC地址)

理解局域网碰撞

  • 我们H1主机给H6主机发送你好吗???其它主机也可以收到,不过根据目的地址发现不是发给自己的就直接丢弃了。
    • 此时我们就可以把局域网理解为一份临界资源,大家都共享的。
  • 我们发数据时,别人也在发;就有可能发生数据碰撞问题。
    • 碰撞避免算法:发生碰撞后发送主机休息随机时间,然后再发送数据。

    • 发送数据时所有主机都可以收到,也包括发送数据的主机。发送主机就可以知道自己的数据是否发生碰撞了。

    • 也可以使用交换机减少碰撞次数,它划分碰撞区域,减少影响。

    • 很容易理解,1号主机向5号主机发送数据出现了碰撞问题,发生在交换机左边;那么交换机就不让它通过;交换机右面部分还可以正常通信。

  • 局域网中主机当然是越少越好。
    • 主机少了不容易发生数据碰撞
  • 局域网的数据帧发送的时候,数据帧长度要适中。
    • 太短了了影响效率
    • 太长了容易发送数据碰撞

理解MTU
在IP中讲过,由于数据链路层有最大的数据承载量(即IP报文不能超过1500字节),该最大承载量称为MTU,因此需要对IP报文进行分片处理。同时也有最小的大小规定,即如果小于46字节要在后面添加补充位。

MTU对UDP影响
UDP是面向数据报的,它是没有缓冲区的:
一旦UDP携带超过1472(1500-20(IP首部)-8(UDP首部)),那么就会在网络层分为多个数据包。这样多个IP数据包有任意一个丢失,都会引起接收端网络层重组失败,那么意味着,如果UDP数据报在网络层被分片,整个数据被丢失的概率就大大增加了。

MTU对TCP协议的影响
TCP是面向字节流的,它是有缓冲区的:
这样就可以通过TCP来控制向网络层发送的数据量的多少了,但是其实TCP的数据长度也不能无限长,它也有一个限制,称为MSS。
TCP在建立链接的过程中,通信双方会进行MSS协商。
最理想的情况下,MSS的值正好是在IP不会分片处理的最长长度(这个长度仍然是受限于数据链路层)。
双方在发送SYN的时候会在TCP头部添加自己能支持的MSS值。
然后双反得知对方的MSS的值之后,选择最小的作为最终MSS。
MSS的值就是在TCP首部的40字节变长选项中(kind=2)。

MTU对IP协议的影响
由于数据链路层MTU的限制, 对于较大的IP数据包要进行分包
将较大的IP包分成多个小包, 并给每个小包打上标签
每个小包IP协议头的 16位标识(id) 都是相同的
每个小包的IP协议头的3位标志字段中, 第2位置为0, 表示允许分片, 第3位来表示结束标记(当前是否是最后一个小包, 是的话置为1, 否则置为0)
到达对端时再将这些小包, 会按顺序重组, 拼装到一起返回给传输层
一旦这些小包中任意一个小包丢失, 接收端的重组就会失败. 但是IP层不会负责重新传输数据

ARP协议(属于网络层的子层,在IP地址到MAC地址的映射过程中起到重要的作用。)
虽然我们在这里介绍ARP协议, 但是需要强调, ARP不是一个单纯的数据链路层的协议, 而是一个介于数据链路层和网络层之间的协议

ARP协议建立了主机 IP地址和 MAC地址的映射关系

  • 当报文到达局域网的时候,需要通过MAC地址来找到目标主机,但是我们只有一个目标主机的IP地址,该如何找到目标主机的MAC地址呢?
  • 此时就需要引入ARP协议来完成这一工作,当报文进入局域网中的入口主机的时候,会通过广播的形式先查找到目标主机,找到后目标主机再向其发送他的MAC地址。

ARP协议格式

  • 注意到源MAC地址、目的MAC地址在以太网首部和ARP请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的
  • 硬件类型指链路层网络类型,1为以太网
  • ·协议类型指要转换的地址类型,0x0800为IP地址
  • 硬件地址长度对于以太网地址为6字节
  • 协议地址长度对于和IP地址为4字节
  • op字段为1表示ARP请求,op字段为2表示ARP应答

当MAC协议层通过对有效载荷的解析发现是ARP的请求或者应答的时候,会将报文直接交给ARP协议,ARP协议对报文进行处理之后再返回给MAC协议,此时就不会经过网络层,如果MAC协议通过解析发现是一个IP协议的话,就会将数据发送给网络层,并一层一层向上处理。

发送返回报文
假设主机A向主机B发消息,其中MACA和MACB分代表两者的MAC地址,IPA和IPB分别代表两者的IP地址。

由于在请求的时候,还并不知道MACB的值,因此将目的MAC地址填为全F。同时数据链路层有要求,发送的数据不能小于46个字节,不能大于1500个字节,因此需要后面的PAD来补位18位。
在发送请求报文的时候,是以广播的形式发送的,这就意味着所有的主机都需要对该报文进行解包,解包之后会先看op(因为代表的是请求还是响应),然后再看IP地址。这是因为局域网中任何一台主机既可以向别人发起ARP,也可以被别人发起ARP。

ARP缓存
由于每次都进行ARP的效率太低了(每次有传输报文的时候都要发送一次广播一次ARP),因此系统中会将ARP做一些缓存,一定时间后再清理缓存。

RARP协议
RARP协议就是将ARP协议反过来,根据MAC地址找到IP地址,也是通过先广播后单向传输的方式。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月7日
下一篇 2023年12月7日

相关推荐