如果文章加载速度比较慢,可以访问我的个人博客:文章地址
1. Netty线程模型
2. Netty服务器搭建步骤
- 1)构建一对主从线程池
- 2)创建服务器启动类
- 3)为服务器启动类并绑定Channel、主从线程组
- 4)设置处理从线程池任务的助手类初始化器
- 5)监听启动和关闭服务器
设置Channel初始化器
每一个Channel都是由一个或多个handler共同组成的管道(pipeline)
可以把管道看成一个大的拦截器,而每个handler就可以看成是若干个小的拦截器,当请求过来的时候,可以对请求进行一层一层的拦截!
3. 入门案例 1
3.1 Netty服务器启动类
/**
* @Auther: csp1999
* @Date: 2020/09/21/22:33
* @Description: Netty服务器启动类:实现客户发送请求,服务器给予响应
*/
public class HelloNettyServer {
public static final int PORT = 8888;
public static void main(String[] args) throws InterruptedException {
// 1.创建一对主从线程池
// 主线程池(老板):用于接收客户端的请求连接,不做任何处理(大老板,只负责管理而不做具体劳动)
EventLoopGroup boosGroup = new NioEventLoopGroup();
// 从线程池(员工):主线程池会把任务全部丢给从线程池,让其去执行任务
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2.创建服务器启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3.为服务器启动类并绑定Channel、主从线程组
serverBootstrap.group(boosGroup, workerGroup)// 绑定主从线程组
.channel(NioServerSocketChannel.class)// 绑定channel:客户端与服务端通信的nio双向通道
// 4.设置处理从线程池任务的助手类初始化器
// 子处理器,用于处理从线程池交给的任务
.childHandler(new HelloNettyServerInitializer());
// 5.监听启动和关闭服务器
// 启动服务,并且设置端口号,同时启动方式为同步
ChannelFuture channelFuture = serverBootstrap.bind(PORT).sync();
// 监听关闭的channel,设置为同步方式
channelFuture.channel().closeFuture().sync();
} finally {
// 优雅关闭
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
3.2 Netty服务初始化器
/**
* @Auther: csp1999
* @Date: 2020/09/22/17:28
* @Description: Netty服务初始化器
* Netty服务器启动类绑定Channel,并在channel子处理器中绑定该初始化器后,就会执行该初始化器的初始化方法
*/
public class HelloNettyServerInitializer extends ChannelInitializer<SocketChannel> {
/**
* 继承ChannelInitializer<SocketChannel>后需要重写的 channel初始化器方法
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 通过SocketChannel 获得对应的pipeline管道
// 可以通过pipeline管道来为其添加handler
ChannelPipeline channelPipeline = socketChannel.pipeline();
/*
* 通过管道添加 handler
* HttpServerCodec 是由netty 自己提供的助手类,可以理解为拦截器
* 当请求到服务器时,我们需要解码,响应到客户端做编码
*/
// 添加一个handler ---> HttpServerCodec
channelPipeline.addLast("HttpServerCodec",new HttpServerCodec());
// 添加自定义助手类handler,给客户端浏览器渲染 hello netty~
channelPipeline.addLast("CustomHandler",new CustomHandler());
}
}
3.3 自定义助手类handler
/**
* @Auther: csp1999
* @Date: 2020/09/22/18:20
* @Description: 自定义助手类handler:用于给客户端浏览器渲染 hello netty~
* 需要继承SimpleChannelInboundHandler<HttpObject>抽象类,并重写相应方法!
*/
public class CustomHandler extends SimpleChannelInboundHandler<HttpObject> {
/**
* 从channel中数据读取和输出数据
*
* @param: ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @param: httpObject 消息的类型
* @return: void
* @create: 2020/9/22 18:25
* @author: csp1999
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject httpObject) throws Exception {
// 获取channel
Channel channel = ctx.channel();
// 在控制台打印访问请求的远程地址
System.out.println("远程地址:" + channel.remoteAddress());
// 定义要向客户端发送的内容
ByteBuf content = Unpooled.copiedBuffer("hello netty ~", CharsetUtil.UTF_8);
// 构建http response: 用于把content响应到客户端的浏览器页面上!
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
// 为响应设置数据类型和内容长度
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");// 为响应设置数据类型
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());// 为响应设置内容长度
// 把响应渲染到html页面(客户端浏览器页面)上
ctx.writeAndFlush(response);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 注册...");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 移除...");
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("CustomHandler 添加...");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("CustomHandler 移除...");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 活跃...");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 不活跃...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 数据读取完毕...");
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("用户事件触发...");
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel 可写更改...");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("channel 异常捕获...");
}
}
3.4 启动测试
- 运行Netty服务启动类
- 浏览器访问:http://localhost:8888/
- 得到如下结果:
查看控制台打印:
测试成功!
3.5 案例 1流程总结
4. 入门案例 2 Netty + WebSocket
WebSocket:
- 是一种持久化的协议
- 可以主动实时的反馈服务端信息给客户端
4.1 搭建服务器启动类
/**
* @Auther: csp1999
* @Date: 2020/09/23/9:51
* @Description: websocket + Netty 服务器启动类
*/
public class WebSocketServer {
private static final int PORT = 8088;
public static void main(String[] args) throws InterruptedException {
// 1.创建一对主从线程池
// 主线程池(老板):用于接收客户端的请求连接,不做任何处理(大老板,只负责管理而不做具体劳动)
EventLoopGroup mainGroup = new NioEventLoopGroup();
// 从线程池(员工):主线程池会把任务全部丢给从线程池,让其去执行任务
EventLoopGroup subGroup = new NioEventLoopGroup();
try {
// 2.创建服务器启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3.为服务器启动类并绑定Channel、主从线程组
serverBootstrap.group(mainGroup, subGroup)// 绑定主从线程组
.channel(NioServerSocketChannel.class)// 绑定channel:客户端与服务端通信的nio双向通道
// 4.设置处理从线程池任务的助手类初始化器
// 子处理器,用于处理从线程池交给的任务
.childHandler(new WebSocketServerInitializer());
// 5.监听启动和关闭服务器
// 启动服务,并且设置端口号,同时启动方式为同步
ChannelFuture channelFuture = serverBootstrap.bind(PORT).sync();
// 监听关闭的channel,设置为同步方式
channelFuture.channel().closeFuture().sync();
}finally {
// 优雅关闭线程池
mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
}
}
}
4.2 创建服务初始化器
/**
* @Auther: csp1999
* @Date: 2020/09/23/9:59
* @Description: WebSocket + Netty服务初始化器
* Netty服务器启动类绑定Channel,并在channel子处理器中绑定该初始化器后,就会执行该初始化器的初始化方法
*/
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
/**
* 继承ChannelInitializer<SocketChannel>后需要重写的 channel初始化器方法
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 通过SocketChannel 获取客户端与服务端建立的管道(pipeline)
// 可以通过pipeline管道来为其添加handler
ChannelPipeline channelPipeline = socketChannel.pipeline();
// 添加一个handler ---> HttpServerCodec: websocket基于http协议所需要的http编解码器
channelPipeline.addLast(new HttpServerCodec());
/*
* 在http 上有一些数据流产生(数据流有大有小)我们需要对其进行处理
* 因此 我们需要使用netty对该大数据流的读写提供支持
* 这个类是:ChunkedWriteHandler
*/
// 添加一个handler ---> ChunkedWriteHandler
channelPipeline.addLast(new ChunkedWriteHandler());
// 添加一个handler ---> HttpObjectAggregator: 对httpMessage进行聚合处理,聚合成response或request
// 设置的最大内容数据长度maxContentLength: 1024 * 64
channelPipeline.addLast(new HttpObjectAggregator(1024 * 64));
/*
* 基于http websocket 添加路由地址
* WebSocketServerProtocolHandler 会帮助处理一些繁重复杂的事情
* 比如处理握手动作的:handshaking(close、ping、ping):ping + ping = 心跳
* 对于websocket来说 都是以frames 进行传输的 不同的数据类型对应的frames也不同
*/
// 添加一个handler ---> WebSocketServerProtocolHandler:
// websocketPath: /ws 可以自定义
channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 添加一个handler ---> 自定义的handler:用于处理聊天消息
channelPipeline.addLast(new ChatHandler());
}
}
4.3 自定义消息处理助手类(handler)
/**
* @Auther: csp1999
* @Date: 2020/09/23/10:49
* @Description: 自定义助手类handler:用于处理聊天消息的handler
* 需要继承SimpleChannelInboundHandler<TextWebSocketFrame>抽象类,并重写相应方法!
* 由于传输数据的载体是 frame,这个 frame在 netty中是专门用于为websocket处理文本对象的,
* frame就是消息的载体,这个类名为:TextWebSocketFrame
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用于记录和管理所有客户端(Client)的管道组ChannelGroup
// 一个客户端(Client) 对应一个 Channel 通道~
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* (客户端创建完成就会触发该方法):
* 客户端与服务器端建立连接的时候就在管道组ChannelGroup 中添加该channel通道
*
* @param: ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @return: void
* @create: 2020/9/23 11:01
* @author: csp1999
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 获取channel
Channel channel = ctx.channel();
// 在通道管理组ChannelGroup中添加该channel
clients.add(channel);
}
/**
* (浏览器关闭(用户离开客户端)就会触发该方法):
* 客户端与服务器端断开连接的时候就从管道组ChannelGroup中移除该channel通道
*
* @param: ctx
* @return: void
* @create: 2020/9/23 11:01
* @author: csp1999
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 获取该客户端的channel
Channel channel = ctx.channel();
/*
* 从通道管理组ChannelGroup中移除该channel
*
* 如果用户把相应的浏览器关闭也会自动移除该channel通道,
* 所以即使不调用remove方法channel仍被移除,
*
* 因此:可以不写 clients.remove(channel); 这行代码
*/
clients.remove(channel);
System.out.println("客户端断开,channel 对应的长id为:" + ctx.channel().id().asLongText());
System.out.println("客户端断开,channel 对应的短id为:" + ctx.channel().id().asShortText());
}
/**
* 从channel中数据读取和输出数据
*
* @param ctx ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @param msg 客户端传输的消息:消息类型为TextWebSocketFrame
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 获取客户端所传输的消息
String content = msg.text();
System.out.println("服务器接收到的消息内容:" + content);
System.out.println("ip是:" + ctx.channel().remoteAddress());
// 将数据刷新到客户端上面
clients.writeAndFlush(
new TextWebSocketFrame(
"[服务器在: ]" + LocalDateTime.now()
+ "接收到消息,消息内容为:" + content
)
);
}
}
4.5 WebSocket 相关 API
// 1.初始化websocket, ws: 和后端netty指定的协议前缀保持一致
var socket = new WebSocket("ws//ip:[port]");
// 2.生命周期方法:
onopen()// 客户端和服务端建立连接时,会触发该方法,只触发一次
onmessage()// 每当服务端向客户端推送消息时,就会触发此方法
onerror()// 客户端和服务端连接出错时候该方法触发
onclose()// 客户端和服务端连接关闭时触发该方法
// 3.主动方法
Socket.send()// 客户端向服务端发送消息
Socket.close()// 客户端端口与服务端的连接
4.6 前端整合WebSocket向服务端发送消息
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Netty 实时通信</title>
</head>
<body>
<div id="app">
发送消息: <input type="text" id="msgContent" />
<button onclick="CHAT.chat()">消息发送</button>
<hr>
接收消息:
<div id="receiveMsg"></div>
</div>
</body>
</html>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.20.0/axios.js"></script>
<script>
window.CHAT = {
socket: null,
// 定义一个websocket初始化方法
init: function() {
// 判断浏览器是否支持websocket
if (window.WebSocket) {
// 1.如果支持,则创建websocket对象
CHAT.socket = new WebSocket("ws://localhost:8088/ws")
// 客户端和服务端建立连接时,会触发该方法,只触发一次
CHAT.socket.onopen = function() {
console.log("连接建立成功!")
};
// 客户端和服务端连接关闭时触发该方法
CHAT.socket.close = function() {
console.log("连接关闭!")
};
// 客户端和服务端连接出错时候该方法触发
CHAT.socket.onerror = function() {
console.log("连接发生异常!")
};
// 每当服务端向客户端推送消息时,就会触发此方法
CHAT.socket.onmessage = function(data) {
// data为服务端响应到客户端的数据
console.log("接收消息:" + data.data)
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
// 新接收的消息内容嵌入到页面
receiveMsg.innerHTML = html + "<br/>" + data.data;
}
} else {
console.log("您的浏览器不支持websocket 协议")
}
},
// 聊天发送方法:封装Socket.send()方法
chat: function() {
// 获取发送消息框中所输入的内容
var msgContent = document.getElementById("msgContent").value;
// 将客户端输入的消息内容发送给服务端
CHAT.socket.send(msgContent);
}
}
// 执行自定义的websocket初始化方法
CHAT.init()
</script>
4.7 案例2 启动测试
前端页面测试向后端发送:
后端Netty服务器接收到消息,查看控制台打印:
测试成功!
4.8 消息发送流程分析
对于WebSocket + Netty 服务器端的内部处理流程,在案例 1 流程总结里面分析过了!
5. 扩展:聊天APP实战——聊天功能部分的代码
5.1 前端代码
- 聊天页面chat.html:chat.html代码查看
- 聊天列表页面chatlist.html:chatlist.html代码查看
- 前端封装的对象以及逻辑方法app.js:app.js代码查看
5.2 后端代码
后端完整代码请参考:后端代码地址
消息相关实体类
- DataContent:消息体实体类(内部封装了UserChatMsg)
- UserChatMsg:消息内容实体类
WebSocketServer 服务器主启动类
/**
* @Auther: csp1999
* @Date: 2020/09/23/9:51
* @Description: websocket + Netty 服务器启动类(这里将其嵌入springboot,伴随springboot主函数启动)
*/
@Component
public class WebSocketServer {
/**
* WebSocketServer 服务器实例化方法
* (private)修饰,外部不能直接调用
*/
private static class SingletionWSServer {
static final WebSocketServer instance = new WebSocketServer();
}
/**
* 外部可以调用的WebSocketServer 服务器实例
* 在NettyBooter类中被调用,使其伴随springboot主函数启动的同时启动netty服务端
*
* (public)修饰,对外开放的方法
*
* @return
*/
public static WebSocketServer getInstance() {
return SingletionWSServer.instance;
}
/**
* 主线程池
*/
private EventLoopGroup mainGroup;
/**
* 从线程池
*/
private EventLoopGroup subGroup;
/**
* 服务器驱动类
*/
private ServerBootstrap serverBootstrap;
/**
* ChannelFuture
*/
private ChannelFuture future;
/**
* 端口
*/
private static final int PORT = 8088;
/**
* 服务器私网IP
*/
//private static final String HOST = "172.17.218.21";// 服务器内网IP
private static final String HOST = "192.168.1.6";// 本机IP
public WebSocketServer() {
// 主线程组:用于接收客户端的请求链接,不作任何处理
mainGroup = new NioEventLoopGroup();
// 从线程组:主线程组会把任务交给从线程组,让其做任务
subGroup = new NioEventLoopGroup();
// 定义服务器驱动类
serverBootstrap = new ServerBootstrap();
// 服务器驱动类绑定主从线程组
serverBootstrap.group(mainGroup, subGroup)
// 服务器驱动类绑定nio双向通道
.channel(NioServerSocketChannel.class)
// 服务器驱动类初始化子处理器(用于处理从线程池交给的任务)
.childHandler(new WebSocketServerInitialzer());
}
public void start() {
this.future = serverBootstrap.bind(PORT);// 服务器驱动类绑定端口和IP
//this.future = serverBootstrap.bind(HOST,PORT);// 服务器驱动类绑定端口和IP
if (future.isSuccess()) {
System.out.println("启动 Netty 成功...");
}
}
}
WebSocketServerInitialzer 服务初始化器
/**
* @Auther: csp1999
* @Date: 2020/09/23/9:59
* @Description: WebSocket + Netty服务初始化器
* Netty服务器启动类绑定Channel,并在channel子处理器中绑定该初始化器后,就会执行该初始化器的初始化方法
*/
public class WebSocketServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 获取客户端与服务端建立的管道(pipeline)
ChannelPipeline channelPipeline = socketChannel.pipeline();
// websocket 基于http 协议所需要的 http编解码器
channelPipeline.addLast(new HttpServerCodec());
/*
* 在http 上有一些数据流产生(数据流有大有小) 我们需要对其进行处理
* 因此 我们需要使用netty 对该大数据流的读写提供支持
* 这个类是:ChunkedWriteHandler
*/
channelPipeline.addLast(new ChunkedWriteHandler());
// 对httpMessage 进行聚合处理 , 聚合成 response 或者 request
channelPipeline.addLast(new HttpObjectAggregator(1024 * 64));
// ===========================增加心跳支持==============================
/*
* netty 自带的读写handler
* 针对客户端,如果在1分钟时间内没有向服务端发送读写心跳(ALL),则主动断开连接
* 如果有读空闲和写空闲,则不做任何处理
* 参数1:读的空闲时间 单位s
* 参数1:写的空闲时间 单位s
* 参数1:读写的空闲时间 单位s
*/
channelPipeline.addLast(new IdleStateHandler(8,16,32));
// ===================================================================
// 自定义的空闲状态监测的handler
channelPipeline.addLast(new HeartBeatHandler());
/*
* 基于http websocket 添加 路由地址
* WebSocketServerProtocolHandler 会帮助处理一些繁重复杂的事情
* 比如处理握手动作:handshaking(close、ping、ping):ping+ping=心跳
* 对于websocket来说 都是以frams 进行传输的 不同的数据类型对应的frams也不同
*/
channelPipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
// 自定义的handler
channelPipeline.addLast(new ChatHandler());
}
}
每个客户端的用户id对应一个Channel
/**
* @Auther: csp1999
* @Date: 2020/09/25/12:30
* @Description: 用户与Channel管道关系实体类 (用于将客户端获取的管道Channel和用户的userid关联)
*/
public class UserChannelRelation {
/**
* 使用hashmap集合来存储 ---> 用户id与Channel管道关系
* key: 用户id
* value: Channel管道
*/
private static HashMap<String, Channel> manage = new HashMap<>();
public static void put(String senderId, Channel channel) {
manage.put(senderId, channel);
}
public static Channel get(String senderId) {
return manage.get(senderId);
}
// 用于打印输出测试
public static void output() {
for (Map.Entry<String, Channel> entry : manage.entrySet()) {
System.out.println("UserId: " + entry.getKey()
+ ",ChannelId: " + entry.getValue().id().asLongText());
}
}
}
ChatHandler 自定义消息处理助手类
/**
* @Auther: csp1999
* @Date: 2020/09/23/10:49
* @Description: 自定义助手类handler:用于处理聊天消息的handler
* 需要继承SimpleChannelInboundHandler<TextWebSocketFrame>抽象类,并重写相应方法!
* 由于传输数据的载体是 frame,这个 frame在 netty中是专门用于为websocket处理文本对象的,
* frame就是消息的载体,这个类名为:TextWebSocketFrame
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 用于记录和管理所有客户端(Client)的管道组ChannelGroup
* 一个客户端(Client) 对应一个 Channel 通道~
*/
public static ChannelGroup userClients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 从channel中数据读取和输出数据
*
* @param ctx ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @param msg 客户端传输的消息:消息类型为TextWebSocketFrame
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 获取客户端所传输的消息
String content = msg.text();
// 1.获取客户端发来的消息,并将其传换成DataContent对象类型
// 方式一:
DataContent dataContent = JSON.parseObject(content, DataContent.class);
// 方式二:DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
Integer action = dataContent.getAction();// 消息行为
Channel channel = ctx.channel();// 客户端请求过来之后生成channel通道,通过ctx 上下文对象获取该通道
// 2.判断消息类型(行为),根据不同的类型来处理不同的业务
// MsgActionEnum.CONNECT.type == 1, "第一次(或重连)初始化连接" 行为
if (action.equals(MsgActionEnum.CONNECT.type)) {
// 2.1 当websocket 第一次open的时候 初始化channel,把channel 和发送者的userid关联起来,并存入UserChannelRelation
String senderId = dataContent.getUserChatMsg().getSenderId();
UserChannelRelation.put(senderId, channel);// hashMap集合
//UserChannelRelation.output();// 测试
} else if (action.equals(MsgActionEnum.CHAT.type)) {// 聊天消息
// MsgActionEnum.CONNECT.type == 2, "聊天消息" 行为
// 2.2收到聊天类型的消息 把聊天记录保存到数据库 同时标记消息的签收状态(未签收)
UserChatMsg userChatMsg = dataContent.getUserChatMsg();
String msgContent = userChatMsg.getMsg();
String senderId = userChatMsg.getSenderId();
String receiverId = userChatMsg.getReceiverId();
// 调用userService 保存消息到数据库 并且消息标记为未签收
// 注意:chatHandler 并未交由spring容器接管 因此无法使用注入方式获取userService
// 因此:这里采用SpringUtils 工具类手动注入
UserService userService = (UserService) SpringUtil.getBean("userServiceImpl");
String msgId = userService.saveMsg(userChatMsg);// 聊天消息保存数据库
userChatMsg.setMsgId(msgId);
DataContent dataContentMsg = new DataContent();
dataContentMsg.setUserChatMsg(userChatMsg);// 将用户聊天消息内容保存到完整消息内容实体类中
// 发送消息
// 从全局用户的channel关系中获取接收方的channel
Channel receiveChannel = UserChannelRelation.get(receiverId);
if (receiveChannel == null) {
// 离线用户
} else {
// 当 receiveChannel 不为空的时候 从channelGroup 中查找对应的channel是否存在
Channel findChannel = userClients.find(receiveChannel.id());
if (findChannel != null) {
// 用户在线
// 方式一: 将消息内容写入通道并刷新
JSONObject jsonObject = new JSONObject();
String jsonString = jsonObject.toJSONString(dataContentMsg);
receiveChannel.writeAndFlush(
new TextWebSocketFrame(
jsonString
//方式二:JsonUtils.objectToJson(dataContent)
)
);
} else {
// 离线用户
}
}
} else if (action.equals(MsgActionEnum.SIGNED.type)) {// 消息签收
// MsgActionEnum.CONNECT.type == 3, "消息签收" 行为
// 2.3签收消息类型,针对具体的消息进行签收 修改数据库中对应消息的签收状态(已签收)
UserService userService = (UserService) SpringUtil.getBean("userServiceImpl");
// 扩展字段在 signed 类型消息中 代表需要去签收的消息id 逗号间隔
String msgIdStr = dataContent.getExtend();
String[] msgsId = msgIdStr.split(",");
List<String> msgIdList = new ArrayList<>();
for (String msgId : msgsId) {
if (!StringUtils.isEmpty(msgId)) {
msgIdList.add(msgId);
}
}
// 控制台打印输出查看
msgIdList.forEach(msgId -> {
//System.out.println(msgId);
});
if (!msgIdList.isEmpty()) {
// 消息批量签收,更新标记
userService.updateMsgSigned(msgIdList);
}
} else if (action.equals(MsgActionEnum.KEEPALIVE.type)) {// 客户端保持心跳
// MsgActionEnum.CONNECT.type == 4, "客户端保持心跳" 行为
// 2.4收到心跳类型的消息
System.out.println("收到来自channel 为【" + channel + "】的心跳包");
}
}
/**
* (客户端创建完成就会触发该方法):
* 客户端与服务器端建立连接的时候就在管道组ChannelGroup 中添加该channel通道
*
* @param: ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @return: void
* @create: 2020/9/23 11:01
* @author: csp1999
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 获取channel
Channel channel = ctx.channel();
// 在通道管理组中添加 该channel
userClients.add(channel);// 将存在心跳链接的channel 放入userClients统一录和管理
}
/**
* (浏览器关闭(用户离开客户端)就会触发该方法):
* 客户端与服务器端断开连接的时候就从管道组ChannelGroup中移除该channel通道
*
* @param: ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @return: void
* @create: 2020/9/23 11:01
* @author: csp1999
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 获取channelId
String chanelId = ctx.channel().id().asShortText();
System.out.println("客户端被移除:channel id 为:" + chanelId);
// 从userClients移出channel
userClients.remove(ctx.channel());
/*
* 在通道管理组中移除 该channel
* 如果用户把相应的浏览器关闭 也会自动移除该channel通道,
* 所以即使不调用remove方法 channel仍被移除,
* 所以可以不写 clients.remove(channel); 这行代码
System.out.println("客户端断开,channel 对应的长id为:" + ctx.channel().id().asLongText());
System.out.println("客户端断开,channel 对应的短id为:" + ctx.channel().id().asShortText());
*/
}
/**
* 如果客户端与netty服务器连接发生异常,则将其从channelGroup 移除
*
* @param ctx 上下文:可以通过上下文获取channel,也可以通过上下文,将响应结果渲染回客户端
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 发生了异常后关闭连接,同时从channelGroup 移除
ctx.channel().close();
userClients.remove(ctx.channel());
}
}
让Netty + WebSocket服务器伴随SpringBoot 主函数一起启动
/**
* 在IOC的容器的启动过程,当所有的bean都已经处理完成之后,spring ioc容器会有一个发布事件的动作。
* 让我们的bean实现ApplicationListener接口,这样当发布事件时,[spring]的ioc容器就会以容器的实例对象作为事件源类,
* 并从中找到事件的监听者,此时ApplicationListener接口实例中的onApplicationEvent(E event)方法就会被调用,
*/
@Component
public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {
try {
WebSocketServer.getInstance().start();// 跟随springboot主函数启动的同时启动netty服务端
} catch (Exception e) {
e.printStackTrace();
}
}
}
}