WebSocket简介
WebSocket协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执
WebSocket特点:
- HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
- 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户端与服务端频繁交互的情况下,如实时共享、多人协作等平台
- 采用新的协议,后端需要单独实现
- 客户端并不是所有浏览器都支持
WebSocket通信握手
在从标准的 HTTP 或者 HTTPS协议切换到WebSocket时,将会使用一种称为握手的机制 ,因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后
下面是WebSocket请求和响应的标识信息:
客户端的请求:
- Connection属性中标识Upgrade,表示客户端希望连接升级
- Upgrade属性中标识为Websocket,表示希望升级成 Websocket 协议
- Sec-WebSocket-Key属性,表示随机字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
- Sec-WebSocket-Version属性,表示支持的 Websocket 版本,RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用
服务器端响应:
- Upgrade属性中标识为websocket
- Connection告诉客户端即将升级的是 Websocket 协议
- Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key
Netty为WebSocket数据帧提供的支持
由 IETF 发布的WebSocket RFC,定义了6种帧,Netty为它们每种都提供了一个POJO实现
帧类型 | 描述 |
---|---|
BinaryWebSocketFrame | 包含了二进制数据 |
TextWebSocketFrame | 包含了文本数据 |
ContinuationWebSocketFrame | 包含属于上一个BinaryWebSocketFrame 或TextWebSocketFrame 的文本或者二进制数据 |
CloseWebSocketFrame | 标识一个CLOSE请求,包含一个关闭的状态码 |
PingWebSocketFrame | 请求传输一个PongWebSocketFrame |
PongWebSocketFrame | 作为一个对于PingWebSocketFrame的响应被发送 |
实战
首先,定义WebSocket服务端,其中创建了一个Netty提供ChannelGroup变量用来记录所有已经连接的客户端channel,而这个ChannelGroup就是用来完成群发和单聊功能的
//定义websocket服务端
public class WebSocketServer {
private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private static EventLoopGroup workerGroup = new NioEventLoopGroup();
private static ServerBootstrap bootstrap = new ServerBootstrap();
private static final int PORT =8761;
//创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
public static void startServer(){
try {
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(channelGroup));
Channel ch = bootstrap.bind(PORT).sync().channel();
System.out.println("打开浏览器访问: http://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
startServer();
}
}
接下来,初始化Pipeline,向当前Pipeline中注册所有必需的ChannelHandler,主要包括:用于处理HTTP请求编解码的HttpServerCodec、自定义的处理HTTP请求的HttpRequestHandler、用于处理WebSocket帧数据以及升级握手的WebSocketServerProtocolHandler以及自定义的处理TextWebSocketFrame数据帧和握手完成事件的WebSocketServerHanlder
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{
/*websocket访问路径*/
private static final String WEBSOCKET_PATH = "/ws";
private ChannelGroup channelGroup;
public WebSocketServerInitializer(ChannelGroup channelGroup){
this.channelGroup=channelGroup;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//用于HTTP请求的编解码
ch.pipeline().addLast(new HttpServerCodec());
//用于写入一个文件的内容
ch.pipeline().addLast(new ChunkedWriteHandler());
//用于http请求的聚合
ch.pipeline().addLast(new HttpObjectAggregator(64*1024));
//用于WebSocket应答数据压缩传输
ch.pipeline().addLast(new WebSocketServerCompressionHandler());
//处理http请求,对非websocket请求的处理
ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));
//根据websocket规范,处理升级握手以及各种websocket数据帧
ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, "", true));
//对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件
ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));
}
}
HttpRequestHandler用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了WebSocket的URI,如果是那么HttpRequestHandler将调用FullHttpRequest对象上的retain方法,并通过调用fireChannelRead(msg)方法将它转发给下一个ChannelInboundHandler(之所以调用retain方法,是因为调用channelRead0方法完成之后,会进行资源释放)
接下来,读取磁盘上指定路径的index.html文件内容,将内容封装成ByteBuf对象,之后,构造一个FullHttpResponse响应对象,将ByteBuf添加进去,并设置请求头信息。最后,调用writeAndFlush方法冲刷所有写入的消息
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private static final File INDEX = new File("D:/学习/index.html");
private String websocketUrl;
public HttpRequestHandler(String websocketUrl)
{
this.websocketUrl = websocketUrl;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
if(websocketUrl.equalsIgnoreCase(msg.getUri())){
//如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理
ctx.fireChannelRead(msg.retain());
}else{
//生成index页面的具体内容,并送往浏览器
ByteBuf content = loadIndexHtml();
FullHttpResponse res = new DefaultFullHttpResponse(
HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE,
"text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, msg, res);
}
}
public static ByteBuf loadIndexHtml(){
FileInputStream fis = null;
InputStreamReader isr = null;
BufferedReader raf = null;
StringBuffer content = new StringBuffer()