当前位置: 首页 > article >正文

netty编程之基于websocket实现聊天功能

写在前面

源码 。
本文看下netty如何通过websocket实现聊天功能。

类似于实现http server,netty实现websocket也很简单,同样使用对应的编码器和解码器就行了,相关的有HttpServerCodec,HttpObjectAggregator,ChunkedWriteHandler。

1:编码

1.1: netty websocket server

server类:

package com.dahuyou.netty.chat.domain.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

@Component("nettyServer")
public class NettyServer {

    private Logger logger = LoggerFactory.getLogger(NettyServer.class);

    //配置服务端NIO线程组
    private final EventLoopGroup parentGroup = new NioEventLoopGroup(); //NioEventLoopGroup extends MultithreadEventLoopGroup Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
    private final EventLoopGroup childGroup = new NioEventLoopGroup();
    private Channel channel;

    public ChannelFuture bing(InetSocketAddress address) {
        ChannelFuture channelFuture = null;
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(parentGroup, childGroup)
                    .channel(NioServerSocketChannel.class)    //非阻塞模式
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childHandler(new MyChannelInitializer());

            channelFuture = b.bind(address).syncUninterruptibly();
            channel = channelFuture.channel();
        } catch (Exception e) {
            logger.error(e.getMessage());
        } finally {
            if (null != channelFuture && channelFuture.isSuccess()) {
                logger.info("netty server start done. {}");
            } else {
                logger.error("netty server start error. {}");
            }
        }
        return channelFuture;
    }

    public void destroy() {
        if (null == channel) return;
        channel.close();
        parentGroup.shutdownGracefully();
        childGroup.shutdownGracefully();
    }

    public Channel getChannel() {
        return channel;
    }

}

MyChannelInitializer:

package com.dahuyou.netty.chat.domain.server;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;

import java.nio.charset.Charset;

public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) {
        channel.pipeline().addLast("http-codec", new HttpServerCodec());
        channel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        channel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        // 在管道中添加我们自己的接收数据实现方法
        channel.pipeline().addLast(new MyServerHandler());
    }

}

其中设置了支持websocket相关的编解码器是关键。
MyServerHandler:

package com.dahuyou.netty.chat.domain.server;

