基于Netty实现WebSocket服务端

本文基于Netty实现WebSocket服务端,实现和客户端的交互通信,客户端基于JavaScript实现。

在【WebSocket简介-CSDN博客】中,我们知道WebSocket是基于Http协议的升级,而Netty提供了Http和WebSocket Frame的编解码器和Handler,我们可以基于Netty快速实现WebSocket服务端。

一、基于Netty快速实现WebSocket服务端

我们可以直接利用Netty提供了Http和WebSocket Frame的编解码器和Handler,快速启动一个WebSocket服务端。服务端收到Client消息后,转发给其他客户端。

服务端代码:

ChannelSupervise:用户存储客户端信息

java 复制代码
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配置:

java 复制代码
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();
        }
    }
}

消息转发逻辑:

java 复制代码
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 开源社区

服务端代码:

java 复制代码
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();
    }
}
java 复制代码
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日志:

bash 复制代码
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代码如下:

html 复制代码
<!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文件,使用浏览器打开即可运行测试;

相关推荐
FungLeo40 分钟前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
用户84298142418101 小时前
ByteJS,让重要的 JavaScript 以字节码运行。
javascript
张小姐的猫2 小时前
【基于 WebSocket 的多用户网页五子棋】—— 项目结构设计与实用工具类模块
网络·websocket·网络协议
FungLeo3 小时前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo3 小时前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query
程序员大阳3 小时前
HTML+CSS+JS对话式实战(7)-学生端活动报名功能开发
javascript·css·html·活动报名
西安小哥4 小时前
做 Agent 写到第三个小时,我开始给它算账
javascript
西安小哥4 小时前
让大模型操作浏览器跑了两周,我发现瓶颈根本不在模型
javascript
用户298698530145 小时前
用 Python 自动化把 JavaScript 代码转换为 Word 文档
javascript·python·api
学以智用5 小时前
正则表达式完整参考手册(JavaScript 版,兼容绝大多数语言)
javascript