由于Http协议是无状态的,每一次请求只能响应一次,下次请求需要重新连接。
如果客户端请求一个服务端资源,需要实时监服务端执行状态(比如导出大数据量时需要前端监控导出状态),这个时候不断请求连接浪费资源。可以通过WebSocket建立一个长连接,实现客户端与服务端双向交流。
websocket服务器
public class NioWebSocketServer {
private final Logger logger=Logger.getLogger(this.getClass());
private void init(){
logger.info("正在启动websocket服务器");
NioEventLoopGroup boss=new NioEventLoopGroup();
NioEventLoopGroup work=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap=new ServerBootstrap();
bootstrap.group(boss,work);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new NioWebSocketChannelInitializer());
Channel channel = bootstrap.bind(8083).sync().channel();
logger.info("webSocket服务器启动成功:"+channel);
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
logger.info("运行出错:"+e);
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
logger.info("websocket服务器已关闭");
}
}
public static void main(String[] args) {
new NioWebSocketServer().init();
}
}
ChannelInitializer
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NioWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
ch.pipeline().addLast("http-codec",new HttpServerCodec());//设置解码器
ch.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));//聚合器,使用websocket会用到
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());//用于大数据的分区传输
ch.pipeline().addLast("handler",new NioWebSocketHandler());//自定义的业务handler
}
}
Hander
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.apache.log4j.Logger;
import org.wisdom.netty.global.ChannelSupervise;
import java.util.Date;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
public class NioWebSocketHandler extends SimpleChannelInboundHandler<Object> {
private final Logger logger=Logger.getLogger(this.getClass());
private WebSocketServerHandshaker handshaker;
/**
重写channelRead0方法,处理接收到的消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.debug("收到消息:"+msg);
if (msg instanceof FullHttpRequest){
//以http请求形式接入,但是走的是websocket
handleHttpRequest(ctx, (FullHttpRequest) msg);
}else if (msg instanceof WebSocketFrame){
//处理websocket客户端的消息
handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//添加连接
logger.debug("客户端加入连接:"+ctx.channel());
ChannelSupervise.addChannel(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//断开连接
logger.debug("客户端断开连接:"+ctx.channel());
ChannelSupervise.removeChannel(ctx.channel());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){
// 判断是否关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程仅支持文本消息,不支持二进制消息
if (!(frame instanceof TextWebSocketFrame)) {
logger.debug("本例程仅支持文本消息,不支持二进制消息");
throw new UnsupportedOperationException(String.format(
"%s frame types not supported", frame.getClass().getName()));
}
// 返回应答消息
String request = ((TextWebSocketFrame) frame).text();
logger.debug("服务端收到:" + request);
TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
+ ctx.channel().id() + ":" + request);
// 群发
ChannelSupervise.send2All(tws);
// 返回【谁发的发给谁】
// ctx.channel().writeAndFlush(tws);
}
/**
* 唯一的一次http请求,用于创建websocket
* */
private void handleHttpRequest(ChannelHandlerContext ctx,
FullHttpRequest req) {
//要求Upgrade为websocket,过滤掉get/Post
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
//若不是websocket方式,则创建BAD_REQUEST的req,返回给客户端
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://localhost:8083/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
/**
* 拒绝不合法的请求,并返回错误信息
* */
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req, DefaultFullHttpResponse res) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
ChannelFuture f = ctx.channel().writeAndFlush(res);
// 如果是非Keep-Alive,关闭连接
if (!isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
存储信息
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ChannelSupervise {
private static ChannelGroup GlobalGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private static ConcurrentMap<String, ChannelId> ChannelMap=new ConcurrentHashMap();
public static void addChannel(Channel channel){
/**
channel(通道是socket的连接信息;) = [id: 0x2a5ba781, L:/127.0.0.1:8083 - R:/127.0.0.1:60663]
==ChannelMap=={28176dd1=28176dd1, 2a5ba781=2a5ba781}
*/
GlobalGroup.add(channel);
ChannelMap.put(channel.id().asShortText(),channel.id());
System.out.println("==channel=="+channel);
System.out.println("==ChannelMap=="+ChannelMap.toString());
}
public static void removeChannel(Channel channel){
GlobalGroup.remove(channel);
ChannelMap.remove(channel.id().asShortText());
System.out.println("==removeChannel=="+channel);
System.out.println("==removeChannelChannelMap=="+ChannelMap.toString());
}
public static Channel findChannel(String id){
return GlobalGroup.find(ChannelMap.get(id));
}
/**
* 根据channel id 进行 群发通知
* @param tws
*/
public static void send2All(TextWebSocketFrame tws){
GlobalGroup.writeAndFlush(tws);
}
}
html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
</head>
<body>
<h1>WebSocket Test</h1>
<div>
<input type="text" id="message" placeholder="Message">
<button onclick="send()">Send</button>
</div>
<div id="output"></div>
<script>
var socket = new WebSocket("ws://localhost:8083/");
socket.onopen = function(event) {
console.log("WebSocket opened: " + event);
};
socket.onmessage = function(event) {
console.log("WebSocket message received: " + event.data);
var output = document.getElementById("output");
output.innerHTML += "<p>" + event.data + "</p>";
};
socket.onclose = function(event) {
console.log("WebSocket closed: " + event);
};
function send() {
var message = document.getElementById("message").value;
socket.send(message);
}
</script>
</body>
</html>
redis 订阅
public void sendAlarmFaultMessage(String message) {
String newMessge= null;
try {
newMessge = new String(message.getBytes(RedisKeyConstant.UTF8), RedisKeyConstant.UTF8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//redisTemplate.convertAndSend(RedisKeyConstant.REDIS_CHANNEL, newMessge);
RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
RTopic topic = redissonClient.getTopic(RedisKeyConstant.REDIS_CHANNEL_FAULT);
topic.publish(newMessge);
redisTemplate.opsForList().rightPush(RedisKeyConstant.REDIS_MESSAGE_FAULT, newMessge);
}
redis发布
@Bean
public RTopic rFaultTopic(RedissonClient redissonClient) {
RTopic rTopic = redissonClient.getTopic(RedisKeyConstant.REDIS_CHANNEL_FAULT);
try{
if(rTopic != null){
rTopic.addListener(String.class, (channel, message) -> {
if (channel.toString().contains(RedisKeyConstant.REDIS_CHANNEL_FAULT)) {
RedisUtil.lpop(RedisKeyConstant.REDIS_MESSAGE_FAULT);
log.info("Channel is already fault "+message);
AlarmWebsocketService alarmWebsocketService = SpringUtil.getBean(AlarmWebsocketService.class);
alarmWebsocketService.sendAllMessage(message);
}
});
}
}catch (Exception e){
log.info("Error sending alarm "+ ExceptionUtils.getStackTrace(e));
}
return rTopic;
}