TCP-HTTP相关问题学习

142 阅读12分钟

OSI七层模型

不同的层负责的工作,以及对于数据单元不一样的称呼:

  • 链路层:帧 1500字节 并有MTU最大传输单元的概念
  • 传输层:段 该层用来保证传输的可靠性,还会有分段传输、超时重传、处理丢包的逻辑
    • 个人感觉:传输层中的端口号,应该是和进程会形成映射关系,端口号是外部网络访问本地机器时的标识,进程号是机器内部分配的一个供自己的操作系统工作的标识
  • 网络层:包或数据包 用来寻址 所谓寻址,就是寻找mac地址
  • 应用层:报文

网络设备:

  • 中继器:网线最长可以拉100米,再长的话由于信号衰减就不可以了,可以将信号放大的机器叫中继器,但中继器只有两个口
  • 集线器:有多个口的中继器叫集线器,集线器是通过广播的形式传递信息,没有敏感信息过滤
  • 交换机:会维护物理端口和mac地址的映射,从而记住哪个端口是哪个设备在用
  • 路由器:交换机只提供内网间通信的功能,如果要连外网,就要用到路由器,路由器通常有两张网卡,其中一个和局域网通信,另一个负责连外网,路由器的网线插口通常分LAN口和WAN口,如果没有WAN口则不能连外网,此时路由器的功能就是交换机,路由器通常充当网关的角色,网关的作用可以类比海关或跳板机

tcp的特点及问题:

缺点:

  • tcp顺序问题 后面的包要等前面的包返回之后才可以继续传输,滑动窗口才能右移,同时接收端的缓存区才能清空,简称“队头阻塞问题”
  • 如果接收方的缓存区满了,会主动告诉发送方调整窗口大小为0.如果接收方的数据被上层应用读取,读取后会主动通知发送方,如果接收方发送的数据丢包了,不用担心,发送方会定时发送探测包,来监控接收方的窗口大小
  • 慢启动问题,由于要三次握手,因此比较耗费性能
  • Time-wait 客户端试图断开连接时,服务器不会立即断开,在高并发 短连接的情况下 会出现端口全被占用,没法释放
  • http1.0最早的时候就是短链接,用完tcp后就断开
  • 拥塞问题:如果发送的数据,可以立刻被接收方确认,也不可以无限的发送数据吗? 因为还要受带宽限制,因此发送方需要根据带宽计算拥塞窗口大小 如何计算拥塞窗口大小?通过慢启动的方式,这种方式很浪费时间,http中默认可以同时在一个域名下 开启6个tcp的链接 (经历6次的慢启动,如果中间丢包了,也会出现从0开始) (用快重传来达到减少慢启动的过程)

优点:

  • 有序,可靠,快重传,粘包 粘包算法:nagle cork
  • 粘包的目的就是为了解决减少tcp段数据较小,每次传的时候都要加上20字节的头的问题

http

综述:

  • http学习的过程中,很多时候就是学习各种头部,这些请求头和响应头的核心在于内容协商
  • http实现长连接,会默认在请求的时候 增加connection: keep-alive

最初的http(0.9版本)只用于传输文本内容,只有get请求,基于tcp,响应后就会断开连接

后来http1.0增加了header,并支持了更多格式内容的传输,例如图片 音乐等,增加了响应状态码,以及post head等请求的method,但该版本问题较多,后来很快被废弃

再后来的http1.1,允许持久连接(keep-alive)和响应数据分块,增加了缓存管理和控制,增加了 PUT、DELETE 等新的方法。逐渐成为正式的和使用较多的版本,直到目前依然有很多网站在使用

HTTP/1.1的缺点:

  • 单个 TCP 连接在一个时刻只能处理一个请求。
  • header没法压缩
  • 不安全(明文,可能内容被篡改)
  • 我们可以在一个域名下建立6个tcp链接(为了实现并发 , 会导致有6次慢启动, 带宽竞争问题)因为http没有序号
  • http1.1 没法乱序收发的(1个tcp链接上)

