HTTP
此文章只讨论HTTP和HTTP2,不讨论TLS和HTTP3
什么是HTTP
HTTP(超文本传输协议,HyperText Transfer Protocol)是
- OSI七层(应用层,表示层,会话层,传输层,网络层,数据链路层,物理层)
- TCP/IP四层(应用层,传输层,网络层,数据链路层)
中的应用层协议。
为什么需要HTTP?
HTTP(超文本传输协议)的主要作用是在客户端和服务器之间传输超文本(如HTML)及其他资源。
为什么不直接用tcp传输
- tcp是一个面向字节流的协议,会有分包,粘包等
- tcp传输数据没有规定格式,无法解析数据(就是这些规定形成了应用层协议)
- tcp没标准化 ,没高层语义
HTTP/1.1
HTTP/1.1就是要解决上面直接用tcp的问题
HTTP/1.1 request由四个部分组成
-
http url http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
-
http 请求行 GET / HTTP/1.1
-
http headers Content-Length: 11
-
http body { "json":"example" }
http/1.1的三种发送方式
1. http短连接
2. http长连接
3. tcp管道
其中tcp管道已被证明难以实现因为返回时需要按发送的顺序返回。
http/1.1的缺点
- 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
- 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
- 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,队头阻塞;
- 没有请求优先级控制;
- 请求只能从客户端开始,服务器只能被动响应。
HTTP/2
HTTP/2就是来解决HTTP/1.1的缺点的
- 使用HPACK头部压缩
- 使用Stream实现多路复用
- 有优先级控制,stream dependency等设置
- 支持服务器推送
HTTP/2的优先级,依赖,流量控制后面再实现,这里先实现http2的编解码
HTTP/2 Connection
HTTP/2 建议在客户端与服务端之间只建立一条tcp连接,因为HTTP2可以多路复用
Java
/**
* http2 connection
*
* @author qqiu
*/
@RequiredArgsConstructor
public class Connection {
/**
* tcp socket
*/
final Socket socket;
/**
* key: stream id
* value: stream
*/
Map<Integer, Stream> streams=new ConcurrentHashMap<>();
/**
* key: index
* value: header `name=value`
*/
DynamicTable dynamicTable=new DynamicTable();
}
动态表在HPACK中讲解
HTTP/2 Stream
- 流 是 HTTP/2 连接中的一个逻辑通道,用于传输一个 HTTP 请求和响应。
- 每个流有一个唯一的 流标识符(Stream ID) ,用于区分不同的流。
- 流是双向的,客户端和服务器可以在同一个流上发送和接收帧。
- 一个流只能用于传输一个 HTTP 请求和响应,不能复用。
流的特点
- 唯一性:每个流有一个唯一的流标识符(Stream ID),由发起流的端点分配。
- 独立性:多个流可以在同一个连接上并行传输,互不干扰。
- 双向性:客户端和服务器可以在同一个流上发送和接收帧。
- 有序性:流中的帧必须按顺序发送和接收,以确保消息的完整性。
stream state
perl
Stream States
+--------+
send PP | | recv PP
,--------| idle |--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,------| reserved | | recv H | reserved |------.
| | (local) | | | (remote) | |
| +----------+ v +----------+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------| open |-------. | recv H |
| | / | | \ | |
| v v +--------+ v v |
| +----------+ | +----------+ |
| | half | | | half | |
| | closed | | send R / | closed | |
| | (remote) | | recv R | (local) | |
| +----------+ | +----------+ |
| | | | |
| | send ES / | recv ES / | |
| | send R / v send R / | |
| | recv R +--------+ recv R | |
| send R / `----------->| |<-----------' send R / |
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+
idle(空闲)
- 初始状态 :流在创建时处于
idle
状态。 - 转换条件 :
- 发送或接收
HEADERS
帧(带END_STREAM
标志)或PUSH_PROMISE
帧时,流会从idle
状态转换到其他状态。 - 发送
PUSH_PROMISE
帧时,流会进入reserved (local)
状态。 - 接收
PUSH_PROMISE
帧时,流会进入reserved (remote)
状态。
- 发送或接收
reserved (local)(本地保留)
- 状态描述 :当本地端点(客户端或服务器)发送了
PUSH_PROMISE
帧,表示它承诺将来会推送资源,流进入reserved (local)
状态。 - 转换条件 :
- 发送
HEADERS
帧后,流会进入half closed (remote)
状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 发送
reserved (remote)(远程保留)
- 状态描述 :当远程端点发送了
PUSH_PROMISE
帧,表示它承诺将来会推送资源,流进入reserved (remote)
状态。 - 转换条件 :
- 接收
HEADERS
帧后,流会进入half closed (local)
状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 接收
open(打开)
- 状态描述:流处于打开状态,双方都可以发送和接收数据帧。
- 转换条件 :
- 如果发送或接收
HEADERS
帧(带END_STREAM
标志),流会进入half closed
状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 如果发送或接收
half closed (local)(本地半关闭)
- 状态描述 :本地端点已经发送了
END_STREAM
标志,表示本地不会再发送数据,但仍然可以接收来自远程端点的数据。 - 转换条件 :
- 如果接收到
END_STREAM
标志,流会进入closed
状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 如果接收到
half closed (remote)(远程半关闭)
- 状态描述 :远程端点已经发送了
END_STREAM
标志,表示远程不会再发送数据,但本地仍然可以发送数据。 - 转换条件 :
- 如果发送
END_STREAM
标志,流会进入closed
状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 如果发送
closed(关闭)
- 状态描述:流已经完全关闭,不能再发送或接收任何数据。
- 转换条件 :
- 一旦流进入
closed
状态,就不能再转换到其他状态。 - 如果发送或接收
RST_STREAM
帧,流会进入closed
状态。
- 一旦流进入
HTTP/2 message
在 HTTP/2 中,消息(Message) 是一个逻辑概念,表示一个完整的 HTTP 请求或响应。一个 HTTP/2 消息由一个或多个 帧(Frames) 组成,这些帧通过 流(Stream) 传输。
一个 HTTP/2 消息由以下部分组成:
- HEADERS 帧:包含消息的头部字段(如请求方法、状态码、头部字段等)。
- DATA 帧(可选):包含消息的负载数据(如请求体或响应体)。
- 其他帧 (可选):如
PRIORITY
帧、CONTINUATION
帧等。
HTTP/2 frame
frame Name | frame Feature |
---|---|
DATA | HTTP DATA |
HEADERS | HTTP HEADER |
PRIORITY | 设置优先级 |
RST_STREAM | 错误处理 |
SETTINGS | 参数设置 |
PUSH_PROMISE | 服务端推送 |
PING | CHECK ALIVE |
GOAWAY | 停止接受新流 |
WINDOW_UPDATE | 流量控制 |
CONTINUATION | 延续HEADER |
frame
所有frame共有的前缀
Java
/**
* http2 frame
*
* @author qqiu
*/
public abstract class Frame {
/**
* 24 bit frame length
*/
int length;
/**
* 8 bit type
*/
@Getter
short type;
/**
* 8 bit flags
*/
@Getter
short flags;
/**
* 1 bit r
*/
boolean r;
/**
* 31 bit streamId
*/
@Getter
int streamId;
public abstract Frame decode(byte[] bytes);
public abstract byte[] encode();
public byte[] encodeFrameHead() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(ByteUtils.int2bytes(length,3));
baos.write(ByteUtils.int2bytes(type,1));
baos.write(ByteUtils.int2bytes(flags,1));
byte[] bytes = ByteUtils.int2bytes(streamId, 4);
bytes[0] = ByteUtils.setBit(bytes[0], 7, r);
baos.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
return baos.toByteArray();
}
public void decodeFrameHead(byte[] bytes) {
Assert.isTrue(bytes.length >= 9);
length = ByteUtils.bytes2int(bytes,0,3);
type = (short) ByteUtils.bytes2int(bytes,3,1);
flags = (short) ByteUtils.bytes2int(bytes,4,1);
r = ByteUtils.byteIndex(bytes[5], 7);
streamId = ByteUtils.bytes2int(bytes,6,4);
}
}
后面的encode decode太占位置我就不放了
DATA Frame
Java
/**
* http2 data frame
*
* @author qiu
*/
public class DataFrame extends Frame {
/**
* 8bit exist when flag is PADDED(0x8)
*/
short padLength;
String data;
byte[] padding;
}
HEADERS Frame
Java
/**
* http2 header frame
*
* @author qiu
*/
public class HeadersFrame extends Frame {
/**
* 8bit exist when flag is PADDED(0x8)
*/
short padLength;
/**
* 1bit exist when flag is PRIORITY(0x20)
* when e is true, streamId is exclusive
* when e is false, streamId is inclusive
*/
boolean e;
/**
* 31bit streamId exist when flag is PRIORITY(0x20)
*/
int streamId;
/**
* 8bit exist when flag is PRIORITY(0x20)
*/
short weight;
@Getter
byte[] data;
byte[] padding;
}
PRIORITY Frame
Java
/**
* http2 priority frame
*
* @author qqiu
*/
public class PriorityFrame extends Frame {
/**
* 1bit
* when e is true, streamId is exclusive
* when e is false, streamId is inclusive
*/
boolean e;
/**
* 31bit streamId when flag is PRIORITY(0x20)
*/
int streamId;
/**
* 0-256
*/
short weight;
}
RST_STREAM
Java
/**
* http2 reset stream frame
*
* @author qqiu
*/
public class ResetStreamFrame extends Frame{
/**
* 32bit 错误码
*/
int errorCode;
}
SETTINGS Frame
Java
/**
* http2 settings frame
*
* @author qqiu
*/
public class SettingsFrame extends Frame {
public static class Setting {
/**
* 16 bit id
*/
public int id;
/**
* 32 bit unsigned value
*/
public long value;
}
List<Setting> settings;
}
PUSH_PROMISE Frame
Java
/**
* http2 push promise frame
*
* @author qqiu
*/
public class PushPromiseFrame extends Frame {
/**
* 8bit exist when flag is PADDED(0x8)
*/
short padLength;
/**
* 1bit 保留位
*/
boolean r;
/**
* 31bit promisedStreamId
*/
int promisedStreamId;
byte[] data;
byte[] padding;
}
PING Frame
Java
/**
* http2 ping frame
*
* @author qqiu
*/
public class PingFrame extends Frame {
/**
* 64bit
*/
String data;
}
GOAWAY Frame
Java
/**
* http2 GOAWAY frame
*
* @author qqiu
*/
public class GoAwayFrame extends Frame {
/**
* 1bit 保留位
*/
boolean r;
/**
* 31bit 流id
*/
int lastStreamId;
/**
* 32bit 错误码
*/
int errorCode;
byte[] data;
}
WINDOW_UPDATE Frame
Java
/**
* http2 window update frame
*
* @author qqiu
*/
public class WindowUpdateFrame extends Frame {
/**
* 1bit 保留位
*/
boolean r;
/**
* 31bit 窗口大小增量
*/
int windowSizeIncrement;
}
CONTINUATION Frame
Java
/**
* http2 CONTINUATION frame
*
* @author qqiu
*/
public class ContinuationFrame extends Frame{
byte[] data;
}
Pseudo-Header Fields(伪头部字段)
在HTTP/2中没有请求行,使用伪头部字段代替,
伪头部字段特点
- 名称以冒号开头 :伪头部字段的名称以冒号(
:
)开头,例如:method
、:path
。 - 必须出现在普通头部字段之前 :在 HTTP/2 的
HEADERS
帧中,伪头部字段必须出现在所有普通头部字段之前。 - 不可重复:每个伪头部字段在同一个请求或响应中只能出现一次。
- 大小写敏感:伪头部字段的名称必须是小写的。
伪头部字段的使用规则
- 请求中必须包含的伪头部字段 :
- 每个请求必须包含
:method
、:scheme
、:path
伪头部字段。 - 如果请求的目标是权威服务器(如 HTTP/1.1 的
Host
头部字段),则必须包含:authority
伪头部字段。
- 每个请求必须包含
- 响应中必须包含的伪头部字段 :
- 每个响应必须包含
:status
伪头部字段。
- 每个响应必须包含
- 伪头部字段的顺序 :
- 伪头部字段必须出现在普通头部字段之前。
- 伪头部字段的顺序没有严格要求,但通常按照
:method
、:scheme
、:authority
、:path
的顺序排列。
HPACK
HPACK是Header帧中data的压缩算法,只用于Header帧的data中
HPACK使用两个表将标题字段与索引相关联。
- 静态表是预定义的,包含公共标题字段(其中大多数字段为空值)。
- 动态表动态的,编码器可以使用它来索引编码标题列表中重复的标题字段。
静态表
静态表由预定义的标题字段静态列表组成。
动态表
动态表由按先进先出顺序维护的标题字段列表组成。动态表中的第一个也是最新的条目位于最低索引,而动态表中最早的条目位于最高索引。
动态表最初为空。在解压每个头块时添加条目。
动态表可以包含重复的条目(即具有相同名称和相同值的条目)。因此,解码器不得将重复条目视为错误。
编码器决定如何更新动态表,因此可以控制动态表使用的内存量。为了限制解码器的内存需求,动态表大小受到严格限制。
动态表可以看作一个队列,当容量超过时,先进先出
lua
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
^ |
| V
Insertion Point Dropping Point
Java
/**
* @author qiu
*/
public class DynamicTable {
private int maxSize = 4096;
private final AtomicInteger index = new AtomicInteger(Const.STATIC_TABLE_SIZE);
@Getter
private final LinkedHashMap<Integer, String> table = new LinkedHashMap<>();
public String get(int index) {
Assert.isTrue(index >= Const.STATIC_TABLE_SIZE);
return table.get(index);
}
public void put(String value) {
table.put(index.getAndIncrement(), value);
while (table.size() > maxSize) {
table.pollLastEntry();
}
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
while (table.size() > maxSize) {
table.pollLastEntry();
}
}
public int size() {
return table.size();
}
}
HPACK编码
HPACK编码使用两种基本类型:无符号变长整数和八位字节字符串。
无符号边长整数
如果整数值足够小,即严格小于2^N-1,则在N位前缀内对其进行编码。
否则,前缀的所有位都设置为1,值减少2^N-1,使用一个或多个八位字节的列表进行编码。
每个八位字节的最高有效位用作连续标志:除列表中的最后一个八位字节外,其值设置为1。八位字节的剩余位用于对减少的值进行编码。
diff
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | ? | Value |
+---+---+---+-------------------+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | ? | 1 1 1 1 1 |
+---+---+---+-------------------+
| 1 | Value-(2^N-1) LSB |
+---+---------------------------+
...
+---+---------------------------+
| 0 | Value-(2^N-1) MSB |
+---+---------------------------+
Java
/**
* use HPACK encode int
* N=n
* prefix 0
*/
private static byte[] encodeInt(int value, int n) {
int nBitMax = 0xff >> (8 - n);
if (value < nBitMax) {
return new byte[]{(byte) value};
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
value -= nBitMax;
while (value >= 128) {
byteArrayOutputStream.write(value % 128 + 128);
value /= 128;
}
byteArrayOutputStream.write(value);
return byteArrayOutputStream.toByteArray();
}
/**
* use HPACK decode int
* N=n
*/
private static int parseInt(ByteReader br, int n) {
int suffix = ByteUtils.byteSuffix(br, n);
int nBitMax = 0xff >> (8 - n);
if (suffix != nBitMax) {
br.start += 1;
return suffix;
}
int m = 0;
do {
br.start += 1;
suffix = suffix + (br.bytes[br.start] & 127) * (1 << m);
m += 7;
} while ((br.bytes[br.start] & 128) == 128);
return suffix;
}
字符串
标题字段名称和标题字段值可以表示为字符串文字。字符串文字被编码为八位字节序列,可以直接编码字符串文字的八位字节,也可以使用哈夫曼代码
lua
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
字符串文字表示法包含以下字段:
- H:一位标志,H,指示字符串的八位字节是否为哈夫曼编码。
- 字符串长度:用于编码字符串文字的八位字节数,编码为带有7位前缀的整数
- 字符串数据:字符串文字的编码数据。如果H为"0",则编码数据为字符串文字的原始八位字节。如果H是"1",则编码数据是字符串文本的哈夫曼编码。
ByteUtils
Java
/**
* @author qiu
*/
public abstract class ByteUtils {
/**
* 字节的位索引:从右到左编号为 01234567。
* 例如:
* 00000000
* index
* 76543210
*/
public static boolean byteIndex(byte b, int index) {
Assert.isTrue(index >= 0 && index <= 7);
return (b & (1 << index)) != 0;
}
public static boolean byteIndex(ByteReader b, int index) {
return byteIndex(b.bytes[b.start], index);
}
public static byte setBit(byte b, int index, boolean value) {
Assert.isTrue(index >= 0 && index <= 7);
if (value) {
return (byte) (b | (1 << index));
}
return (byte) (b & ~(1 << index));
}
public static byte[] int2bytes(int i, int length) {
Assert.isTrue(length >= 0 && length <= 4);
byte[] bytes = new byte[length];
for (int j = 0; j < length; j++) {
bytes[j] = (byte) ((i >> (length - j - 1) * 8) & 0xff);
}
return bytes;
}
public static int bytes2int(byte[] bytes, int start, int length) {
Assert.isTrue(length >= 0 && length <= 4);
int i = 0;
for (int j = 0; j < length; j++) {
i += (bytes[start + j] & 0xff) << (length - j - 1) * 8;
}
return i;
}
public static byte setBytePrefix(byte b, int length, byte prefix) {
Assert.isTrue(length >= 0 && length <= 8);
int mask = (1 << length) - 1;
return (byte) ((b & mask) | (prefix << (8 - length)));
}
public static byte byteSuffix(ByteReader b, int length) {
return byteSuffix(b.bytes[b.start], length);
}
public static byte byteSuffix(byte b, int length) {
Assert.isTrue(length >= 0 && length <= 7);
return (byte) (b & (0xff >> (8 - length)));
}
private static final String[] ZEROS = {"", "0", "00", "000", "0000", "00000", "000000", "0000000", "00000000"};
public static String toBinaryString(byte b, int length) {
String binaryString = Integer.toBinaryString(b & 0xFF);
if (binaryString.length() < length) {
int diff = length - binaryString.length();
binaryString = ZEROS[diff] + binaryString;
}
return binaryString;
}
public static byte[] concat(byte[]... bytes) {
int length = 0;
for (byte[] b : bytes) {
length += b.length;
}
byte[] result = new byte[length];
int index = 0;
for (byte[] b : bytes) {
System.arraycopy(b, 0, result, index, b.length);
index += b.length;
}
return result;
}
}