netty(六)WebSocket实践
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// http 编解码
pipeline.addLast(new HttpServerCodec());
// 大对象出站
// pipeline.addLast(new ChunkedWriteHandler());
// 捕捉出站异常
pipeline.addLast(new OutBoundExceptionHandler());
// 聚合
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 初始http
pipeline.addLast(new HttpRequestHandler());
// netty 代理 握手,处理 close ping pong
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// string -> websocketframe
pipeline.addLast(new WebSocketFrameEncoder());
// websocketframe -> string
pipeline.addLast(new WebSocketFrameDecoder());
// 读空闲9秒激发
ch.pipeline().addLast(new IdleStateHandler(9, 0, 0, TimeUnit.SECONDS));
// 自定义 websocket 处理
pipeline.addLast(new TextWebSocketFrameHandler());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
super.exceptionCaught(ctx, cause);
}
}
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
public static AttributeKey<String> key = AttributeKey.valueOf("userName");
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
String url = request.getUri();
if(-1 != url.indexOf("/ws")) {
String temp [] = url.split(";");
String name = URLDecoder.decode(temp[1], "UTF-8");
ctx.channel().attr(key).set(name);
// 传递到下一个handler:升级握手
ctx.fireChannelRead(request.retain());
} else {
System.out.println("not socket");
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
public class WebSocketFrameDecoder extends MessageToMessageDecoder<WebSocketFrame> {
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
if(frame instanceof TextWebSocketFrame) {
TextWebSocketFrame tframe = (TextWebSocketFrame)frame;
// 内存泄漏
// out.add(tframe.retain().text()); // 虽然 frame 会自动释放,且往下传递的是String对象,所以让MessgeToMessageDecoder自动释放掉fram
out.add(tframe.text());
} else {
// 不必要,但这种情况下release无害
// frame.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
}
}
public class WebSocketFrameEncoder extends MessageToMessageEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
// MessageToMessageEncoder 会自动释放msg,if msg instanceof ReferenceCounted
// out.add(new TextWebSocketFrame(msg).retain()); // 往下一个outHandler传递,不用+1年龄
out.add(new TextWebSocketFrame(msg));
}
}
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<String> {
private static final String SPLIT = ":\t";
private static final ChannelGroup group = new DefaultChannelGroup("ChannelGroups", GlobalEventExecutor.INSTANCE);
private static final ConcurrentHashMap<String, Channel> userChannel = new ConcurrentHashMap<String, Channel>();
private static String getOnine() {
StringBuilder stringBuilder = new StringBuilder(group.size() * 20);
String online = "[在线用户]";
stringBuilder.append(online);
for(Channel channel : group) {
String name = channel.attr(HttpRequestHandler.key).get();
stringBuilder.append(" " + name);
}
return stringBuilder.toString();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
// 移除性能更加
ctx.pipeline().remove(HttpRequestHandler.class);
String userName = ctx.channel().attr(HttpRequestHandler.key).get();
Channel old = userChannel.put(userName, ctx.channel());
if(old != null) {
group.remove(old);
System.out.println(old.attr(HttpRequestHandler.key).get() + SPLIT + "[切换设备]");
ChannelFuture channelFuture = old.writeAndFlush("您的账户在其它地方登陆");
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
ctx.writeAndFlush("-=====登录成功=====-");
String up = userName + SPLIT + "[上线]";
System.out.println(up);
group.writeAndFlush(up);
group.add(ctx.channel());
String online = getOnine();
System.out.println(online);
group.writeAndFlush(online);
}else if (evt instanceof IdleStateEvent) {
// 2*4+1 s内读空闲时,关掉连接,表示客户端不可见了
IdleStateEvent evnet = (IdleStateEvent) evt;
if (evnet.state().equals(IdleState.READER_IDLE)) {
ctx.close();
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String userName = ctx.channel().attr(HttpRequestHandler.key).get();
userChannel.remove(userName);
String down = userName + SPLIT + "[下线]";
System.out.println(down);
group.writeAndFlush(down);
group.remove(ctx.channel());
String online = getOnine();
System.out.println(online);
group.writeAndFlush(online);
super.channelInactive(ctx);
}
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 这一段也是错的,书上因为是直接write,因此要retain,这里已经新建了String实例,所以retain会造成内存泄漏
// String send = ctx.channel().attr(HttpRequestHandler.key).get() + SPLIT + msg.retain().text();
// System.out.println(send);
// group.writeAndFlush(new TextWebSocketFrame(send));
if("HeartBeat".equals(msg)) {
// 心跳只给自己发
ctx.writeAndFlush(msg);
} else {
// 聊天发给所有人
String send = ctx.channel().attr(HttpRequestHandler.key).get() + SPLIT + msg;
System.out.println(send);
group.writeAndFlush(send);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
public class OutBoundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if(!channelFuture.isSuccess()) {
channelFuture.cause().printStackTrace();
channelFuture.channel().close();
}
}
});
}
}
public class WebSocketServerJob implements Runnable {
static final EventLoopGroup CLIENT_BOSS_LOOP_GROUP = new NioEventLoopGroup(4);
static final EventLoopGroup CLIENT_WORKER_LOOP_GROUP = new NioEventLoopGroup(4);
public static void main(String[] args) throws Exception {
new Thread(new WebSocketServerJob()).start();
}
@Override
public void run() {
startHttpServer(8990);
}
public static void startHttpServer(int port) {
try {
ServerBootstrap bs = new ServerBootstrap();
bs.group(CLIENT_BOSS_LOOP_GROUP, CLIENT_WORKER_LOOP_GROUP);
bs.channel(NioServerSocketChannel.class);
bs.childHandler(new WebSocketServerInitializer());
ChannelFuture future = bs.bind(port).sync();
System.out.println("web socket server start at " + port);
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
CLIENT_BOSS_LOOP_GROUP.shutdownGracefully();
CLIENT_WORKER_LOOP_GROUP.shutdownGracefully();
}
}
}
前端:http://localhost:8080/wechat-demo/websocket.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket demo</title>
<script src="jquery-1.11.3.js"></script>
<script type="text/javascript">
var urlRoad = 'ws://192.168.201.227:8990/ws;' ;
var ws = null;
function connect () {
if(ws != null) return;
ws = new WebSocket(urlRoad + $("#fname").val());
ws.onopen = WSonOpen;
ws.onmessage = WSonMessage;
ws.onclose = WSonClose;
ws.onerror = WSonError;
}
function WSonOpen() {
alert("登陆成功,连接已经建立。");
};
function WSonMessage(event) {
$("#board").val(event.data + "\n" + $("#board").val());
};
function WSonClose() {
ws = null;
alert("连接关闭。");
};
function WSonError() {
alert("WebSocket错误。");
};
function send() {
if(ws == null) {
alert("未登录");
return;
}
var text = $("#tosend").val();
if(text.trim() == '') return;
$("#tosend").val('');
ws.send(text)
}
</script>
</head>
<body>
用户名: <input type="text" id="fname"><input type="submit" onclick="connect()" value="登陆">
<br><textarea onchange="this.scrollTop=this.scrollHeight" style= "overflow-x:auto" id="board" rows=10 cols=80></textarea>
<br><input id="tosend" type="text" style="width:400px"></input><input type="submit" onclick="send()" value="发送">
</body>
</html>
1 疑问:pipeline.addLast(new WebSocketServerProtocolHandler(""));这一句中,有实际情况表明有时需要 "/ws" 服务器才能握手
2 这篇文章提供另外一种实现结构:https://www.cnblogs.com/tohxyblog/p/7946498.html
2021.4.5 补充,该文章与
/Users/joyce/work/jds/warn/push-center/push_center/src/main/java/com/jince/push/ws/WebSocketHandler.java
好像一致,都是在运行期执行:handshaker.handshake(ctx.channel(), req);
这里,利用了 netty channel的线程安全性与@Sharable 的结论:运行期pipeline可以随意add、remove,因为每个channel都有一个自己的pipeline,自始至终只会被一个线程执行
/*
* 功能:读取 h5页面发送过来的信息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {// 如果是HTTP请求,进行HTTP操作
LOGGER.debug("当前为Http请求");
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {// 如果是Websocket请求,则进行websocket操作
LOGGER.debug("当前为Websocket请求");
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
private static final String WSURI = "/ws";
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
// 如果HTTP解码失败,返回HHTP异常
if (req instanceof HttpRequest) {
HttpMethod method = req.method();
// 如果是websocket请求就握手升级
if (method.equals(HttpMethod.GET) && WSURI.equalsIgnoreCase(req.uri())) {
LOGGER.debug("req instanceof HttpRequest");
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(wsFactoryUri, null,
false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
}
}
public final ChannelFuture handshake(Channel channel, FullHttpRequest req, HttpHeaders responseHeaders, final ChannelPromise promise) {
if(logger.isDebugEnabled()) {
logger.debug("{} WebSocket version {} server handshake", channel, this.version());
}
FullHttpResponse response = this.newHandshakeResponse(req, responseHeaders);
ChannelPipeline p = channel.pipeline();
if(p.get(HttpObjectAggregator.class) != null) {
p.remove(HttpObjectAggregator.class);
}
if(p.get(HttpContentCompressor.class) != null) {
p.remove(HttpContentCompressor.class);
}
ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
final String encoderName;
if(ctx == null) {
ctx = p.context(HttpServerCodec.class);
if(ctx == null) {
promise.setFailure(new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
return promise;
}
p.addBefore(ctx.name(), "wsdecoder", this.newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", this.newWebSocketEncoder());
encoderName = ctx.name();
} else {
p.replace(ctx.name(), "wsdecoder", this.newWebsocketDecoder());
encoderName = p.context(HttpResponseEncoder.class).name();
p.addBefore(encoderName, "wsencoder", this.newWebSocketEncoder());
}
当然本文的脉络更清晰
3 ChannelGroup的基础实践参见:https://www.cnblogs.com/silyvin/articles/9413329.html
4 package:链接: https://pan.baidu.com/s/1y7X8Jik7ycgaOmz3mYuYUg 密码: 9xy9
5 实践版本4.0.17.Final,在更高版本中,4.1.17.Final,
FullHttpRequest.getUri与WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE已被置为:@Deprecated,虽然能够继续使用,但需要:
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
public static AttributeKey<String> key = AttributeKey.valueOf("userName");
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
String url = request.getUri();
if(-1 != url.indexOf("/ws")) {
String temp [] = url.split(";");
String name = URLDecoder.decode(temp[1], "UTF-8");
ctx.channel().attr(key).set(name);
// 在更高版本中4.1.17.Final,此句是需要的
// request.setUri("/ws");
// 传递到下一个handler:升级握手
ctx.fireChannelRead(request.retain());
} else {
System.out.println("not socket");
ctx.close();
}
}
6 (2019.12.5)
每个WebSocket连接都始于一个HTTP请求。具体来说,WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
注意,关键的地方是,这里面有个Upgrade首部,用来把当前的HTTP请求升级到WebSocket协议,这是HTTP协议本身的内容,是为了扩展支持其他的通讯协议。如果服务器支持新的协议,则必须返回101:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
7 2021.5.6
浏览器tab关闭,会发送fin包,netty这边进入channelInActive
浙公网安备 33010602011771号