http2的优点:

  • 我们一个域名只建立一个tcp链接, 在一个tcp链接中实现数据的收发。 给数据标号(类似tcp的seq)
  • http2 采用了多路复用, 可以将数据编程帧进行传输 (以前http1.1 有队头阻塞问题, http2 请求不用等待 可以并发发送) 流、帧

    流是虚拟的, 真正传输的叫帧

  • http2 中实现了这样一个分帧层,可以实现流量控制、优先级控制。 支持了服务端推送 (1.1应答模式)
  • 头部压缩 静态表 替换相同的内容 为数字 动态表,比较两次header的差异 只发送差异的部分 哈夫曼编码(哈夫曼树 左树为0 右树为1) 核心 就是用出现少的数编码采用长的,出现多的数 编码用短的

http2的缺点:

  • 我们解决的永远是http层面的,但是服务端在处理的时候 还是 tcp(队头阻塞问题) 无法解决的 -> http3 采用了udp协议

缓存:

1、最早的方式-强制缓存

  • 服务端通过响应头告诉客户端如何控制缓存时,就是强制缓存,强制缓存主要是服务器通过Expires/Cache-control两个Header来告诉客户端实现的:

  • Expires头是服务端给客户端的一个缓存过期时间,该值是绝对值,当服务端和客户端时间不一致时会出问题

  • Cache-control头是服务端给客户端的一个相对时间,功能与Expires类似,例如: cache-control: max-age=2592000

  • 强制缓存的状态码是200 disk cache

  • Cache-control的两个常见值:

    No-cache 不缓存,但浏览器客户端会存缓存,每次发起请求时还要询问服务器

    No-store 真正的不缓存,浏览器客户端就没有存缓存

    设置了这两个值,就不会走协商缓存了

2、更进一步-协商缓存

  • 强制缓存的规则不被命中时,再次向服务端发起请求,服务端上存储的文件如果和上次请求时相比没变化,就返回304,告诉客户端可以用之前缓存的资源,否则返回新的资源,完成该功能的头是last-modified和if-modified-since

  • 还有一种类似的协商缓存是通过签名实现的,即将服务端资源通过md5摘要算法生成一个16进制值(摘要主要包含文件大小和最后修改时间),然后将该值通过Etag返回给客户端,客户端下次请求时会通过if-none-match发送给服务端,从而判断缓存是否过期

  • 但每次计算签名会非常耗费性能,因此有时候会使用弱指纹,例如可以用last-modified + 文件长度 成为一个指纹

强制缓存和协商缓存可以一起使用,也可以只用协商缓存,或只用强制缓存

对于网站的主logo,很久都不会变,采用强制缓存较为合适 对于js css这些资源,由于经常需要发版更新,采用协商缓存更合适

状态码 (状态码,可以服务端定义)

  • 1xx 101ws 基于http的,要将协议升级成ws 101
  • 2xx 200成功 204没有响应体 206部分请求
  • 3xx 301(永久重定向,尽可能减少永久重定义) 302 (临时重定向) 304 (缓存)
    • 例如掘金的地址从juejin.im改为juejin.cn就是永久重定向
  • 4xx 400 参数错误 不识别的请求 401 (没有登录 没有权限) 403 (登录了没权限) 404 找不到 405 方法不支持
  • 5xx 500 服务端错误 502 网关错误,服务器当了

Content-Length和Transfer-Encoding

浏览器客户端向服务器发送请求时,如果知道了要传输数据的大小且为一次传输完成,则会添加Content-Length头,并将长度写进去,而如果无法预先得知数据大小,则可以使用Transfer-Encoding头,这两个头是互斥的

关于这两个头的实践,可以参考这篇文章: juejin.cn/post/694760…

tcp连接建立及数据传输

三次握手的原因:

  • 第1次:client对server说,我可以给你发送消息吗?这是一个SYN包

  • 第2次:server收到第1次请求发来的包后,首先需要给客户端一个确认信息,标识这个确认信息的是ACK这个位

    与此同时,server还需要对client说,我可以给你发消息吗?这也是一个SYN包

    可以看到,SYN和ACK是合并发送的

  • 第3次:客户端给服务端ACK包,以确认可以连接

综上来看,三次握手,是为了保证双方都可以建立连接

数据传输

之后就可以发送PSH包传送数据了

