WebSocket
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
当WebSocket的客户端与服务端通信以后,就不需要之前握手请求HTTP协议的参与了
WebSocket的优点
- 节省通信开销(HttpRequest中的head很长,占用带宽和资源)
- 服务器可以主动传送数据给客户端
- 实时通信
因为HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
WebSocket的生命周期
WebSocket端点生命周期的第一个事件是打开通知,它用来指示到WebSocket会话另一端的连接已经建立。一旦打开通知被WebSocket对话的两端都接收到,参与的任意WebSocket后续就可以发送消息了。在WebSocket对话期间,可能会出现一些消息传递的错误。接受消息的WebSocket端点本身就可能产生错误,或者WebSocket实现本身在某些情况下也会产生错误。要注意对错误的处理。不管在WebSocket对话的哪一端准备结束对话,他都可以初始化关闭事件。下面从Java组件的视角来看看其生命周期如何呈现。
-
打开事件:@OnOpen 此事件发生在端点上建立新连接时并且在任何其他事件发生之前
-
消息事件:@OnMessage 此事件接收WebSocket对话中另一端发送的消息。
-
错误事件:@OnError 此事件在WebSocket连接或者端点发生错误时产生
-
关闭事件:@OnClose 此事件表示WebSocket端点的连接目前部分地关闭,它可以由参与连接的任意一个端点发出
Netty实现:
1、前端页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
Netty WebSocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8090/srws");
socket.onmessage = function (event) {
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = event.data
};
socket.onopen = function (event) {
var ta = document.getElementById('responseText');
ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
};
socket.onclose = function (event) {
var ta = document.getElementById('responseText');
ta.value = "";
ta.value = "WebSocket 关闭!";
};
}
else {
alert("抱歉,您的浏览器不支持WebSocket协议!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
}
else {
alert("WebSocket连接没有建立成功!");
}
}
</script>
<form onsubmit="return false;">
<input type="text" name="message" value="Netty实践"/>
<br><br>
<input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/>
<hr color="blue"/>
<h3>服务端返回的应答消息</h3>
<textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>
2、服务端
server
package com.suirui.websocket_http.server;
import com.suirui.websocket_http.server.handler.HttpServerRequestHandler;
import com.suirui.websocket_http.server.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by zongx on 2019/12/26.
*/
public class Server {
public static void main(String[] args) {
int port ;
if (args != null && args.length > 0) {
port = Integer.valueOf(args[0]);
} else {
port = 8090;
}
System.out.println(port);
new Server().bind(port);
}
public void bind(int port) {
//配置服务端的NIO线程组
//两个线程组的原因是:
// 一个用于服务端接收客户端连接
//一个用于进行socketChannel网络读写
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
// 业务用。http工作线程池,http消息处理都由这个线程池进行
ExecutorService httpExecutor = Executors.newFixedThreadPool(8);
//ServerBootstrap 是netty的辅助启动类
ServerBootstrap b = new ServerBootstrap();
// .group传入两个线程组
// .channel设置创建的Channel类型为NioServerSocketChannel
// .option 配置NioServerSocketChannel的TCP参数
// .childHandler绑定IO事件的处理类 类似reactor模式中的handler:进行连接与读写socket。
// ChildChannelHandler会重写initChannel,保证当创建NioServerSocketChannel成功之后,再进行初始化。
/*************.handler 与 .childHandler的区别*******/
// handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后。
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChildChannelHandler(httpExecutor));
//绑定端口,等待成功
try {
//.sync()是同步阻塞接口,等待绑定操作完成
//ChannelFuture主要用于异步操作的通知回调
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
//注意此处泛型不要使用ServerSocketChannel,否则客户端无法启动端口
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
private ExecutorService httpExecutor;
public ChildChannelHandler( ExecutorService httpExecutor) {
this.httpExecutor = httpExecutor;
}
@Override
//websocket与http使用同一netty服务器是为了端口统一
protected void initChannel(SocketChannel ch) throws Exception {
//继承了ChannelHandlerAppender,并且创建了一个HttpRequestDecode和一个HttpResponseEncoder
ch.pipeline().addLast(new HttpServerCodec());
// 目的是将多个消息转换为单一的request或者response对象,参数为聚合后http请求的大小线程
ch.pipeline().addLast(new HttpObjectAggregator(64 * 1024));
//目的是支持异步大文件传输,websocket通讯需要
ch.pipeline().addLast(new ChunkedWriteHandler());
//http业务逻辑
ch.pipeline().addLast(new HttpServerRequestHandler(httpExecutor, "/http"));
//支持websocket通讯
//插入位置,顺序非常重要,必须插在http编解码器的后面,必须插在当前处理器的前面
//处理ping-pong 二帧。
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/srws"));
//websocket业务逻辑
ch.pipeline().addLast(new WebSocketHandler());
}
}
}
HttpServerRequestHandler
package com.suirui.websocket_http.server.handler;
import com.alibaba.fastjson.JSONObject;
import com.suirui.websocket_http.server.controller.HttpWorkerThread;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
import org.apache.log4j.Logger;
import java.util.concurrent.ExecutorService;
/**
* Created by zongx on 2019/12/31.
*/
public class HttpServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static Logger logger = Logger.getLogger(HttpServerRequestHandler.class);
private final String httpPrefix;
private ExecutorService httpExecutors;
public HttpServerRequestHandler(ExecutorService httpExecutors, String httpPrefix) {
this.httpPrefix = httpPrefix;
this.httpExecutors = httpExecutors;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
String uri = msg.getUri();
if (uri.equalsIgnoreCase("/srws")) {
//如果请求了WebSocket协议,则增加引用技术,并将它传递给下一个ChannelInboundHandler
ctx.fireChannelRead(msg.retain());
} else if (uri.startsWith(httpPrefix)) {
//判断前端来的url路径是否正确
JSONObject json = new JSONObject();
json.put("code", 200);
json.put("message", "操作成功");
//获取要进行的操作与json参数
int index = uri.indexOf("?");
String op = uri.substring(httpPrefix.length(), index == -1 ? uri.length() : index);
String param = (index == -1) ? "" : uri.substring(index + 1);
String body = "";
//获取请求体
int len = msg.content().readableBytes();
if (len > 0) {
byte[] content = new byte[len];
msg.content().readBytes(content);
body = new String(content, "UTF-8");
}
//通过多线程线程池将处理结果返还
HttpWorkerThread t = new HttpWorkerThread(
msg.getProtocolVersion(),
HttpHeaders.isKeepAlive(msg),
ctx.channel(),
param,
body,
op);
httpExecutors.execute(t);
}
}
}
package com.suirui.websocket_http.server.controller;
import com.alibaba.fastjson.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.*;
import org.apache.log4j.Logger;
import java.io.UnsupportedEncodingException;
/**
* Created by zongx on 2019/12/31.
*/
public class HttpWorkerThread implements Runnable {
private Channel browserChannel;
private String param;
private String body;
private String operation;
private HttpVersion protocalVersion;
private boolean keepAlive;
private static Logger logger = Logger.getLogger(HttpWorkerThread.class);
/**
* @Description: 构造方法,保存
* @Author: zongx
* @Date: 2019/12/31
* @Param: protocolVersion:http协议版本
* @Param: keepAlive:是否是长连接
* @Param: channel:http请求来的channel
* @Param: param:参数
* @Param: body:请求体
* @Param: op:操作字符
* @return
*/
public HttpWorkerThread(HttpVersion protocolVersion, boolean keepAlive, Channel channel, String param, String body, String op) {
this.browserChannel = channel;
this.param = param;
this.body = body;
this.operation = op;
this.keepAlive = keepAlive;
this.protocalVersion = protocolVersion;
}
@Override
public void run() {
try {
//获取请求内容
JSONObject json = (JSONObject) JSONObject.parse(this.body);
System.out.println(json);
//构造响应体
JSONObject result = new JSONObject();
result.put("code", 200);
switch (this.operation) {
//通知当前服务邀请
case "/add":
result.put("message", "增加成功");
break;
//通知当前服务邀请
case "/del":
result.put("message", "删除成功");
break;
//通知当前服务邀请
case "/update":
result.put("message", "更新成功");
break;
//通知当前服务邀请
case "/select":
result.put("message", "查找成功");
break;
}
//构造响应
FullHttpResponse response = new DefaultFullHttpResponse(
this.protocalVersion,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer(result.toJSONString().getBytes("UTF-8"))
);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
if (this.keepAlive) {
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
//返回response
ChannelFuture future = browserChannel.writeAndFlush(response);
// 如果不是长连接,要监听当前channel关闭
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
//关闭当前channel
browserChannel.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
WebSocketHandler
package com.suirui.websocket_http.server.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.apache.commons.lang3.StringUtils;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
/**
* Created by zongx on 2020/1/6.
*/
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception {
if (msg instanceof TextWebSocketFrame) {
TextWebSocketFrame request = (TextWebSocketFrame) msg;
String receiveMsg = request.text();
System.out.println("websocketServer 收到信息: " + receiveMsg);
if(StringUtils.isNotBlank(receiveMsg)) {
JSONObject jsonObject = JSON.parseObject(receiveMsg);
String op = jsonObject.getString("op");
switch (op) {
//通知当前服务邀请
case "add":
jsonObject.put("message", "增加成功");
break;
//通知当前服务邀请
case "del":
jsonObject.put("message", "删除成功");
break;
//通知当前服务邀请
case "update":
jsonObject.put("message", "更新成功");
break;
//通知当前服务邀请
case "select":
jsonObject.put("message", "查找成功");
break;
}
String s = jsonObject.toJSONString();
TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(s);
ctx.channel().writeAndFlush(textWebSocketFrame);
}
}
}
}