websocket协议是对http协议的扩充, 也是使用的TCP协议可以全双工通信的应用层协议。 websocket协议允许服务端向客户端推送消息。 浏览器和服务端只需要进行一次握手,不必像http协议一样,每次连接都要新建立连接,两者之间创建持久性的连接,并进行双向的数据交互。
http/1.1 是 请求-响应设计的,后来支持了更多的传输类型 图片,但都是基于请求响应。
不足:
websocket首次请求服务端建立连接,也是客户端发起的,基于http请求的。 请求头中多携带消息
GET /test HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg==
Origin: http://example.com
Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13
首先客户端(如浏览器)发出带有特殊消息头(Upgrade
、Connection
)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket
就是握手成功。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Sec-WebSocket-Protocol: v12.stomp
Sec-WebSocket-Accept
的字段值是由握手请求中的Sec-WebSocket-Key
的字段值生成的。成功握手确立WebSocket
连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket
独立的数据帧。
WebSocket
使用二进制消息帧作为双向通信的媒介。何为消息帧?发送方将每个应用程序消息拆分为一个或多个帧,通过网络将它们传输到目的地,并重新组装解析出一个完整消息。
有别于HTTP/1.1
文本消息格式(冗长的消息头和分隔符等),WebSocket
消息帧规定一定的格式,以二进制传输,更加短小精悍。二者相同之处就是都是基于TCP/IP流式协议(没有规定消息边界)
FIN:
1 bit,表示该帧是否为消息的最后一帧。1-是,0-否。RSV1,RSV2,RSV3
: 1 bit each,预留(3位),扩展的预留标志。一般情况为0,除非协商的扩展定义为非零值。如果接收到非零值且不为协商扩展定义,接收端必须使连接失败。Opcode
: 4 bits,定义消息帧的操作类型,如果接收到一个未知Opcode,接收端必须使连接失败。(0x0-延续帧,0x1-文本帧,0x2-二进制帧,0x8-关闭帧,0x9-PING帧,0xA-PONG帧(在接收到PING帧时,终端必须发送一个PONG帧响应,除非它已经接收到关闭帧),0x3-0x7保留给未来的非控制帧,0xB-F保留给未来的控制帧)Mask
: 1 bit,表示该帧是否为隐藏的,即被加密保护的。1-是,0-否。Mask=1时,必须传一个Masking-key,用于解除隐藏(客户端发送消息给服务器端,Mask必须为1)。Payload length
: 7 bits, 7+16 bits, or 7+64 bits,有效载荷数据的长度(扩展数据长度+应用数据长度,扩展数据长度可以为0)。Masking-key
: 0 or 4 bytes,用于解除帧隐藏(加密)的key,Mask=1时不为空,Mask=0时不用传。Payload dat
a: (x+y) bytes,有效载荷数据包括扩展数据(x bytes)和应用数据(y bytes)。有效载荷数据是用户真正要传输的数据。这样的二进制消息帧设计,与HTTP协议相比,WebSocket
协议可以提供约500:1的流量减少和3:1的延迟减少。
挥手相对于握手要简单很多,客户端和服务器端任何一方都可以通过发送关闭帧来发起挥手请求。发送关闭帧的一方,之后不再发送任何数据给对方;接收到关闭帧的一方,如果之前没有发送过关闭帧,则必须发送一个关闭帧作为响应。关闭帧中可以携带关闭原因。
在发送和接收一个关闭帧消息之后,就认为WebSocket连接已关闭,且必须关闭底层TCP连接。
除了通过关闭握手来关闭连接外,WebSocket连接也可能在另一方离开或底层TCP连接关闭时突然关闭。
协议介绍与图片来自https://blog.csdn.net/weixin_36586120/article/details/120025498
Websocket
需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。Websocket
定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。Websocket
定义了扩展,用户可以扩展协议、实现部分自定义的子协议。Websocket
在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著提高压缩率。初始handler需要添加http协议的编解码器。
添加 HttpServerCodec
转为HttpRequest,添加HttpObjectAggregator
将http消息聚合为一个FullHttpRequest,因为websocket的协议handler的channerRead接受的是该参数。
添加WebSocketServerProtocolHandler
,这个是websocket协议的处理器,会处理首次请求的握手操作,并升级协议,更换处理器。
当客户端连接时,WebSocketServerProtocolHandler 触发 handlerAdded()
回调,会立刻为这个channel注册一个握手处理器。 握手处理器位置是先于websocket处理器 但是晚于http协议处理器。
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
// Add the WebSocketHandshakeHandler before this one.
ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols,
allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));
}
当客户端发送第一个http握手请求,请求升级为websocket协议。先是经过http协议处理器,将消息转为了FullHttpMessgae,之后在 握手处理的read方法中。
//1. 根据ws协议版本号创建了一个指定版本的处理器。
final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,
allowExtensions, maxFramePayloadSize, allowMaskMismatch);
final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
//2. 指定版本的处理器 处理握手请求
final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
//3.握手处理器,替换为新的处理器, 这个处理器对非ws请求拒绝处理。
ctx.pipeline().replace(this, "WS403Responder",
WebSocketServerProtocolHandler.forbiddenHttpRequestResponder());
4的第二步 将处理都交到指定ws协议WebSocketServerHandshaker手中,这个处理是先创建一个响应,告诉客户端服务升级。 之后将http相关的处理器都移除掉, 添加ws协议相关的处理器。
// 1. 首先创建服务端确定升级ws的响应
FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
//2.移除http协议处理器
ChannelPipeline p = channel.pipeline();
if (p.get(HttpObjectAggregator.class) != null) {
p.remove(HttpObjectAggregator.class);
}
if (p.get(HttpContentCompressor.class) != null) {
p.remove(HttpContentCompressor.class);
}
//3. 添加ws协议的编解码器 ,也是handler
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
此时客户端收到服务端响应,就升级为ws协议了,之后的用户自定义的处理器,就能处理了。
package eWebscoket;
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.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* netty对websocket协议实现测试:
* 1. netty 提供了{@link WebSocketServerProtocolHandler} handler用于处理websocket协议。
* handler中有ws的握手支持,ping、pong响应,close请求这些支持, 同时将Text 与 binary 数据传递
* 给之后的handler进行业务处理。WebSocketServerProtocolHandler 通过WebSocketServerProtocolHandshakeHandler 首次read请求
* 确定websocket协议版本(一般是13), 给channel绑定了不容版本协议下的WebSocket13FrameDecoder。
*
*
* 2.WebSocketServerProtocolHandler在有新的channel连接注册回调方法中{@link WebSocketServerProtocolHandler#handlerAdded(ChannelHandlerContext)},
* 会在当前handler中添加一个新的handler {@link WebSocketServerProtocolHandshakeHandler}专门用于处理首次客户端请求的握手操作, 查看
* {@link io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandshakeHandler#channelRead(ChannelHandlerContext, Object)}的接收客户端数据的处理,会为客户返回
* 一个FullHttpResponse,响应头中,含有确认更新为websocket协议的响应。 当这个channel写会给客户端,此时这个channel的通信协议就有http转为websocket,这个握手用的handler也会
* 自己移除了。
*
* 3. 握手处理过程:
* 就会将pipLine中用于握手的http的解析handler给移除掉了,
* p.remove(HttpContentCompressor.class);
* p.remove(HttpObjectAggregator.class);
* 然后在 HttpServerCodec 的 handler 之前添加 websocket协议的handler:
* p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder()); {@link WebSocket08FrameDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List)}
* p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder()); {@link WebSocket08FrameEncoder#encode(io.netty.channel.ChannelHandlerContext, io.netty.handler.codec.http.websocketx.WebSocketFrame, java.util.List)}
* 之后将response写会客户端成功后升级,将HttpServerCodec移除掉,删除代码在handshake()方法的写会回调中。
* 在握手升级之后WebSocketServerProtocolHandshakeHandler 没有用了,就会将这个handler,替换成WebSocketServerProtocolHandler.forbiddenHttpRequestResponder()
* 对非ws的请求拒绝处理。
*
* @author mahao
* @date 2022/10/18
*/
public class ServerWebSocket {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加http消息的转换,将socket数据流 转换为 HttpRequest, websocket协议添加这个是为了
//握手时候使用。 HttpServerCodec 与 HttpObjectAggregator会在第一次http请求后,被移除掉,握手结束了
//协议升级就会只用websocket协议了。
pipeline.addLast(new HttpServerCodec());
//未知
pipeline.addLast(new ChunkedWriteHandler());
//将拆分的http消息聚合成一个消息。
pipeline.addLast(new HttpObjectAggregator(8096));
//用户websocket协议的 握手,ping pang处理, close处理,对于二进制或者文件数据,直接交付给下层
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast("myHandler", new WebSocketHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webSock客户端title>
head>
<body>
<script type="text/javascript">
var socket;
if (window.WebSocket){
socket= new WebSocket("ws://localhost:9999/ws");
socket.onmessage = function (ev) {
var ta = document.getElementById("responseText");
ta.value = ta.value + "\n" + ev.data;
}
socket.onopen = function (ev) {
var ta = document.getElementById("responseText");
ta.value = "连接开启";
}
socket.onclose = function (ev) {
var ta = document.getElementById("responseText");
ta.value = ta.value + "\n" + "连接关闭";
}
} else {
alert("浏览器不支持websocket");
}
function send(message) {
alert(123);
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN){
alert(123);
socket.send(message);
}
}
script>
<form onsubmit="return false;">
<textarea name="message" style="width: 400px;height: 200px">textarea>
<input type="button" value="发送消息" onclick="send(this.form.message.value);">
<h3>服务器端输出h3>
<textarea id="responseText" style="width: 400px;height: 300px">textarea>
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空内容">
form>
body>
html>