发送数据的过程,可以总结为以下几点:

  • 无论客户端还是服务端,都维护了自己的序列号,初始时,双方序列号的相对值(需要注意是相对值而非绝对值)都是0

  • 无论哪一端发送的ack确认号,都是要确认自己已经收到了对方发来的seq及之前的消息

  • 收到消息时,会立刻回复ack包,以告知对方自己已经收到消息,如果需要再给对方发消息,则会重新发一个包过去

举例来说:

client.js:

const net = require("net"); // net 就是tcp模块
const socket = new net.Socket(); // 套接字  双工流
// 连接8080端口
socket.connect(8080, "localhost");
// 连接成功后给服务端发送消息
socket.on("connect", function (data) {
  //   socket.write("hello"); // 浏览器和客户端说 hello
  socket.end();
});
socket.on("data", function (data) {
  // 监听服务端的消息
  console.log(data.toString());
});
socket.on("error", function (error) {
  console.log(error);
});

server.js

const net = require("net");
const server = net.createServer(function (socket) {
  socket.on("data", function (data) {
    // 客户端和服务端
    // socket.write("hi"); // 服务端和客户端说 hi
  });
  socket.on("end", function () {
    console.log("客户端关闭");
  });
});
server.on("error", function (err) {
  console.log(err);
});
server.listen(8080);

三次握手后,客户端和服务端的seq序号都是1

接下来

客户端和服务端说 hello -> 服务端要立刻响应

  • 客户端发送 序列号 seq = 1, 确认号 ack = 1, 发送内容(hello)长度为 5 个字节大小,这个包的[PUSH ACK]两个标识位为1
  • 服务端响应 序列号 seq = 1, 确认号 ack = 客户端的 seq + 内容长度len = 6,这个包的[ACK]标识位为1

此处需要注意ACK标识和ack确认号之间的差别

服务端和客户端说 hi -> 客户端要响应

  • 服务端发送 序列号 seq = 1, 确认号 ack = 6, 发送内容(hi)长度为 2 个字节大小,这个包的[PUSH ACK]两个标识位为1
  • 客户端响应 序列号 seq = 6, 确认号 ack = 服务端的 seq + 内容长度len = 3,这个包的[ACK]标识位为1

四次挥手的原因:

  • 假设断开连接的一方是客户端
  • 第1次:客户端先给服务端发一个FIN包 下列标识[FIN ACK]都是1
  • 第2次:服务端收到FIN之后需要立刻发一个ACK以确认收到该包 标识[ACK]是1
  • 第3次:由于服务端还需要做一系列收尾工作,因此在做完这些工作后才能给客户端也发一个FIN包 下列标识[FIN ACK]都是1
  • 第4次:客户端收到FIN包,连接断开 标识[ACK]是1

可以看出,断开连接的过程,其实是PSH位置1被替换为FIN位置1,其余过程和正常传输数据是类似的

关于四次挥手中的第2次和第3次,ACK和FIN包分开发送,可以类比如下场景:

微信AB发消息,想询问B觉得C怎么样?

B收到A的询问后,应该先回复收到,然后再慢慢构思具体细节再发一条消息出去给A,否则A会认为B好像没有收到消息,可能会导致A尝试重新发送消息

注意,在断开连接的阶段,上述的过程中,第3次服务端给客户端发消息结束后,有以下可能:

  • 客户端可能没有收到,然后服务器就会超时重发
  • 客户端收到了,但第4次过程,客户端发出去的包中途丢失,但客户端是不确定自己发出去的包是不是丢失了,所以需要等一段时间,过段时间后,等服务端没有给自己发消息,才断开连接,以确保真正断开

https

http要解决的核心问题:

  • https = http + ssl/tls 目前都叫tls 不叫ssl了 (核心就是将数据进行加密)
  • 防止传递的数据被篡改 签名

对于自签名ssl证书,Chrome是不信任的,为了安全起见,直接禁止访问了,这时可以键盘输入thisisunsafe 这个命令,说明你已经了解并确认这是个不安全的网站,你仍要访问就给你访问了。

代理

  • 正向代理:vpn 跳板机 用户权限
  • 反向代理:webpack代理 nginx代理

位于客户端的代理通常是正向代理,位于服务端的代理通常是反向代理