前言
传统的请求-应答模式(http)越来越不能满足现实需求,服务器过于被动,而采用轮训或者long poll的方式过于浪费资源,这便有了WebSocket。WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说,二者区别如下。
- HTTP是运行在TCP协议传输层上的应用协议,而WebSocket是通过HTTP协议协商如何连接,然后独立运行在TCP协议传输层上的应用协议。
- Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
- websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
接下来使用一个小例子来实现服务器往客户端的主动推送功能。
示例
index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>无标题文档</title>
<script type="text/javascript">
var socket;
if(!window.WebSocket){
window.WebSocket = window.MozWebSocket;
}
if(window.WebSocket){
socket = new WebSocket("ws://127.0.0.1:12345/ws");
socket.onmessage = function(event){
var ta = document.getElementById('responseText');
ta.value += event.data+"\r\n";
};
socket.onopen = function(event){
var ta = document.getElementById('responseText');
ta.value = "这里显示服务器推送信息"+"\r\n";
};
socket.onclose = function(event){
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = "WebSocket 关闭"+"\r\n";
};
}else{
alert("您的浏览器不支持WebSocket协议!");
}
function send(message){
if(!window.WebSocket){return;}
if(socket.readyState == WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket 连接没有建立成功!");
}
}
</script>
</head>
<body>
<form onSubmit="return false;">
<input type="text" name="message" value="这里输入消息" /> <br />
<br /> <input type="button" value="发送 WebSocket 请求消息"
onClick="send(this.form.message.value)" />
<hr color="blue" />
<h3>服务端返回的应答消息</h3>
<textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
</html>
第一次握手请求由客户端发起,当服务器收到握手请求后,返回响应,这时客户端收到详情并打开socket完成握手,这样就建立了服务器与客户端之间的tcp长连接,对于 WebSocket 来说,它必须依赖HTTP协议的第一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
服务器目录结构如下:
首先看下启动类:
WebsocketApplication.java
package com.jhz.websocket;
import com.jhz.websocket.server.NettyServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebsocketApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebsocketApplication.class, args);
new NettyServer(12345).start();
}
}
在启动类中启动NettyServer(Netty服务器)。
NettyServer.java
package com.jhz.websocket.server;
import com.jhz.websocket.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
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.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @author jhz
* @date 18-10-21 下午9:45
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast(new WebSocketHandler());
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
这里要注意这四个Handler,HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler,其中HttpServerCodec用于对HttpObject消息进行编码和解码,但是HTTP请求和响应可以有很多消息数据,你需要处理不同的部分,可能也需要聚合这些消息数据,这是很麻烦的。为了解决这个问题,Netty提供了一个聚合器,它将消息部分合并到FullHttpRequest和FullHttpResponse,因此不需要担心接收碎片消息数据,这就是HttpObjectAggregator的作用;ChunkedWriteHandler,允许通过处理ChunkedInput来写大的数据块;而WebSocketServerProtocolHandler是Netty封装好的WebSocket协议处理类,有了它可以少写很多步骤,包括握手的过程,以及url的定义(这里的/ws其实就定义了url指定的后缀)。
WebSocketHandler.java
package com.jhz.websocket.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Scanner;
/**
* @author jhz
* @date 18-10-21 下午9:51
*/
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭!");
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("客户端收到服务器数据:" + msg.text());
Scanner s = new Scanner(System.in);
System.out.println("服务器推送:");
while(true) {
String line = s.nextLine();
if(line.equals("exit")) {
ctx.channel().close();
break;
}
String resp= "(" +ctx.channel().remoteAddress() + ") :" + line;
ctx.writeAndFlush(new TextWebSocketFrame(resp));
}
}
}
可以看到,建立长连接的过程都由WebSocketServerProtocolHandler为我们做完了(但是个人觉得还是要去自己写一次http握手的处理过程,Netty也做了一些封装,非常方便),客户端与服务器之间形成了一个全双工通讯的管道。
DefaultController.java
package com.jhz.websocket.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author jhz
* @date 18-10-21 下午8:25
*/
@Controller
public class DefaultController {
@RequestMapping("/")
public String index(){
return "index";
}
}
application.properties
# 定位模板的目录
spring.mvc.view.prefix=classpath:/templates/
# 给返回的页面添加后缀名
spring.mvc.view.suffix=.html
测试结果
在前端页面发送123:
在服务器的控制台可以看到已经收到了消息:
在服务器控制台推送消息“456”、“789”,再次查看前端页面:
WebSocket的小Demo便完成了。
小结
在前面的IO章节中,已经对比了使用Netty与传统的NIO方式的区别,Netty是高度封装的NIO框架,用起来会比传统的NIO编程方式方便很多,而其对WebSocket的支持同样为我们带来了极大的便利,WebSocket服务器在接收到客户端消息时需要对其判断,这个消息是http消息还是已经建立tcp连接的WebSocketFrame消息,若是前者,则代表是握手请求,服务器需要对握手请求进行响应,通常的写法如下:
private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
//如果不是WebSocket握手请求消息,那么就返回 HTTP 400 BAD REQUEST 响应给客户端。
if (!req.getDecoderResult().isSuccess()
|| !("websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
//如果是握手请求,那么就进行握手
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
WEB_SOCKET_URL, null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
// 通过它构造握手响应消息返回给客户端,
// 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
// 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
handshaker.handshake(ctx.channel(), req);
}
}
而使用WebSocketServerProtocolHandler就能为我们省下很多事了。其实通常使用tomcat不需要我们实现WebSocket,从tomcat7之后就开始支持Websocket了,这里为了进一步的学习一下Netty,但是万一不用Tomcat呢?相对于Tomcat这种Web Server(顾名思义主要是提供Web协议相关的服务的),Netty是一个 是一个Network Server,是处于Web Server更下层的网络框 架,也就是说你可以使用Netty模仿Tomcat做一个提供HTTP服务的Web容器。简而言之,Netty通过使用NIO的很多新特性,对TCP/UDP编程进行了简化和封 装,提供了更容易使用的网络编程接口,让你可以根据自己的需要封装独特的HTTP Server或者FTP Server等.