netty建立websocket通道

本文档展示了如何使用Netty构建WebSocket服务器并处理客户端连接。通过引入Netty依赖,定义`LcfcSebsocketChannelHandler`处理通道事件,包括连接建立、消息处理和异常捕获。同时,配置`LcfcWebsocketChannelInitializer`初始化通道,并启动`LcfcWebsockerServerStart`来启动WebSocket服务。此外,还提供了WebSocket客户端的`WebSocketClientHandler`处理类和启动客户端的`WebsocketClient`示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用netty建立websocket通道

项目地址

1、引入maven依赖

<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.60.Final</version>
		</dependency>

2、编写通道处理

@Slf4j
@NoArgsConstructor
public class LcfcSebsocketChannelHandler extends SimpleChannelInboundHandler<Object> {

    private static final String TOKEN="token";

    private WebsocketConfig  socketConfig;

    private WebSocketServerHandshaker handshaker;

    private WebsocketConnectLogService logService;

    public LcfcSebsocketChannelHandler(WebsocketConfig config,WebsocketConnectLogService logService){
        this.socketConfig = config;
        this.logService = logService;
    }


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        log.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 {
        //添加连接
//        String userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
        WebsocketUtils.addChannel(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("链接发生错误=",cause.getMessage());
        String userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
        saveConnectLog(ctx,userId,cause.getMessage(),MessageConstants.exceptionStatus.exception);
        WebsocketUtils.removeChannel(ctx.channel());
    }



    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //断开连接
        log.debug("客户端断开连接:"+ctx.channel());
        WebsocketUtils.removeChannel(ctx.channel());
        saveConnectLog(ctx,null,"客户端断开链接");
    }

    @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)) {
            log.debug("本例程仅支持文本消息,不支持二进制消息");
            throw new UnsupportedOperationException(String.format(
                    "%s frame types not supported", frame.getClass().getName()));
        }
        // 返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        log.debug("服务端收到:" + request);
        TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
                + ctx.channel().id() + ":" + request);
        // 群发
//        log.info("发送消息");
//        WebsocketUtils.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,返回给客户端
            saveConnectLog(ctx,null,"要求Upgrade为websocket",MessageConstants.exceptionStatus.exception);
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST),"非websocket请求");
            return;
        }


        String uri = req.uri();

        ConcurrentMap<String, String> paramMap = getUrlParams(uri);
        log.debug("获取uri中参数={}",paramMap);
        if (paramMap.get(TOKEN)==null){
            saveConnectLog(ctx,null,"请求参数中没有token",
                    MessageConstants.exceptionStatus.exception);
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST),"token不合法");
            return;
        }
        try {
           String userId =  PlatfromComponent.getInstants(socketConfig.getCheckUrl()).checkToken(paramMap.get(TOKEN));
           paramMap.put(WebsocketUtils.KEY_USER_ID,userId);
        }catch (Exception e){
            String msg = "根据token获取用户信息出错";
            log.error(msg);
            saveConnectLog(ctx,null,msg,
                    MessageConstants.exceptionStatus.exception);
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.BAD_REQUEST),msg);
            return;
        }

        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                "ws://"+socketConfig.getRemoteUrl()+":"+socketConfig.getPort()+"/websocket", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory
                    .sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);

            WebsocketUtils.setChannelParam(ctx.channel(),paramMap.get(WebsocketUtils.KEY_USER_ID));
            String ip = req.headers().get("X-real-ip");
            if (ip!=null){
                saveConnectLog(ctx,null,"链接建立成功",MessageConstants.exceptionStatus.normal,ip);
            }else {
                saveConnectLog(ctx,null,"链接建立成功");

            }
        }



    }
    /**
     * 拒绝不合法的请求,并返回错误信息
     * */
    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req, DefaultFullHttpResponse res,String msg) {
        // 返回应答给客户端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(msg,
                    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);
        }
    }


    private static ConcurrentMap<String, String> getUrlParams(String url) {
        ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
        url = url.replace("?", ";");
        if (!url.contains(";")) {
            return map;
        }
        if (url.split(";").length > 0) {
            String[] arr = url.split(";")[1].split("&");
            for (String s : arr) {
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key, value);
            }
            return map;

        } else {
            return map;
        }
    }

    private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg){
        saveConnectLog(ctx,userId,msg,MessageConstants.exceptionStatus.normal,null);
    }

    private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg,Integer status){
        saveConnectLog(ctx,userId,msg,status,null);
    }

    /**
     *
     * @param ctx
     * @param userId 用户id
     * @param msg 链接描述
     * @param status 链接状态
     */
    private void saveConnectLog(ChannelHandlerContext ctx,String userId,String msg,Integer status,String clientIP){
        if (userId==null){
            userId = ctx.channel().attr(WebsocketUtils.ATTRIBUTE_KEY_USER_ID).get();
        }
        if (clientIP==null){
            InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
            clientIP = insocket.getAddress().getHostAddress();
        }

        WebsocketConnectLog log = WebsocketConnectLog.builder()
                .host(clientIP)
                .msg(msg)
                .userId(userId)
                .status(status)
                .time(new Date())
                .build();

        logService.save(log);
    }

}

3、配置通道初始化

@NoArgsConstructor
@AllArgsConstructor
public class LcfcWebsocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    private WebsocketConfig config;

    private WebsocketConnectLogService logService;

    protected void initChannel(SocketChannel socketChannel) throws Exception {

        socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());
        socketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        socketChannel.pipeline().addLast("handler", new LcfcSebsocketChannelHandler(config,logService));
    }
}

4、启动websocket服务端

@Slf4j
@Component
public class LcfcWebsockerServerStart  implements ApplicationRunner {

