技术

并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go细节 codereview mat使用 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 MVCC 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes自动扩容缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 《阿里巴巴云原生实践15讲》笔记 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 扩展Kubernetes 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占cpu怎么办? 容器狂打日志怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 自动化mock AIOps说的啥 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 marathon-client 源码分析 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 swagger PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 mesos 的一些tips mesos 集成 calico calico学习 AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 硬件对软件设计的影响 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 从一个marathon的问题开始的 docker 环境(主要运行java项目)常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 network channel network byte buffer 测试环境docker化实践 netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 mesos深入 Macvlan Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty(六)netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 那些有用的sql语句 异构数据库表在线同步 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM内存与执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp mesos简介 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 Kubernetes 其它特性 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 CoreOS 使用 Go学习 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

标签


http那些事

2017年06月09日

简介

超文本传输协议,这决定了协议传输的内容。

如果你想了解一个http协议,就用一门语言基于socket包写一个特定的响应,然后基于浏览器访问它。

是什么驱动了http 协议的变革

  1. 对于同一个域名,浏览器限制只能开6~8多个连接 ==> 连接复用
  2. 复杂,现在的页面,一个页面上有几十个资源文件是很常见的事儿 ==> 并行请求,请求压缩,优先处理CSS、后传图片
  3. 安全
  4. 服务器推送,服务器在客户端没有请求的情况下主动向客户端推送消息。

http1.0

基本特点是“一来一回”:客户端发起一个TCP连接,在连接上发一个http request 到服务器,服务器返回一个http response,然后连接关闭。

主要有两个问题

  1. 性能问题,连接的建立、关闭都是耗时操作。为此设计了Keep-Alive机制实现Tcp连接的复用
  2. 服务器推送问题

http1.1

一些改进:

  1. Keep-Alive 成为默认
  2. 支持Chunk 机制

问题:

  1. 在上一个请求的响应没有收到之前,无法发送下一个请求。学名pipeline,reids 因为服务端是单线程结构,所以支持pipeline
  2. 服务器推送问题

long polling

http long-polling(推送),服务端故意不响应(一段时间),也不断连接。参见面试时如何优雅的谈论HTTP/1.0/1.1/2.0

Content-Type

Content-Type实体首部字段基本要点:

  1. Content-Type说明了http body的MIME类型的 header字段。
  2. MIME类型由一个主媒体类型(比如text,image,audio等)后面跟一条斜线以及一个子类型组成,子类型用于进一步描述媒体类型。

对于post请求,默认情况下, http 会对表单数据进行编码提交。笔者实现分片文件上传时,上传分片二进制数据,若是不指定Content-Type: application/octet-stream 则http对二进制进行了一定的变化,导致服务端和客户端对不上。

Content-Encoding

http协议中有 Content-Encoding(内容编码)。Content-Encoding 通常用于对实体内容进行压缩编码,目的是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的。

内容编码针对的只是传输正文。在 HTTP/1 中,头部始终是以 ASCII 文本传输,没有经过任何压缩,这个问题在 HTTP/2 中得以解决。

Transfer-Encoding

参见HTTP 协议中的 Transfer-Encoding

Transfer-Encoding 用来改变报文格式。这涉及到一个通信协议的重要问题:如何定义协议数据的边界

  1. 发送完就断连接(非持久连接)
  2. 协议头部设定content-length
  3. 以特殊字符结尾

content-length有几个问题:

  • 发送数据时,对某些场景,计算数据长度本身是一个比较耗时的事情,同时会占用一定的memory。
  • 接收数据时,从协议头拿到数据长度,接收不够这个长度的数据,就不能解码后交给上层处理。

Transfer-Encoding 当下只有一个可选值:分块编码(chunked)。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('Transfer-Encoding: chunked\r\n');
        sock.write('\r\n');

        sock.write('b\r\n');
        sock.write('01234567890\r\n');

        sock.write('5\r\n');
        sock.write('12345\r\n');

        sock.write('0\r\n');
        sock.write('\r\n');
    });
}).listen(9090, '127.0.0.1');

server push

服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求。

http2

HTTP/2协议–特性扫盲篇

http/2中文版 根据rfc7540翻译

HTTP/2 把 HTTP 分解成了“语义”和“语法”两个部分

  1. “语义”层不做改动,与 HTTP/1 完全一致(即 RFC7231)。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。
  2. HTTP/2 在“语法”层做了“天翻地覆”的改造

