Skip to content
On this page

http1.1 vs http2

HTTP/2与HTTP/1.1的主要区别如下:

  1. 二进制格式:HTTP/2采用二进制格式而非HTTP/1.1的文本格式,这意味着HTTP/2中的请求和响应将被分割成更小的帧,以便更好地管理和优化数据传输。

  2. 多路复用:HTTP/2支持多路复用,这意味着可以在一个连接上同时传输多个请求和响应,而HTTP/1.1则需要在一个连接上逐个传输请求和响应。

  3. 服务器推送:HTTP/2支持服务器推送,这意味着服务器可以在客户端请求之前发送额外的资源,从而提高性能和响应时间。

  4. 首部压缩:HTTP/2采用首部压缩技术,将HTTP/1.1中的头信息压缩,减少了网络传输的数据量,提高了传输效率。

  5. 流量控制:HTTP/2支持流量控制,这意味着可以控制每个流的数据传输速率,从而避免了某些流占用过多的带宽,导致其他流的传输速度变慢。

综上所述,HTTP/2相对于HTTP/1.1具有更高的性能和效率,可以更好地适应现代Web应用程序的需要。

多路复用

HTTP/2 多路复用是指在一条连接上同时传输多个 HTTP 请求和响应。这种方式相比 HTTP/1.x 的串行传输,可以大大提高网络传输的效率。

HTTP/2 多路复用是通过将多个请求和响应封装在一个称为帧(frame)的数据块中来实现的。每个帧都有一个唯一的标识符,称为流标识符(stream identifier)。这个标识符用于将帧与特定的请求或响应相关联。

java

下面是一个使用 Java 代码实现 HTTP/2 多路复用的示例:

java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.CountDownLatch;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import org.eclipse.jetty.alpn.ALPN;
import org.eclipse.jetty.alpn.ALPN.ClientProvider;
import org.eclipse.jetty.alpn.ALPN.ServerProvider;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.HTTP2Client;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.HttpFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.http2.parser.Parser.Listener;
import org.eclipse.jetty.http2.parser.ParserFactory;
import org.eclipse.jetty.http2.parser.Parser.Listener.Adapter;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;

public class Http2MultiplexingExample {

    public static void main(String[] args) throws Exception {
        // HTTP/2 客户端连接工厂
        HTTP2ClientConnectionFactory connectionFactory = new HTTP2ClientConnectionFactory();

        // SSL 上下文工厂
        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
        SSLContext sslContext = sslContextFactory.getSslContext();
        SSLParameters sslParameters = sslContext.getDefaultSSLParameters();
        sslParameters.setApplicationProtocols(new String[]{"h2"});
        sslContextFactory.setSslContext(sslContext);
        sslContextFactory.setSslParameters(sslParameters);

        // 创建 HTTP/2 客户端
        HTTP2Client client = new HTTP2Client();
        client.setByteBufferPool(new MappedByteBufferPool());
        client.addConnectionFactory(connectionFactory);
        client.setSslContextFactory(sslContextFactory);

        // 注册 ALPN 提供程序
        ALPN.put(null, new ServerProvider() {
            @Override
            public void unsupported() {}

            @Override
            public void selected(String protocol) {
                System.out.println("Server selected protocol: " + protocol);
            }

            @Override
            public void offer(String[] protocols) {}
        });

        // 创建 SSL 引擎
        SSLEngine engine = sslContext.createSSLEngine();
        engine.setUseClientMode(true);

        // 客户端连接
        client.connect(engine, new InetSocketAddress("localhost", 8080), new Listener.Adapter() {
            @Override
            public void onConnect(Session session) {
                System.out.println("Connected to server");

                // 发送 SETTINGS 帧
                SettingsFrame settingsFrame = new SettingsFrame();
                settingsFrame.put(SettingsFrame.HEADER_TABLE_SIZE, 4096);
                session.settings(settingsFrame, new Callback.Empty());

                // 发送请求
                HttpFields requestFields = new HttpFields();
                requestFields.put("User-Agent", "Jetty HTTP/2 Client");
                HeadersFrame headersFrame = new HeadersFrame(1, requestFields, null, false);
                headersFrame.setStreamId(1);
                headersFrame.setEndStream(false);
                session.newStream(headersFrame, new Stream.Listener.Adapter() {
                    @Override
                    public void onHeaders(Stream stream, HeadersFrame frame) {
                        System.out.println("Received headers: " + frame.getMetaData().toString());
                    }

                    @Override
                    public void onData(Stream stream, HttpFrame frame, Callback callback) {
                        System.out.println("Received data: " + frame.toString());
                        callback.succeeded();
                    }
                });
            }
        }).get();
    }
}

