升级过程描述
- HTTP/1.1 协议提供了一种使用 Upgrade 标头字段的特殊机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。
- 客户端使用 Upgrade 标头字段请求服务器,以降序优先的顺序切换到其中列出的一个协议。
- 因为 Upgrade 是一个逐跳(Hop-by-hop)标头,它还需要在 Connection 标头字段中列出。这意味着包含 Upgrade 的典型请求类似于:
GET /index.html HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: example/1, foo/2
- 根据之前的请求的协议,可能需要其他标头信息,例如:从 HTTP/1.1 升级到 WebSocket 允许配置有关 WebSocket 连接的标头详细信息,以及在连接时提供一定程度的安全性。查看升级到 WebSocket 协议的连接获取更多信息。
- 如果服务器决定升级这次连接,就会返回一个 101 Switching Protocols 响应状态码,和一个要切换到的协议的标头字段 Upgrade。如果服务器没有(或者不能)升级这次连接,它会忽略客户端发送的 Upgrade 标头字段,返回一个常规的响应:例如一个 200 OK).
- 在发送 101 状态码之后,服务器可以使用新协议,并根据需要执行任何额外的特定于协议的握手。实际上,一旦这次升级完成了,连接就变成了双向管道。并且可以通过新协议完成启动升级的请求。
- 至今为止,最经常会需要升级一个 HTTP 连接的场合就是使用 WebSocket,它总是通过升级 HTTP 或 HTTPS 连接来实现。请记住,当你用 WebSocket API 以及其他大部分实现 WebSocket 的库去建立新的连接时,基本上都不用操心升级的过程,因为这些 API 已经实现了这一步。
协议升级示例
- 使用java语言实现一个http协议升级到websocket协议的服务器
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
public class UpgradeServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Server is listening on port 8080...");
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(() -> handleClient(clientSocket)).start();
}
}
public static void handleClient(Socket clientSocket) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
OutputStream out = clientSocket.getOutputStream()) {
String line;
StringBuilder request = new StringBuilder();
while (!(line = reader.readLine()).isEmpty()) {
request.append(line).append("\n");
}
System.out.println("Received Request: \n" + request);
if (request.toString().contains("Upgrade: websocket")) {
String secWebSocketKey = getSecWebSocketKey(request.toString());
String secWebSocketAccept = generateWebSocketAcceptKey(secWebSocketKey);
String response = "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: " + secWebSocketAccept + "\r\n\r\n";
out.write(response.getBytes(StandardCharsets.UTF_8));
out.flush();
System.out.println("Protocol upgraded to WebSocket!");
} else {
String httpResponse = "HTTP/1.1 200 OK\r\n\r\nHello, HTTP client!";
out.write(httpResponse.getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static String getSecWebSocketKey(String request) {
for (String line : request.split("\n")) {
if (line.startsWith("Sec-WebSocket-Key:")) {
return line.split(":")[1].trim();
}
}
return null;
}
public static String generateWebSocketAcceptKey(String secWebSocketKey) throws Exception {
String magicString = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
String acceptKey = secWebSocketKey + magicString;
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(acceptKey.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
}
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NettyWebSocketServer {
private final int port;
public NettyWebSocketServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new WebSocketFrameHandler());
}
});
Channel ch = bootstrap.bind(port).sync().channel();
System.out.println("WebSocket server started on port " + port);
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new NettyWebSocketServer(8080).start();
}
private static class WebSocketFrameHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof FullHttpRequest) {
System.out.println("Received HTTP request, upgrading to WebSocket...");
} else if (msg instanceof WebSocketFrame) {
System.out.println("Received WebSocket frame");
}
}
}
}
- 在nodejs中的net创建的server中绑定一个upgrade事件,这就是在协议升级的时候触发的。
协议升级的应用场景
- 通常使用的场景都是HTTP协议升级到WebSocket的场景。一般考虑的因素有如下:
- WebSocket 是基于 HTTP 的协议升级。WebSocket 是设计为在初始阶段通过 HTTP/1.1 或 HTTP/2 握手来建立连接的协议。这种设计使得 WebSocket 能够利用现有的 HTTP 基础设施进行连接,并在握手完成后切换到全双工通信模式。由于 WebSocket 是基于 HTTP 的,这意味着它可以更容易地通过现有的防火墙、代理和其他网络设备,而这些设备普遍支持 HTTP。这里就是考虑网络设备的兼容性、设备防火墙、代理服务器等等因素。
- 逐步回退机制。在某些情况下,WebSocket 可能无法通过特定的网络设备或防火墙,这时应用可以有回退机制。例如,应用可以首先尝试升级为 WebSocket,如果失败则可以回退到较为传统的轮询或长轮询的 HTTP 技术。通过 HTTP 开始连接,允许应用具备这种灵活性。