备注:语义是对数据符号的解释,而语法则是对于这些符号之间的组织规则和结构关系的定义。

http/2中文版 根据rfc7540翻译

HTTP2引入了三个新概念:

  1. Frame:HTTP2通信的最小单位,二进制头封装,封装HTTP头部或body
  2. Message:逻辑/语义上的HTTP消息,请求或者响应,可以包含多个 frame
  3. Stream: 已经建立连接的双向字节流,用唯一ID标示,可以传输一个或多个frame

HTTP/2 in GO(一)Message 和 Stream 只在端上存在,链路中只存在 frame,这些概念的关系是这样的:

  1. 所有的通信都在一个 tcp 链接上完成,会建立一个或多个 stream 来传递数据
  2. 每个 stream 都有唯一的 id 标识和一些优先级信息,客户端发起的 stream 的 id 为单数,服务端发起的 stream id 为偶数
  3. 每个 message 就是一次 Request 或 Response 消息,包含一个或多个帧,比如只返回 header 帧,相当于 HTTP 里 HEAD method 请求的返回;或者同时返回 header 和 Data 帧,就是正常的 Response 响应。
  4. Frame 是最小的通信单位,承载着特定类型的数据,例如 Headers, Data, Ping, Setting 等等。 来自不同 stream 的 frame 可以交错发送,然后再根据每个 Frame 的 header 中的数据流标识符重新组装。

二进制格式

http2把原来的“Header+Body”的消息“打散”为数个小片的二进制“帧”(Frame),用“HEADERS”帧存放头数据、“DATA”帧存放实体数据。这种做法有点像是“Chunked”分块编码的方式,也是“化整为零”的思路,但 HTTP/2 数据分帧后“Header+Body”的报文结构就完全消失了,协议看到的只是一个个的“碎片”。

Http2Frame 类型

  type值    
data 0x0    
header 0x1    
PRIORITY 0x2    
RST_STREAM 0x3 流结束帧,用于终止异常流  
SETTINGS 0x4 连接配置参数帧 设置帧由两个终端在连接开始时发送,连接生存期的任意时间发送;设置帧的参数将替换参数中现有值;client和server都可以发送;设置帧总是应用于连接,而不是一个单独的流;
PUSH_PROMISE 0x5 推送承诺帧  
PRIORITY 0x6 检测连接是否可用  
GOAWAY 0x7 通知对端不要在连接上建新流  
WINDOW_UPDATE 0x8 实现流量控制  
CONTINUATION 0x9    

我们可以将frame笼统的分为data frame和 control frame,每一种类型的payload都是有自己的结构。可以参考下 go http2 实现 HTTP/2 in GO(三)

多路复用

消息的“碎片”到达目的地后应该怎么组装起来呢?HTTP/2 为此定义了一个“流”(Stream)的概念,它是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID。

因为流是虚拟的,实际上并不存在(除了Frame 结构里有一个StreamId),所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”( Multiplexing)——多个往返通信都复用一个连接来处理。

在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。在概念上,一个 HTTP/2 的流就等同于一个 HTTP/1 里的“请求 - 应答”。在 HTTP/1 里一个“请求 - 响应”报文来回是一次 HTTP 通信,在 HTTP/2 里一个流也承载了相同的功能。

浏览器渲染一个页面需要一个html文件,一个css文件,一个js文件,n个图片文件

备注:对于接收来说,缓冲区让 接收数据从字节到数据包有了完整性,port和streamid 则为数据包 赋予了“身份”。

HTTP/2 的流有哪些特点呢?

  1. 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”;
  2. 客户端和服务器都可以创建流,双方互不干扰;
  3. 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回;
  4. 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺的;
  5. 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验;
  6. 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数;
  7. 在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送;
  8. 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。

http2连接过程

不同于http1直接发送请求

流量控制

简单说,就是发送方启动是有个窗口大小(默认64K-1),发送了10K的DATA帧,就要在窗口里扣血(减掉10K),如果扣到0或者负数,就不能再发送;接收方收到后,回复WINDOW_UPDATE帧,里面包含一个窗口大小,数据发送方收到这个窗口大小,就回血,如果回血到正数,就又能发不超过窗口大小的DATA帧。

