Java游戏服务器开发之二十四–在服务器中添加WebSocket的支持
写在前面
对于只想了解基于netty的WebSocket协议可以看这篇,现在我们是在项目添加,会对其中的一些内容进行修改,
本次更新变化的内容
添加
- WebSocketChannelInitializer.java 服务初始化
- WebSocketHandler.java 消息处理类
- IMessageToWebSocketFrameEncoder 消息发出去的时候进行编码
- WebSocketFrameToIMessageDecoder 消息接收到时进行解码
- HttpResponseUtil.java 返回握手时的http请求消息
- WebSocketClientHandlerTest.java 客户端测试
- WebSocketClientTest.java 客户端消息处理
修改
- MessageDecoder.java 添加一个decodePub方法
- BasicServerImpl.java 根据不同的protocolType设置ChannelInitializer
- server-config-dev.properties 添加一个protocolType=WEBSOCKET
执行过程
- 服务器启动,启动过程中根据server-config-dev.properties中配置的protocolType创建不同的服务,这里是WEBSOCKET
- 配置WEBSOCKET对应的ChannelInitializer,这里是WebSocketChannelInitializer,在其中添加http协议的支持及编解码的过滤器,最后在之中添加消息处理器WebSocketHandler
- 在WebSocketHandler中消息接收的时候添加对HTTP请求和webSocket请求的处理
- 在客户端中进行模拟访问,WebSocketClientTest
代码中的关键
- IMessageToWebSocketFrameEncoder和WebSocketFrameToIMessageDecoder都是继承MessageToMessageEncoder
- 在WebSocketHandler中要针对不同的情况进行处理
if (msg instanceof FullHttpRequest) {
// 传统的HTTP接入
handleHttpMessage(ctx, msg);
} else if (msg instanceof WebSocketFrame) {
// WebSocket接入
handleWebSocketMessage(ctx, msg);
} else if (msg instanceof IMessage) {
// 这里已经通过WebSocketFrameToIMessageDecoder进行解码,获得我们设置好的IMessage类了
consumer.consume((IMessage) msg, ctx.channel());
}
- 在模拟客户端的时候,WebSocketClientHandlerTest中要持有WebSocketClientHandshaker对象,用于发送http握手请求
具体代码
- WebSocketChannelInitializer.java 服务初始化
/*
* Copyright (C), 2015-2018
* FileName: WebSocketChannelInitializer
* Author: zhao
* Date: 2018/8/10 11:42
* Description: webSocket的channel
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.server.channel.websocket;
import com.lizhaoblog.base.message.codec.IMessageToWebSocketFrameEncoder;
import com.lizhaoblog.base.message.codec.WebSocketFrameToIMessageDecoder;
import com.lizhaoblog.server.pojo.ServerConfig;
import org.springframework.stereotype.Component;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* 〈一句话功能简述〉<br>
* 〈webSocket的channel〉
*
* @author zhao
* @date 2018/8/10 11:42
* @since 1.0.1
*/
@Component
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码
pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持
// 消息编解码
pipeline.addLast("encoder", new IMessageToWebSocketFrameEncoder());
pipeline.addLast("decoder", new WebSocketFrameToIMessageDecoder());
WebSocketHandler webSocketHandler = (WebSocketHandler) ServerConfig.getInstance().getApplicationContext()
.getBean("webSocketHandler");
pipeline.addLast(webSocketHandler);
}
}
- WebSocketHandler.java 消息处理类
/*
* Copyright (C), 2015-2018
* FileName: WebSocketHandler
* Author: zhao
* Date: 2018/8/10 11:44
* Description: websocket的消息处理
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.server.channel.websocket;
import com.lizhaoblog.base.constant.ConstantValue;
import com.lizhaoblog.base.exception.MessageCodecException;
import com.lizhaoblog.base.message.IMessage;
import com.lizhaoblog.base.message.codec.MessageDecoder;
import com.lizhaoblog.base.message.impl.ByteMessage;
import com.lizhaoblog.base.message.impl.MessageFactory;
import com.lizhaoblog.base.network.customer.INetworkConsumer;
import com.lizhaoblog.base.network.listener.INetworkEventListener;
import com.lizhaoblog.base.session.SessionManager;
import com.lizhaoblog.base.util.HttpResponseUtil;
import com.lizhaoblog.server.pojo.ServerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
/**
* 〈一句话功能简述〉<br>
* 〈websocket的消息处理〉
*
* @author zhao
* @date 2018/8/10 11:44
* @since 1.0.1
*/
@Component
@Scope("prototype")
public class WebSocketHandler extends SimpleChannelInboundHandler<Object> {
@Autowired
private INetworkEventListener listener;
@Autowired
private INetworkConsumer consumer;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws MessageCodecException {
if (msg instanceof FullHttpRequest) {
// 传统的HTTP接入
handleHttpMessage(ctx, msg);
} else if (msg instanceof WebSocketFrame) {
// WebSocket接入
handleWebSocketMessage(ctx, msg);
} else if (msg instanceof IMessage) {
// 这里已经通过WebSocketFrameToIMessageDecoder进行解码,获得我们设置好的IMessage类了
consumer.consume((IMessage) msg, ctx.channel());
}
}
/**
* 处理WebSocket中的Http消息
*
* @param ctx 上下文
* @param msg 消息
*/
private void handleHttpMessage(ChannelHandlerContext ctx, Object msg) {
// 传统的HTTP接入
FullHttpRequest request = (FullHttpRequest) msg;
// 如果HTTP解码失败,返回HHTP异常
if (!request.decoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) {
HttpResponseUtil.sendHttpResponse(ctx, request,
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
// 正常WebSocket的Http连接请求,构造握手响应返回
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://" + request.headers().get(HttpHeaderNames.HOST), null, false);
WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(request);
if (handshaker == null) { // 无法处理的websocket版本
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else { // 向客户端发送websocket握手,完成握手
handshaker.handshake(ctx.channel(), request);
}
}
/**
* 处理WebSocket中的WebSocket消息
*
* @param ctx 上下文
* @param msg 消息
*/
private void handleWebSocketMessage(ChannelHandlerContext ctx, Object msg) throws MessageCodecException {
ByteBuf content = ((WebSocketFrame) msg).content();
MessageDecoder messageDecoder = new MessageDecoder(ConstantValue.MESSAGE_CODEC_MAX_FRAME_LENGTH,
ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_LENGTH, ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_OFFSET,
ConstantValue.MESSAGE_CODEC_LENGTH_ADJUSTMENT, ConstantValue.MESSAGE_CODEC_INITIAL_BYTES_TO_STRIP, false,
ServerConfig.getInstance().getMessageType());
IMessage iMessage = messageDecoder.decodePub(ctx, content);
// WebSocket接入
consumer.consume(iMessage, ctx.channel());
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
listener.onConnected(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
listener.onDisconnected(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) {
listener.onExceptionCaught(ctx, throwable);
}
}
- IMessageToWebSocketFrameEncoder 消息发出去的时候进行编码
/*
* Copyright (C), 2015-2018
* FileName: WebSocketFrameEncoder
* Author: zhao
* Date: 2018/8/13 17:12
* Description: WebSocketFrameEncoder编码器
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.base.message.codec;
import com.lizhaoblog.base.message.IMessage;
import java.util.List;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
/**
* 〈一句话功能简述〉<br>
* 〈WebSocketFrameEncoder编码器〉
*
* @author zhao
* @date 2018/8/13 17:12
* @since 1.0.1
*/
public class IMessageToWebSocketFrameEncoder extends MessageToMessageEncoder<IMessage> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, IMessage iMessage, List<Object> list)
throws Exception {
//组合缓冲区
CompositeByteBuf byteBuf = Unpooled.compositeBuffer();
byte[] bodyBytes = iMessage.getBodyByte();
byteBuf.writeShort(iMessage.getMessageId());
byteBuf.writeShort(iMessage.getStatusCode());
byteBuf.writeInt(bodyBytes.length);
byteBuf.writeBytes(bodyBytes);
list.add(new BinaryWebSocketFrame(byteBuf));
}
}
- WebSocketFrameToIMessageDecoder 消息接收到时进行解码
/*
* Copyright (C), 2015-2018
* FileName: WebSocketFrameToIMessageDecoder
* Author: zhao
* Date: 2018/8/13 19:25
* Description: WebSocketFrame转换成IMessage
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.base.message.codec;
import com.lizhaoblog.base.constant.ConstantValue;
import com.lizhaoblog.base.message.IMessage;
import com.lizhaoblog.server.pojo.ServerConfig;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
/**
* 〈一句话功能简述〉<br>
* 〈WebSocketFrame转换成IMessage〉
*
* @author zhao
* @date 2018/8/13 19:25
* @since 1.0.1
*/
public class WebSocketFrameToIMessageDecoder extends MessageToMessageDecoder<Object> {
@Override
protected void decode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
if (msg instanceof FullHttpRequest) {
// 传统的HTTP接入
out.add(msg);
} else if (msg instanceof WebSocketFrame) {
// out.add(msg)
// WebSocket接入
ByteBuf content = ((WebSocketFrame) msg).content();
MessageDecoder messageDecoder = new MessageDecoder(ConstantValue.MESSAGE_CODEC_MAX_FRAME_LENGTH,
ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_LENGTH, ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_OFFSET,
ConstantValue.MESSAGE_CODEC_LENGTH_ADJUSTMENT, ConstantValue.MESSAGE_CODEC_INITIAL_BYTES_TO_STRIP, false,
ServerConfig.getInstance().getMessageType());
IMessage iMessage = messageDecoder.decodePub(ctx, content);
out.add(iMessage);
} else {
out.add(msg);
}
}
}
- HttpResponseUtil.java 返回握手时的http请求消息
/*
* Copyright (C), 2015-2018
* FileName: HttpResponseUtil
* Author: zhao
* Date: 2018/8/11 12:16
* Description: 处理http的工具类
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.base.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.util.CharsetUtil;
/**
* 〈一句话功能简述〉<br>
* 〈处理http的工具类〉
*
* @author zhao
* @date 2018/8/11 12:16
* @since 1.0.1
*/
public final class HttpResponseUtil {
/**
* Http返回
*
* @param ctx
* @param request
* @param response
*/
public static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
// 返回应答给客户端
if (response.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(response, response.content().readableBytes());
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(response);
if (!HttpUtil.isKeepAlive(request) || response.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private HttpResponseUtil() {
}
}
- WebSocketClientHandlerTest.java 客户端测试
/*
* Copyright (C), 2015-2018
* FileName: WebSocketClientHandlerTest
* Author: zhao
* Date: 2018/8/2 17:32
* Description:
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.server.channel.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.CharsetUtil;
/**
* 〈一句话功能简述〉<br>
* 〈〉
*
* @author zhao
* @date 2018/8/2 17:32
* @since 1.0.1
*/
public class WebSocketClientHandlerTest extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = LoggerFactory.getLogger(WebSocketClientHandlerTest.class);
private final WebSocketClientHandshaker handshaker;
private ChannelPromise handshakeFuture;
public WebSocketClientHandlerTest(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
handshakeFuture = ctx.newPromise();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable arg1) {
logger.info("异常发生", arg1);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// logger.info("数据内容:data=" + data);
Channel ch = ctx.channel();
if (!handshaker.isHandshakeComplete()) {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
System.out.println("WebSocket Client connected!");
handshakeFuture.setSuccess();
return;
}
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.getStatus() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
System.out.println("WebSocket Client received message: " + textFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
System.out.println("WebSocket Client received pong");
} else if (frame instanceof CloseWebSocketFrame) {
System.out.println("WebSocket Client received closing");
ch.close();
}
//关闭链路
// ctx.close();
// channelInactive(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("客户端连接建立");
// 在通道连接成功后发送握手连接
handshaker.handshake(ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("客户端连接断开");
super.channelInactive(ctx);
}
}
- WebSocketClientTest.java
/*
* Copyright (C), 2015-2018
* FileName: WebSocketClientTest
* Author: zhao
* Date: 2018/8/2 17:32
* Description: websocket测试
* History:
* <author> <time> <version> <desc>
* 作者姓名 修改时间 版本号 描述
*/
package com.lizhaoblog.server.channel.websocket;
import com.lizhaoblog.base.message.codec.IMessageToWebSocketFrameEncoder;
import com.lizhaoblog.base.message.codec.WebSocketFrameToIMessageDecoder;
import com.lizhaoblog.base.message.impl.ByteMessage;
import com.lizhaoblog.common.CommonValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URISyntaxException;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
/**
* 〈一句话功能简述〉<br>
* 〈websocket测试〉
*
* @author zhao
* @date 2018/8/2 17:32
* @since 1.0.1
*/
public class WebSocketClientTest {
private static final Logger logger = LoggerFactory.getLogger(WebSocketClientTest.class);
private static EventLoopGroup group = new NioEventLoopGroup();
private static WebSocketClientHandlerTest handler;
private String uriStr = "ws//" + CommonValue.IP + ":" + CommonValue.PORT;
public void run() throws InterruptedException, URISyntaxException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
// 主要是为handler(自己写的类)服务
URI wsUri = new URI(uriStr);
WebSocketClientHandshaker webSocketClientHandshaker = WebSocketClientHandshakerFactory
.newHandshaker(wsUri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders(), 100 * 1024 * 1024);
handler = new WebSocketClientHandlerTest(webSocketClientHandshaker);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast("encoder", new IMessageToWebSocketFrameEncoder());
pipeline.addLast("decoder", new WebSocketFrameToIMessageDecoder());
// pipeline.addLast("encoder", new MessageEncoder());
// pipeline.addLast("decoder", new MessageDecoder(ConstantValue.MESSAGE_CODEC_MAX_FRAME_LENGTH,
// ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_LENGTH, ConstantValue.MESSAGE_CODEC_LENGTH_FIELD_OFFSET,
// ConstantValue.MESSAGE_CODEC_LENGTH_ADJUSTMENT, ConstantValue.MESSAGE_CODEC_INITIAL_BYTES_TO_STRIP,
// false, ServerConfig.getInstance().getMessageType()));
pipeline.addLast(handler);
}
});
ByteMessage byteMessage = new ByteMessage();
byteMessage.setMessageId(com.lizhaoblog.server.biz.constant.CommonValue.CM_MSG_TEST_BYTE);
byteMessage.setStatusCode(com.lizhaoblog.server.biz.constant.CommonValue.MSG_STATUS_CODE_SUCCESS);
byteMessage.addAttr(2);
// 连接服务端
// ChannelFuture channelFuture = bootstrap.connect(CommonValue.IP, CommonValue.PORT).sync();
ChannelFuture channelFuture = bootstrap.connect(CommonValue.IP, CommonValue.PORT).sync();
handler.handshakeFuture().sync();
// channelFuture.channel().writeAndFlush(byteMessage);
// WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[] { 8, 1, 8, 1 }));
// WebSocketFrame frame = new TextWebSocketFrame("aaa");
// channelFuture.channel().writeAndFlush(frame);
// byte[] bytes = new byte[14];
// bytes[0] = 0;
// bytes[1] = 1;
// bytes[2] = 0;
// bytes[3] = 1;
// bytes[4] = 0;
// bytes[5] = 0;
// bytes[6] = 0;
// bytes[7] = 6;
// bytes[8] = 2;
// bytes[9] = 0;
// bytes[10] = 0;
// bytes[11] = 0;
// bytes[12] = 0;
// bytes[13] = 1;
//// channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer(bytes));
// BinaryWebSocketFrame frame = new BinaryWebSocketFrame(Unpooled.copiedBuffer(bytes));
// channelFuture.channel().writeAndFlush(frame);
channelFuture.channel().writeAndFlush(byteMessage);
logger.info("向Socket服务器发送数据:" + byteMessage);
channelFuture.channel().closeFuture().sync();
}
}
上面的代码在码云上 https://gitee.com/lizhaoandroid/JgServer
以加qq群一起探讨Java游戏服务器开发的相关知识 676231564