本文基于Netty实现WebSocket服务端,实现和客户端的交互通信,客户端基于JavaScript实现。
在【WebSocket简介-CSDN博客】中,我们知道WebSocket是基于Http协议的升级,而Netty提供了Http和WebSocket Frame的编解码器和Handler,我们可以基于Netty快速实现WebSocket服务端。
一、基于Netty快速实现WebSocket服务端
我们可以直接利用Netty提供了Http和WebSocket Frame的编解码器和Handler,快速启动一个WebSocket服务端。服务端收到Client消息后,转发给其他客户端。
服务端代码:
ChannelSupervise:用户存储客户端信息
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) {
GlobalGroup.add(channel);
ChannelMap.put(channel.id().asShortText(), channel.id());
}
public static void removeChannel(Channel channel) {
GlobalGroup.remove(channel);
ChannelMap.remove(channel.id().asShortText());
}
public static Channel findChannel(String id) {
return GlobalGroup.find(ChannelMap.get(id));
}
public static void send2All(TextWebSocketFrame tws) {
GlobalGroup.writeAndFlush(tws);
}
}
服务端Netty配置:
import io.netty.bootstrap.ServerBootstrap;
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.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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
*
* 实现长链接 客户端与服务端;
*/
public class SimpleWsChatServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).
// 在 bossGroup 增加一个日志处理器
handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 基于http协议的长连接 需要使用http协议的解码 编码器
pipeline.addLast(new HttpServerCodec());
// 以块的方式处理
pipeline.addLast(new ChunkedWriteHandler());
/**
* http数据传输过程中是分段, HttpObjectAggregator 将多个段聚合起来
* 当浏览器发起大量数据的时候,会发起多次http请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/**
* 对于websocket是以frame的形式传递
* WebSocketFrame
* 浏览器 ws://localhost:7070/ 不在是http协议
* WebSocketServerProtocolHandler 将http协议升级为ws协议 即保持长链接
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/helloWs"));
// 自定义handler专门处理浏览器请求
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7070).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
消息转发逻辑:
import com.huawei.websocket.nio2.global.ChannelSupervise;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* TextWebSocketFrame 表示一个文本贞
* 浏览器和服务端以TextWebSocketFrame 格式交互
*/
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:MM:ss");
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame)
throws Exception {
System.out.println("服务端收到消息:" + textWebSocketFrame.text());
// 回复浏览器
String resp = sdf.format(new Date()) + ": " + textWebSocketFrame.text();
// 转发给所有的客户端
ChannelSupervise.send2All(new TextWebSocketFrame(resp));
// 发送给当前客户单
// channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame(resp));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加连接
System.out.println("客户端加入连接:" + ctx.channel());
ChannelSupervise.addChannel(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 断开连接
System.out.println("客户端断开连接:" + ctx.channel());
ChannelSupervise.removeChannel(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生:" + cause.getMessage());
ctx.channel().close();
}
}
参考:NIO框架Netty+WebSocket实现网页聊天_nio实现websocket-CSDN博客
一、基于Netty,手动处理WebSocket握手信息:
参考:GitCode - 开发者的代码家园https://gitcode.com/Siwash/websocketWithNetty/blob/master/README.md
基于netty搭建websocket,实现消息的主动推送_websocket_rpf_siwash-GitCode 开源社区
服务端代码:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
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.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.apache.log4j.Logger;
/**
* https://gitcode.com/Siwash/websocketWithNetty/blob/master/README.md
*/
public class NioWebSocketServer {
private final Logger logger = Logger.getLogger(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 ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
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,这里处理WebSocket建链请求和消息发送请求
}
});
Channel channel = bootstrap.bind(7070).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();
}
}
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import com.huawei.websocket.nio2.global.ChannelSupervise;
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.channel.SimpleChannelInboundHandler;
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.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import org.apache.log4j.Logger;
import java.util.Date;
/**
* 这里处理WebSocket建链请求和消息发送请求
*/
public class NioWebSocketHandler extends SimpleChannelInboundHandler<Object> {
private final Logger logger = Logger.getLogger(getClass());
private WebSocketServerHandshaker handshaker;
@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) {
logger.debug("服务端收到ping消息");
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;
}
logger.debug("服务端收到WebSocket建链消息");
WebSocketServerHandshakerFactory wsFactory =
new WebSocketServerHandshakerFactory("ws://localhost:7070/helloWs", 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);
}
}
}
这里我们手动处理了握手信息:
可以看到服务端handler日志:
2024-05-24 09:43:51 DEBUG [NioWebSocketHandler] 收到消息:HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0))
GET /helloWs HTTP/1.1
Host: localhost:7070
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Sec-WebSocket-Key: KYh4//SBKLi+nSu6v1kYqw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
content-length: 0
2024-05-24 09:43:51 DEBUG [NioWebSocketHandler] 服务端收到WebSocket建链消息
Client1的发送和接收消息,在F12小可以看到:
测试用的Client代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
WebSocket Client
</title>
</head>
<body>
<script>
var socket;
//check current explorer wether support WebSockek
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:7070/helloWs");
//event : msg from server
socket.onmessage = function(event) {
console.log("receive msg:" + event.data);
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + event.data;
}
//eq open WebSocket
socket.onopen = function(event) {
var rt = document.getElementById("responseText");
rt.value = "open WebSocket";
}
socket.onclose = function(event) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "close WebSocket";
}
} else {
alert("current exploer not support websocket")
}
//send msg to WebSocket Server
function send(message) {
if (socket.readyState == WebSocket.OPEN) {
//send msg to WebSocket by socket
socket.send(message);
} else {
alert("WebSocket is not open")
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="height: 300px; width: 300px;"></textarea>
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px;"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
直接保存为html文件,使用浏览器打开即可运行测试;