在这个示例中,我们创建了一个 HTTP/2 客户端连接工厂,以及一个 SSL 上下文工厂。然后创建了一个 HTTP/2 客户端,并将连接工厂和 SSL 上下文工厂添加到客户端中。

接着,我们注册了一个 ALPN 提供程序,用于在 SSL 握手期间协商使用的协议。我们创建了一个 SSL 引擎,并使用客户端连接工厂连接到服务器。

连接成功后,我们发送了一个 SETTINGS 帧,用于设置连接参数。然后发送了一个 HTTP GET 请求,这个请求包含一个 HEADERS 帧和一个 DATA 帧。在 HEADERS 帧中,我们设置了请求头,以及一个流标识符(stream identifier),用于将这个帧与这个请求关联起来。在 DATA 帧中,我们设置了请求体。

当服务器接收到这个请求后,会返回一个响应。这个响应也是通过一个 HEADERS 帧和一个 DATA 帧来传输的。在客户端中,我们可以通过实现 Stream.Listener 接口并重写 onHeaders() 和 onData() 方法来处理服务器返回的响应。

总结一下,HTTP/2 多路复用是通过将多个请求和响应封装在一个称为帧(frame)的数据块中来实现的。每个帧都有一个唯一的标识符,称为流标识符(stream identifier)。在 Java 中,我们可以使用 Jetty HTTP/2 客户端库来实现 HTTP/2 多路复用。

kotlin

在 Kotlin 中,可以使用 OkHttp 库来实现 HTTP/2 多路复用。以下是一个示例代码:

kotlin
// 创建 OkHttpClient 实例
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
    .build()

// 创建 Request 对象
val request1 = Request.Builder()
    .url("https://example.com/api/1")
    .build()

val request2 = Request.Builder()
    .url("https://example.com/api/2")
    .build()

// 创建 Call 对象并发送请求
val call1 = client.newCall(request1)
val call2 = client.newCall(request2)

// 异步执行请求
call1.enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // 处理请求失败逻辑
    }

    override fun onResponse(call: Call, response: Response) {
        // 处理请求成功逻辑
    }
})

call2.enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // 处理请求失败逻辑
    }

    override fun onResponse(call: Call, response: Response) {
        // 处理请求成功逻辑
    }
})

在上述代码中,我们首先创建了一个 OkHttpClient 实例,并设置支持 HTTP/2 和 HTTP/1.1 协议。然后创建了两个 Request 对象,分别对应两个不同的 API 接口。最后使用 OkHttpClient 的 newCall 方法创建了两个 Call 对象,并使用 enqueue 方法异步执行请求。

由于 OkHttp 库默认支持 HTTP/2 多路复用,所以在同一个 TCP 连接上可以同时发送两个请求,而不需要等待前一个请求响应完毕。当服务器响应两个请求后,OkHttp 会自动将响应分配给对应的 Call 对象,并分别调用它们的 onResponse 回调方法。这样就实现了 HTTP/2 多路复用。

python

下面是一个使用Python代码实现HTTP/2多路复用的示例:

python
import asyncio
import aiohttp

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://example.com') as response1, \
                   session.get('https://example.net') as response2:
            print(await response1.text())
            print(await response2.text())