这种流控方式就带来一些问题:

  1. 如果接收方发的WINDOW_UPDATE frame丢了,当然tcp会保证重传,但在WINDOW_UPDATE重传之前,就限制了发送方发送数据
  2. 一旦发送方初始windows size确定,那么发送方的发送速度是由接收方 + 网络传输决定的,如果发送方的速度大于接收方的应答,那么就会有大量的数据pending。

流控只限定data类型的frame,其它限定参见http2-frame-WINDOW_UPDATE

https

来自《http权威指南》

对web服务器发起请求时,我们需要一种方式来告知web服务器去执行http的安全协议版本,这是通过url中设定http或https来实现的。

  1. 如果是http,客户端就会打开一条到服务器80端口的连接
  2. 如果是https,客户端就会打开一条到服务器443端口的连接,一旦建立连接,client和server就会初始化ssl layer,对加密参数进行沟通,并交换密钥。ssl握手(SSLHandshake)完成之后,ssl layer初始化完成了。剩下的就是,browser将数据从http layer发到tcp layer之前,要经过ssl layer加密。

Java 和 HTTP 的那些事(四) HTTPS 和 证书

其它

get 和 post 的区别

2018.05.11 补充

99%的人都理解错了HTTP中GET与POST的区别

  1. GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。这个可以说出来十几条。
  2. 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。当然,这并不是强约束,firefox对post 就还只是发了一次。

http1.1 http2 https之间的关系

参见谈谈 HTTP/2 的协议协商机制

accept:image/webp,image/apng,image/*,*/*;q=0.8
accept-encoding:gzip, deflate, br
accept-language:en-US,en;q=0.9
cache-control:no-cache
cookie:GeoIP=US:CA:Los_Angeles:34.05:-118.26:v4; CP=H2; WMF-Last-Access=23-Feb-2018; WMF-Last-Access-Global=23-Feb-2018

cookie 是header的一种,cookie被浏览器自动添加,特殊处理

  1. 浏览器自动存储cookie,存储时按域名组织,并在发送请求时自动带上cookie(这导致某些数据不适合放在cookie中,因为会浪费网络流量)
  2. cookie既可以由服务端来设置(通过set-cookie header),也可以由客户端来设置(js document.cookie = "name=Jonh; ";)。
  3. HTTP cookieAn HTTP cookie is a small piece of data sent from a website and stored on the user’s computer by the user’s web browser while the user is browsing. Cookies were designed to be a reliable mechanism for websites to remember stateful information。The term “cookie” was coined by web browser programmer Lou Montulli. cookie 由一个 browser programmer 提出,由browser存储,目的是为了存储用户的状态信息。

对笔者个人来说,有以下几点要矫正:

  1. header 分为

    • 通用header,比如Date
    • 请求特有header,比如Accept、Authorization、Cookie
    • 响应特有header,比如Server、Set-Cookie
    • body相关header,比如Content-Type
    • 自定义header

    因为springmvc 等framework,开发时不需要了解header,但framework确实进行了必要的设置

  2. 对于服务端开发,我们比较熟悉,将用户数据保存在数据库中,通过http请求改变用户记录的状态。其实,反向的过程也是支持的,常用的本地存储——cookie篇,随着浏览器的处理能力不断增强,越来越多的网站开始考虑将数据存储在「客户端」,提供了许多本地存储的手段。浏览器提供数据存储能力,服务器通过http响应来更改用户记录的状态。

并行性

对于处理多个“活儿”,每个“活儿”多个步骤:

  1. HTTP/1.1 with one connection,说一个活儿,干一个活儿, 干完一个再说下一个
  2. HTTP/1.1 with pipelining,一次说完,走排期,依次干活
  3. HTTP/2,一次说完,自己看着干
  4. HTTP/1.1 with multiple connections,把活儿分派给多个人

实现一个简单的http server

基于node.js socket写一个简单的http server

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('\r\n');
        sock.write('hello world!');
        sock.destroy();
    });
}).listen(9090, '127.0.0.1');

scala版本

object SocketServer {
    def main(args: Array[String]): Unit = {
        try {
            val listener = new ServerSocket(8080);
            val socket = listener.accept()
            val data = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nhello world!"
            socket.getOutputStream.write(data.getBytes())
            socket.close()
            listener.close()
        }
        catch {
            case e: IOException =>
                System.err.println("Could not listen on port: 80.");
                System.exit(-1)
        }
    }
}