    @Autowired
    WebsocketConfig config;

    @Autowired
    WebsocketConnectLogService logService;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        new Thread(()->{init();}).start();
//        init();
    }
    private void init(){
        log.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 LcfcWebsocketChannelInitializer(config,logService));
            Channel channel = bootstrap.bind(config.getPort()).sync().channel();
            log.info("webSocket服务器启动成功:"+channel);
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.info("运行出错:"+e);
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
            log.info("websocket服务器已关闭");
        }
    }

}

启动后如果看到 17:24:38.692 [Thread-32] INFO c.l.w.s.i.LcfcWebsockerServerStart - [init,41] - webSocket服务器启动成功:[id: 0x4dbe0472, L:/0:0:0:0:0:0:0:0:19999],说明启动成功

5、websocket客户端编写

5.1 编写通道处理

public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {

    private final WebSocketClientHandshaker handshaker;
    private ChannelPromise handshakeFuture;

    public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    public ChannelFuture handshakeFuture() {
        return handshakeFuture;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        handshakeFuture = ctx.newPromise();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        handshaker.handshake(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("WebSocket Client disconnected!");
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel ch = ctx.channel();
        if (!handshaker.isHandshakeComplete()) {
            try {
                handshaker.finishHandshake(ch, (FullHttpResponse) msg);
                System.out.println("WebSocket Client connected!");
                handshakeFuture.setSuccess();
            } catch (WebSocketHandshakeException e) {
                System.out.println("WebSocket Client failed to connect");
                handshakeFuture.setFailure(e);
            }
            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();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        if (!handshakeFuture.isDone()) {
            handshakeFuture.setFailure(cause);
        }
        ctx.close();
    }
}

5.2 启动客户端

public class WebsocketClient {
    static final String URL = System.getProperty("url", "ws://127.0.0.1:19999/websocket?userId=456");


    public static void main(String[] args) throws Exception {
        URI uri = new URI(URL);
        String scheme = uri.getScheme() == null ? "ws" : uri.getScheme();
        final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
        final int port;
        if (uri.getPort() == -1) {
            if ("ws".equalsIgnoreCase(scheme)) {
                port = 80;
            } else if ("wss".equalsIgnoreCase(scheme)) {
                port = 443;
            } else {
                port = -1;
            }
        } else {
            port = uri.getPort();
        }

        if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) {
            System.err.println("Only WS(S) is supported.");
            return;
        }

        final boolean ssl = "wss".equalsIgnoreCase(scheme);
        final SslContext sslCtx;
        if (ssl) {
            sslCtx = SslContextBuilder.forClient()
                    .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        } else {
            sslCtx = null;
        }

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
            // If you change it to V00, ping is not supported and remember to change
            // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
            final WebSocketClientHandler handler =
                    new WebSocketClientHandler(
                            WebSocketClientHandshakerFactory.newHandshaker(
                                    uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()));

            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            if (sslCtx != null) {
                                p.addLast(sslCtx.newHandler(ch.alloc(), host, port));
                            }
                            p.addLast(
                                    new HttpClientCodec(),
                                    new HttpObjectAggregator(8192),
                                    handler);
                        }
                    });

            Channel ch = b.connect(uri.getHost(), port).sync().channel();
            handler.handshakeFuture().sync();

            BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                String msg = console.readLine();
                if (msg == null) {
                    break;
                } else if ("bye".equals(msg.toLowerCase())) {
                    ch.writeAndFlush(new CloseWebSocketFrame());
                    ch.closeFuture().sync();
                    break;
                } else if ("ping".equals(msg.toLowerCase())) {
                    WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[]{8, 1, 8, 1}));
                    ch.writeAndFlush(frame);
                } else {
                    WebSocketFrame frame = new TextWebSocketFrame(msg);
                    ch.writeAndFlush(frame);
                }
            }
        } finally {
            group.shutdownGracefully();
        }
    }

}

help文档

config

@Data
@Configuration
@ConfigurationProperties(prefix = "websocket")
public class WebsocketConfig {
    private Integer port;

    private String remoteUrl;

    private String checkUrl;

}

// 配置文件
websocket:
  port: 19999 # 占用端口
  remote-url: 127.0.0.1 远程ip
  check-url: http://10.0.0.0:19200/api/auth/oauth/check_token 验证用户token地址

websocket 工具类

public class WebsocketUtils {
    public static final String KEY_USER_ID = "userId";
    public static final AttributeKey<String> ATTRIBUTE_KEY_USER_ID = AttributeKey.valueOf(KEY_USER_ID);
    private   static ChannelGroup GlobalGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    private  static ConcurrentMap<String, ChannelId> ChannelMap=new ConcurrentHashMap();

    public static void setChannelParam(Channel channel, String userId){
        channel.attr(ATTRIBUTE_KEY_USER_ID).setIfAbsent(userId);
    }

    public  static void addChannel(Channel channel){
        GlobalGroup.add(channel);
    }

    public  static void addChannel(Channel channel,String userId){
        setChannelParam(channel,userId);
        GlobalGroup.add(channel);
//        ChannelMap.put(userId,channel);
    }
    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 Boolean sendToOne(final String userId, TextWebSocketFrame tws){
        Boolean send = false;

        for (Iterator<Channel> it = GlobalGroup.stream().iterator(); it.hasNext(); ) {
            Channel x = it.next();
            String itemUserId = x.attr(ATTRIBUTE_KEY_USER_ID).get();
            if (userId.equals(itemUserId)){
                x.writeAndFlush(tws);
                send = true;
            }

        }
        return send;

    }
    public static void send2All(TextWebSocketFrame tws){
        GlobalGroup.writeAndFlush(tws);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

woniyu123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值