asyncio.run(main())

在这个示例中,我们使用aiohttp库来发送两个HTTP请求:一个请求https://example.com,另一个请求https://example.net。由于我们使用的是同一个aiohttp的ClientSession对象,所以这两个请求将会共享同一个TCP连接,从而实现了HTTP/2的多路复用。

在这个示例中,我们使用了Python的async/await语法来编写异步代码。我们首先创建了一个aiohttp的ClientSession对象,然后使用这个对象来发送两个HTTP请求。我们使用async with语句来自动关闭这个ClientSession对象。

我们使用了Python的async with语句来同时处理两个请求。这样做的好处是可以实现并行处理多个请求,从而提高程序的效率。在这个示例中,我们使用了Python的await语句来等待每个请求的响应。由于我们使用的是同一个TCP连接,所以这两个请求可以并行处理,从而实现了HTTP/2的多路复用。

http2 服务器推送

HTTP/2服务器推送是一种提高性能和响应时间的技术,可以在客户端请求之前发送额外的资源。在HTTP/2中,服务器可以通过推送帧将资源推送到客户端,客户端可以选择接受或拒绝这些资源。

java

在Java中,可以使用Netty框架来实现HTTP/2服务器推送。下面是一个示例代码:

java
// 创建HTTP/2连接
Http2Connection connection = new DefaultHttp2Connection(true);

// 创建HTTP/2帧处理器
Http2FrameProcessor frameProcessor = new DefaultHttp2FrameProcessor(connection);

// 创建HTTP/2服务器推送器
Http2ServerPusher serverPusher = new DefaultHttp2ServerPusher(connection, frameProcessor);

// 创建HTTP/2流
Http2Stream stream = connection.local().createStream(1, false);

// 创建HTTP/2推送帧
Http2PushPromiseFrame pushPromiseFrame = new DefaultHttp2PushPromiseFrame(1, stream.id(), new DefaultHttp2Headers().add("content-type", "text/plain"), 0);

// 推送资源到客户端
serverPusher.push(stream, pushPromiseFrame, new ChannelPromiseAdapter(new DefaultChannelPromise(stream.channel(), ImmediateEventExecutor.INSTANCE)));

在这个示例中,我们首先创建了一个HTTP/2连接和一个帧处理器。然后我们使用帧处理器创建了一个HTTP/2服务器推送器,并创建了一个HTTP/2流。

接下来,我们创建了一个HTTP/2推送帧,并使用服务器推送器将资源推送到客户端。需要注意的是,在实际应用中,我们需要根据实际情况动态地选择推送哪些资源,以避免推送过多的资源导致客户端性能下降。

kotlin

Kotlin OkHttp示例代码:

kotlin
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
    .build()

val request = Request.Builder()
    .url("https://example.com")
    .header("Accept-Encoding", "gzip")
    .build()

val call = client.newCall(request)
call.enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        e.printStackTrace()
    }

    override fun onResponse(call: Call, response: Response) {
        if (!response.isSuccessful) {
            throw IOException("Unexpected code $response")
        }

        // 读取服务器推送的资源
        response.body?.byteStream()?.use { stream ->
            // 处理服务器推送的资源
        }
    }
})

在这个示例中,我们首先创建了一个OkHttpClient实例,并设置了支持HTTP/2协议。然后,我们创建了一个Request实例,指定了要访问的URL,并设置了Accept-Encoding请求头,以指示我们接受gzip压缩的响应。

接下来,我们使用client.newCall(request)方法创建一个Call实例,并使用call.enqueue()方法异步执行请求。在Callback回调中,我们可以检查响应是否成功,并读取服务器推送的资源。

python

下面是一个使用Python实现HTTP/2服务器推送的示例代码:

python
import http.server
import socketserver

PORT = 8000

Handler = http.server.SimpleHTTPRequestHandler