import com.alibaba.fastjson.JSON;
import com.dahuyou.netty.chat.pojo.ClientMsgProtocol;
import com.dahuyou.netty.chat.types.util.ChannelHandler;
import com.dahuyou.netty.chat.types.util.MsgUtil;
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.ChannelInboundHandlerAdapter;
import io.netty.channel.socket.SocketChannel;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyServerHandler extends ChannelInboundHandlerAdapter {

    private Logger logger = LoggerFactory.getLogger(MyServerHandler.class);

    private WebSocketServerHandshaker handshaker;

    /**
     * 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel channel = (SocketChannel) ctx.channel();
        logger.info("链接报告开始");
        logger.info("链接报告信息:有一客户端链接到本服务端");
        logger.info("链接报告IP:{}", channel.localAddress().getHostString());
        logger.info("链接报告Port:{}", channel.localAddress().getPort());
        logger.info("链接报告完毕");
        ChannelHandler.channelGroup.add(ctx.channel());
    }

    /**
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("客户端断开链接{}", ctx.channel().localAddress().toString());
        ChannelHandler.channelGroup.remove(ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //http
        if (msg instanceof FullHttpRequest) {

            FullHttpRequest httpRequest = (FullHttpRequest) msg;

            if (!httpRequest.decoderResult().isSuccess()) {

                DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);

                // 返回应答给客户端
                if (httpResponse.status().code() != 200) {
                    ByteBuf buf = Unpooled.copiedBuffer(httpResponse.status().toString(), CharsetUtil.UTF_8);
                    httpResponse.content().writeBytes(buf);
                    buf.release();
                }

                // 如果是非Keep-Alive,关闭连接
                ChannelFuture f = ctx.channel().writeAndFlush(httpResponse);
                if (httpResponse.status().code() != 200) {
                    f.addListener(ChannelFutureListener.CLOSE);
                }

                return;
            }

            WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws:/" + ctx.channel() + "/websocket", null, false);
            handshaker = wsFactory.newHandshaker(httpRequest);

            if (null == handshaker) {
                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
            } else {
                handshaker.handshake(ctx.channel(), httpRequest);
            }

            return;
        }

        //ws
        if (msg instanceof WebSocketFrame) {

            WebSocketFrame webSocketFrame = (WebSocketFrame) msg;

            //关闭请求
            if (webSocketFrame instanceof CloseWebSocketFrame) {
                handshaker.close(ctx.channel(), (CloseWebSocketFrame) webSocketFrame.retain());
                return;
            }

            //ping请求
            if (webSocketFrame instanceof PingWebSocketFrame) {
                ctx.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
                return;
            }

            //只支持文本格式,不支持二进制消息
            if (!(webSocketFrame instanceof TextWebSocketFrame)) {
                throw new Exception("仅支持文本格式");
            }

            String request = ((TextWebSocketFrame) webSocketFrame).text();
            System.out.println("服务端收到:" + request);

            ClientMsgProtocol clientMsgProtocol = JSON.parseObject(request, ClientMsgProtocol.class);
            //1请求个人信息
            if (1 == clientMsgProtocol.getType()) {
                ctx.channel().writeAndFlush(MsgUtil.buildMsgOwner(ctx.channel().id().toString()));
                return;
            }
            //群发消息
            if (2 == clientMsgProtocol.getType()) {
                TextWebSocketFrame textWebSocketFrame = MsgUtil.buildMsgAll(ctx.channel().id().toString(), clientMsgProtocol.getMsgInfo());
                ChannelHandler.channelGroup.writeAndFlush(textWebSocketFrame);
            }

        }

    }

    /**
     * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        logger.info("异常信息:\r\n" + cause.getMessage());
    }

}

对解码后的websocket消息按照不同的消息类型做了不同的处理,工作中如果有类似需求的话,这里也会是我们写代码的主要地方咯。

1.2: 通过spring boot启动netty websocket server

package com.dahuyou.netty.chat.trigger;

import com.dahuyou.netty.chat.domain.server.NettyServer;
import io.netty.channel.ChannelFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import java.net.InetSocketAddress;

@SpringBootApplication
@ComponentScan("com.dahuyou.netty")
public class NettyApplication implements CommandLineRunner {

    @Value("${netty.host}")
    private String host;
    @Value("${netty.port}")
    private int port;
    @Autowired
    private NettyServer nettyServer;

    public static void main(String[] args) {
        SpringApplication.run(NettyApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        InetSocketAddress address = new InetSocketAddress(host, port);
        ChannelFuture channelFuture = nettyServer.bing(address);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> nettyServer.destroy()));
        channelFuture.channel().closeFuture().syncUninterruptibly();
    }

}

1.3: websocket client

<!--
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-->
<title>大忽悠</title>

<script type="text/javascript" src="res/js/jquery.min.js"></script>
<script type="text/javascript" src="res/js/jquery.serialize-object.min.js"></script>
<script type="text/javascript" src="res/js/index.js"></script>

<style>
body{
	background-image:url(res/img/1121416_071010Daniel12.jpg);
	background-repeat:no-repeat;
	background-size:cover;
	font-family:”微软雅黑”;
}


#chatDiv{
	position: relative;
	margin:0 auto;
	margin-top:150px;
	width:839px;
	height:667px;
	background-color:#CCCCCC;
	border-radius:3px;-moz-border-radius:3px;
}

</style>

</head>

<body>

<div id="chatDiv">
	<!-- left -->
	<div style="width:60px; height:667px; background-color:#2D2B2A; float:left;">
		<!-- 头像 -->
		<div style="width:35px; height:35px; margin:0 auto; margin-top:19px; margin-left:12px; float:left; border:1px solid #666666;border-radius:3px;-moz-border-radius:3px;">
			<img src="res/img/Snipaste_2024-09-02_14-29-46.png" width="35px" height="35px"/>
		</div>

		<!-- 聊天 -->
		<div style="width:28px; height:28px; margin:0 auto; margin-top:25px; margin-left:16px; float:left;">
			<img src="res/img/chat.png" width="28px" height="28px"/>
		</div>

		<!-- 好友 -->
		<div style="width:28px; height:28px; margin:0 auto; margin-top:20px; margin-left:16px; float:left;">
			<img src="res/img/friend.png" width="28px" height="28px"/>
		</div>

		<!-- 收藏 -->
		<div style="width:28px; height:28px; margin:0 auto; margin-top:20px; margin-left:16px; float:left;">
			<img src="res/img/collection.png" width="28px" height="28px"/>
		</div>

		<!-- 设置 -->
		<div style="width:20px; height:20px; margin:0 auto; margin-left:20px; float:left; position:absolute;bottom:0; margin-bottom:12px;">
			<img src="res/img/set.png" width="20px" height="20px"/>
		</div>

	</div>

	<!-- center -->
	<div style="width:250px; height:667px; background-color:#EBE9E8; float:left;">
		<div style=" background-image:url(res/img/search.png); background-repeat:no-repeat;margin:0 auto; margin-top:25px; padding-top:5px; padding-bottom:5px; width:210px; background-color:#DBD9D8;border-radius:3px;-moz-border-radius:3px; float:left; margin-left:20px; font-size:12px; color:#333333;text-indent:27px;">
			找呀找呀找朋友!!!
		</div>

		<!-- friendList -->
		<div id="friendList" style="float:left; margin-top:5px;">
			<div style="width:250px; height:65px;">
				<table style="width:240px; height:60px; margin:0 auto; margin-top:2px; background-color:#E5E5E5;">
					<tr>
						<td rowspan="2" width="50"><img src="res/img/2487997a9f274d86a61a8dffff3e0070_wang0907.jpg" width="50px" height="50px" style="border-radius:3px;-moz-border-radius:3px;"/></td>
						<td style="color:#333333; text-indent:5px; font-size:12px; vertical-align:bottom; font-weight:bolder;">大忽悠</td>
					</tr>
					<tr>
						<td style="color:#999999; text-indent:5px; font-size:10px;">你好,这里是大忽悠有限责任公司</td>
					</tr>
				</table>
			</div>
		</div>

	</div>


	<!-- chat -->
	<div id="chat" style="width:529px; height:667px; background-color:#F5F5F5; float:right;">
		<div style="width:16px; height:16px; background-image:url(res/img/exit.png); background-repeat:no-repeat; float:right; margin-top:10px; margin-right:30px;"></div>
		<div style="width:16px; height:16px; background-image:url(res/img/min.png); background-repeat:no-repeat; float:right; margin-top:10px; margin-right:12px;"></div>
		<div style="border-bottom:1px #E7E7E7 solid;width:509px; padding-top:0px; padding-left:20px; padding-bottom:10px; font-size:18px; font-weight:bolder;float:left;">
			大忽悠(6666666)
		</div>

		<!-- 会话区域 begin -->
		<div id="show" style="width:529px; height:450px; float:left;overflow-y:scroll;">

			<!-- 消息块;系统 -->
			<div class="msgBlockSystem" style="margin-left:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:left;">
				<div class="msgBlock_userHeadImg" style="float:left; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
					<img class="point" src="res/img/Snipaste_2024-09-02_14-29-46.png" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
				</div>

				<div class="msgBlock_channelId" style="float:left; width:100px; margin-top:-5px; margin-left:10px; padding-bottom:2px; font-size:10px;">
					大忽悠
				</div>

				<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
					<div style="width:4px; height:20px; background-color:#CC0000; float:left;border-radius:3px;-moz-border-radius:3px;"></div>
					<div class="point" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
						这里是大忽悠有限责任公司,你可别被我忽悠了<hr/><img width="260" height="260" src="res/img/2487997a9f274d86a61a8dffff3e0070_wang0907.jpg" />
					</div>
				</div>
			</div>

			<!-- 消息块;好友 -->
			<div class="msgBlockFriendClone" style=" display:none; margin-left:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:left;">
				<div class="msgBlock_userHeadImg" style="float:left; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
					<img class="headPoint" src="res/img/head5.jpg" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
				</div>

				<div class="msgBlock_channelId" style="float:left; width:100px; margin-top:-5px; margin-left:10px; padding-bottom:2px; font-size:10px;">
					<!-- 名称 -->
				</div>

				<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
					<div style="width:4px; height:20px; background-color:#CC0000; float:left;border-radius:3px;-moz-border-radius:3px;"></div>
					<div class="msgPoint" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
						<!-- 信息 -->
					</div>
				</div>
			</div>

			<!-- 消息块;自己 -->
			<div class="msgBlockOwnerClone" style=" display:none; margin-right:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:right;">

				<div style="float:right; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
					<img class="headPoint" src="res/img/head3.jpg" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
				</div>

				<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
					<div class="msgPoint" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
						<!-- 信息 -->
					</div>
					<div style="width:4px; height:20px; background-color:#CC0000; float:right;border-radius:3px;-moz-border-radius:3px;"></div>
				</div>

			</div>

			<span id="msgPoint"></span>

		</div>
		<!-- 会话区域 end -->
		<div style="width:100%; height:2px; float:left; background-color:#CCCCCC;"></div>
		<div style="margin:0 auto; width:100%; height:149px; margin-top:5px;  background-color:#FFFFFF; float:left;">
			<textarea id="sendBox" style="font-size:14px; border:0; width:499px; height:80px; outline:none; padding:15px;font-family:”微软雅黑”;resize: none;"></textarea>

			<div style="margin-top:20px; float:right; margin-right:35px; padding:5px; padding-left:15px; padding-right:15px; font-size:12px; background-color:#F5F5F5;border-radius:3px;-moz-border-radius:3px; cursor:pointer;" onclick="javascript:util.send();">发送(S)</div>
		</div>
	</div>

</div>


</body>
</html>

2:测试

运行:
在这里插入图片描述
在这里插入图片描述
发送一个消息:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
酱!!!

写在后面

参考文章列表

idea中的html到浏览器中的乱码问题解决方案 。


http://www.kler.cn/news/294380.html

相关文章:

  • 【SRC】某次众测绕过限制注册用户+敏感信息泄露漏洞
  • 鸿蒙双向认证
  • 贷款利率高低跟什么有关?仅凭身份证就能贷到款?额度是多少?
  • SCSS darken函数
  • Socket编程---TCP篇
  • Kotlin高阶函数与Lambda表达式及内联函数的介绍
  • 深度学习速通系列:推荐五个提高机器学习模型鲁棒性和稳定性的开源工具或框架
  • 打靶记录16——Momentum
  • 周末总结(2024/09/07)
  • springboot+vue+mybatis计算机毕业设计智慧篮球馆预约+PPT+论文+讲解+售后
  • html 单页面路由模式hash和history
  • Shell脚本基本语法(Linux篇)
  • MapSet之二叉搜索树
  • 1-7 掩膜的运用 opencv树莓派4B 入门系列笔记
  • 力扣239题详解:滑动窗口最大值的多种解法与模拟面试问答
  • GNN会议期刊汇总(人工智能、机器学习、深度学习、数据挖掘)
  • kubernetes--配置与存储(ConfigMap、加密数据配置Secret、SubPath、热更新、Volumes、NFS挂载、PV与PVC)
  • C#基础(5)交错数组*
  • 【Rust光年纪】Rust 机器人学库全景:功能、安装与API概览
  • 多线程篇(阻塞队列- BlockingQueue)(持续更新迭代)
  • 不同vlan之间的通信方法
  • 微信小程序仿微信聊天界面
  • 【spring】does not have member field ‘com.sun.tools.javac.tree.JCTree qualid
  • 【网络安全】密码学概述
  • 『功能项目』更换URP场景【32】
  • 【BUUCTF】HardSQL
  • 交换两个变量数值的3种方法
  • 创建Hive表后,查看表结构发现中文注释乱码
  • 【C++模版初阶】——我与C++的不解之缘(七)
  • Maven使用指南的笔记