class MyHandler(Handler):
    def do_GET(self):
        if self.path == '/':
            # 发送HTML页面
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            with open('index.html', 'rb') as f:
                self.wfile.write(f.read())
            # 推送JS和CSS资源
            self.push_resource('/main.js', 'application/javascript')
            self.push_resource('/style.css', 'text/css')
        else:
            super().do_GET()

    def push_resource(self, path, content_type):
        self.send_response(200)
        self.push_headers()
        self.wfile.write(('Content-type: %s\r\n' % content_type).encode('utf-8'))
        self.end_headers()
        with open(path, 'rb') as f:
            self.wfile.write(f.read())

上述代码中,我们创建了一个自定义的Handler类,重写了do_GET方法。在处理根路径请求时,我们先发送HTML页面,然后使用push_resource方法推送JS和CSS资源。push_resource方法会发送一个200响应,然后调用push_headers方法发送推送相关的HTTP头信息,最后发送JS或CSS资源内容。

使用上述代码启动一个HTTP/2服务器后,当浏览器请求根路径时,服务器会先发送HTML页面,然后推送JS和CSS资源。如果浏览器支持HTTP/2并启用了服务器推送功能,它就会在收到HTML页面时同时收到JS和CSS资源,从而提高页面加载速度。

http2 服务器推送 vs SSE

HTTP/2服务器推送和SSE(Server-Sent Events)的服务端推送都是用于提高性能和响应时间的技术,但它们有一些区别:

  1. 协议不同:HTTP/2服务器推送是基于HTTP/2协议的,而SSE是基于HTTP/1.1协议的。

  2. 推送方式不同:HTTP/2服务器推送是通过推送帧将资源推送到客户端,而SSE是通过HTTP长连接在服务端和客户端之间传递数据。

  3. 推送对象不同:HTTP/2服务器推送可以推送任何类型的资源,包括HTML、CSS、JavaScript、图片等,而SSE主要用于推送文本数据,如JSON、XML等。

  4. 客户端支持不同:HTTP/2服务器推送需要客户端支持HTTP/2协议,而SSE可以在任何支持HTTP/1.1协议的浏览器中使用。

  5. 安全性不同:HTTP/2服务器推送可以使用TLS加密保证数据传输的安全性,而SSE没有内置的安全性保护措施,需要开发者自己实现。

综上所述,HTTP/2服务器推送和SSE的服务端推送有各自的优势和适用场景,开发者需要根据实际情况选择合适的技术来实现服务端推送。

控流

HTTP/2的控流机制是通过流控制和窗口控制来实现的。流控制是为了防止某个流占用过多的带宽,窗口控制是为了防止整个连接占用过多的带宽。

流控制是在每个流上进行的,每个流都有一个流控制窗口,用于控制发送方发送数据的速度。接收方会在收到数据后发送WINDOW_UPDATE帧来告诉发送方可以继续发送数据。发送方会根据接收方返回的窗口大小和流控制窗口大小来决定发送数据的大小。

窗口控制是在整个连接上进行的,每个端点都有一个连接窗口。连接窗口的大小是由SETTINGS_INITIAL_WINDOW_SIZE设置的。发送方会根据连接窗口大小和每个流的流控制窗口大小来决定发送数据的大小。接收方会在收到数据后发送WINDOW_UPDATE帧来告诉发送方可以继续发送数据。

java

在Java中,可以使用Netty框架来实现HTTP/2流量控制。下面是一个示例代码:

java
// 创建HTTP/2连接
Http2Connection connection = new DefaultHttp2Connection(true);

// 创建HTTP/2流量控制器
Http2RemoteFlowController remoteFlowController = new DefaultHttp2RemoteFlowController(connection);

// 创建HTTP/2流
Http2Stream stream = connection.local().createStream(1, false);

// 获取流控制窗口大小
int initialWindowSize = remoteFlowController.initialWindowSize(stream);

// 更新流控制窗口大小
remoteFlowController.incrementWindowSize(stream, 1024);

// 发送数据到流
ByteBuf data = Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8);
remoteFlowController.sendFlowControlled(stream, data, data.readableBytes(), new ChannelPromiseAdapter(new DefaultChannelPromise(stream.channel(), ImmediateEventExecutor.INSTANCE)));

在这个示例中,我们首先创建了一个HTTP/2连接和一个流量控制器。然后我们创建了一个HTTP/2流,并获取了其初始流控制窗口大小。接下来,我们使用incrementWindowSize方法来增加流控制窗口的大小,然后使用sendFlowControlled方法将数据发送到流中。

需要注意的是,在实际应用中,我们需要根据实际情况动态地调整流控制窗口的大小,以避免某些流占用过多的带宽,导致其他流的传输速度变慢。

kotlin

下面是使用Kotlin OkHttp库实现HTTP/2控流的示例代码:

kotlin
val client = OkHttpClient.Builder()
        .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
        .build()

val request = Request.Builder()
        .url("https://example.com")
        .build()

val call = client.newCall(request)

call.enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // Handle failure
    }

    override fun onResponse(call: Call, response: Response) {
        // Handle response
    }

    override fun onHeaders(call: Call, headers: Headers) {
        // Handle headers
    }

    override fun onStream(stream: Stream) {
        // Handle stream
        val source = stream.source()
        val buffer = Buffer()

        while (!source.exhausted()) {
            val bytesToRead = minOf(source.windowSize.toInt(), 8192)
            val bytesRead = source.read(buffer, bytesToRead.toLong())
            if (bytesRead == -1L) {
                break
            }
            stream.windowUpdate(bytesRead.toInt())
            // Process buffer
        }
    }
})

在上面的代码中,我们创建了一个OkHttpClient实例,设置了支持HTTP/2协议。然后创建一个Request对象并发送异步请求。在回调函数中,我们可以处理请求的响应、头信息、流信息。在处理流信息的回调函数中,我们使用stream.source()获取数据源,然后使用source.windowSize()获取流控制窗口大小。我们使用minOf(source.windowSize.toInt(), 8192)获取每次读取数据的大小,然后使用source.read(buffer, bytesToRead.toLong())读取数据。如果读取到了数据,我们使用stream.windowUpdate(bytesRead.toInt())告诉发送方可以继续发送数据。最后,我们可以处理读取到的数据。

python

python
import socket

# 定义连接参数
HOST = 'www.example.com'
PORT = 443

# 创建socket连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

# 发送HTTP/2请求
s.sendall(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')
s.sendall(b'\x00\x00\x12\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01')

# 发送SETTINGS帧,设置连接的初始窗口大小
s.sendall(b'\x00\x00\x06\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00')

# 发送HEADERS帧,请求网站首页
s.sendall(b'\x00\x00\x0c\x01\x05\x00\x00\x00\x01\x82\x87\x85\x41\x8b\x08\x05\x3d\x3f\x0d\x0a')

# 发送DATA帧,请求网站首页
s.sendall(b'\x00\x00\x0f\x00\x01\x00\x00\x00\x01Hello, World!\n')

# 发送WINDOW_UPDATE帧,更新连接的窗口大小
s.sendall(b'\x00\x00\x04\x08\x00\x00\x00\x01\x00\x00\x00\x64')

# 发送WINDOW_UPDATE帧,更新流的窗口大小
s.sendall(b'\x00\x00\x04\x08\x00\x00\x00\x01\x00\x00\x00\x64')

# 接收HTTP/2响应
data = s.recv(1024)
print(data)

# 关闭socket连接
s.close()

以上示例中,我们在发送数据之前,使用了WINDOW_UPDATE帧来更新连接和流的窗口大小。在这个例子中,我们将连接和流的窗口大小都设置为100,表示接收方可以接收100字节的数据。如果发送方发送的数据超过了窗口大小,就需要等待接收方更新窗口大小。这样就可以避免发送方发送过多的数据,导致接收方无法处理。

Released under the MIT License.