实现思路概述
首先是客户端与Netty服务的连接
客户端与Netty服务的连接不必通过SpringMVC这一套流程,而是单独的与Netty服务建立连接,而netty服务类也不必纳入Spring容器的管理,并且Netty服务会设置自己的路由,客户端会根据服务器ip与路由来与其建立连接。连接建立后即可开始各项聊天功能的开展。
如果Netty服务需要调用Spring容器管理的Service,则需要单独写一个Spring工具来提供Service对象供Netty服务调用。
Netty处理客户端请求分四种情况,即四种连接类别:1->建立连接、2->聊天、3->签收消息、4->心跳监听
Netty主动推送的情况有一,即第五种连接类别:好友请求
建立连接流程
0.将Netty服务初始化完毕。
1.等待客户端连接(一般客户端登录后第一页就是chatList页,该页面负责客户端与Netty服务器的连接,该页面加载后会对Netty连接进行初始化,建立连接的类别即 action=1)。
2.收到客户端连接初始化请求后,建立连接,连接建立时会初始化channel,并把此channel与数据库内的用户ID进行关联(该关联维护在一个专门的类里边的一个map中UserChannelRel.put(senderId, currentChannel);)。
聊天流程
0.将Netty服务初始化完毕。
1.等待客户端发送请求,如果发送请求类型为chat,if(action == MsgActionEnum.CHAT.type) ,此时前端传来的数据为:
// 构建ChatMsg
var chatMsg = new app.ChatMsg(me.id, friendUserId, msg_text_val, null);
// 构建DataContent
var dataContent = new app.DataContent(app.CHAT, chatMsg, null);
// 调用websocket发送消息到netty后端
var wsWebview = plus.webview.getWebviewById("chatList.html");
wsWebview.evalJS("CHAT.chat('"+ JSON.stringify(dataContent) +"')");
即:
dataContent{
2,
chatMsg{
发送方ID,
接收方ID,
消息内容,
null
},
null
}
与后端模型匹配。
2.把聊天记录保存到数据库chatMsg表中,同时标记消息的签收状态[未签收]
3.从全局用户channel关系中获取接收方的channel(只要在线,每个用户都有id与channel的关联)Channel receiverChannel = UserChannelRel.get(receiverId);
if(receiverChannel == null){
channel为空代表用户离线,推送消息(JPush,个推, 小米推送)
}else{
当receiverChannel不为空,从ChannelGroup去查找对应的channel是否存在
Channel findChannel = users.find(receiverChannel.id());
if(findChannel != null) {
// 用户在线, 将消息发送给对方
receiverChannel.writeAndFlush(
new TextWebSocketFrame(JsonUtils.objectToJson(dataContentMsg)));
}else{
// 用户离线 TODO 推送消息
}
}
代码实现概述
主类
1.创建主从线程池EventLoopGrou且指定为NIO模式(与图1对应)
服务端处理客户端的连接及后续业务,全部由从线程池来做EventLoopGroup mainGroup = new NioEventLoopGroup();
2.并将两个线程池按顺序放入启动类ServerBootstrap server.group(mainGroup, subGroup);
3.添加从线程池处理器childHandler(new ...)
4.启动类绑定至相应端口上ServerBootstrap server.bind(8088);
从线程池处理类
0.先extends ChannelInitializer<SocketChannel>,重写initChannel方法
@Override
protected void initChannel(SocketChannel ch) throws Exception{}
1.先创建Pipeline(与图2对应)
ChannelPipeline pipeline = ch.pipeline();
2.针对业务添加所需处理器
pipeline.addLast(new HttpServerCodec());// websocket 基于http协议, 所以要有http编解码器
3.添加Websocket协议处理器,用于支持Websocket协议,并添加路由
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
4.添加自定义处理器
pipeline.addLast(new ChatHandler());
编写自定义处理器
0.先extends SimpleChannelInboundHandler<TextWebSocketFrame>,重写
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)throws Exception
1.创建用于记录和管理所有客户端channel的channel组
private static ChannelGroup users =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
1.1当客户端连接服务端之后(打开连接)->获取客户端的channel,并放入ChannelGroup中管理
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
users.add(ctx.channel());
}
1.2 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
users.remove(ctx.channel());
}
1.3 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
users.remove(ctx.channel());
}
2.根据具体业务添加代码
根据前端传来的不同消息类型执行不同的业务。
在此之前应准备两个模型类,用于封装消息
DataContent 和 ChatMsg
DataContent封装了消息类ChatMsg和消息类型、还有一个扩展字段。
ChatMsg封装了消息的各个变量,包括聊天内容、发送者ID、接收者ID、还有消息ID(用于签收)。
- 如果是websocket第一次open的时候,初始化channel,把用户的channel和userId关联
这很重要,无论是发送者和接收者,在第一次连接时都会触发这个关联, 用于其他方法获取用户channel。
- 如果是聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]。
然后发送消息,但在发送前要判断对方是否在线,
从上一步channel和userId的对应关系中获取到接收方的channel,
如果没有获取到,则证明对方不在线(没有执行过open类型的请求),推送消息(小米推送,个推)
如果获取到了,再从ChannelGroup中查找对方的channel是否在其中
如果没在则证明不在线,推送消息
如果上述验证通过了,则表明在线,将信息推送至客户端
package com;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import com.netty.WSServer;
@Component
public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null) {
try {
WSServer.getInstance().start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package com.netty;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class WSServer {
private static class SigletionWSServer{
static final WSServer instance = new WSServer();
}
public static WSServer getInstance() {
return SigletionWSServer.instance;
}
private EventLoopGroup mainGroup;
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;
public WSServer() {
mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();
server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class) //设置nio的双向通道
.childHandler(new WSServerInitializer()); //子处理器,用于处理workerGroup
}
public void start() {
this.future = server.bind(8088);
System.err.println("netty websocket server 启动完毕!");
}
}
package com.netty;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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.stream.ChunkedWriteHandler;
public class WSServerInitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// websocket 基于http协议, 所以要有http编解码器
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 对HttpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse
// 几乎在netty中的编程都会使用到此handler
pipeline.addLast(new HttpObjectAggregator(1024*64));
//===============以上用于支持http协议===========================
/**
* websocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws
* 此handler会处理一些繁重复杂的事情
* 握手: handshaking(close,ping,pong)
* 对于websocket,都是以frames进行传输的,不同的数据类型对应的frames也不同
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 自定义handler
pipeline.addLast(new ChatHandler());
}
}
package com.netty;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.SpringUtil;
import com.enums.MsgActionEnum;
import com.service.UserService;
import com.utils.JsonUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
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;
/**
*
* @author Edward
* 处理消息的handler
* TextWebSocketFrame:在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
// 用于记录和管理所有客户端的channel
private static ChannelGroup users =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
throws Exception {
// 获取客户端传输过来的消息
String content = msg.text();
Channel currentChannel = ctx.channel();
// 1. 获取客户端发来的消息
DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
Integer action = dataContent.getAction();
// 2. 判断消息的类型,根据不同类型来处理不同的业务
if(action == MsgActionEnum.CONNECT.type) {
//2.1 当websocket第一次open的时候,初始化channel,把用户的channel和userId关联
String senderId = dataContent.getChatMsg().getSenderId();
UserChannelRel.put(senderId, currentChannel);
}else if(action == MsgActionEnum.CHAT.type){
//2.2 聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
ChatMsg chatMsg = dataContent.getChatMsg();
String msgText = chatMsg.getMsg();
String receiverId = chatMsg.getReceiverId();
String senderId = chatMsg.getSenderId();
// 保存到数据库,并标记为未签收
UserService userService = (UserService)SpringUtil.getBean("userServiceImpl");
String msgId = userService.saveMsg(chatMsg);
chatMsg.setMsgId(msgId);
DataContent dataContentMsg = new DataContent();
dataContentMsg.setChatMsg(chatMsg);
// 发送消息
// 从全局用户channel关系中获取接收方的channel
Channel receiverChannel = UserChannelRel.get(receiverId);
if(receiverChannel == null) {
// TODO channel为空代表用户离线,推送消息(JPush,个推, 小米推送)
}else {
// 当receiverChannel不为空,从ChannelGroup去查找对应的channel是否存在
Channel findChannel = users.find(receiverChannel.id());
if(findChannel != null) {
// 用户在线
receiverChannel.writeAndFlush(
new TextWebSocketFrame(JsonUtils.objectToJson(dataContentMsg)));
}else {
// 用户离线 TODO 推送消息
}
}
}else if(action == MsgActionEnum.SIGNED.type){
//2.3 签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收]
UserService userService = (UserService)SpringUtil.getBean("userServiceImpl");
// 扩展字段在signed类型的消息中,代表需要被签收的消息id,逗号间隔
String msgIdsStr = dataContent.getExtend();
String msgIds[] = msgIdsStr.split(",");
List<String> msgIdList = new ArrayList<>();
for(String mid : msgIds) {
if(StringUtils.isNoneBlank(mid)) {
msgIdList.add(mid);
}
}
System.out.println(msgIdList.toString());
if(msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) {
// 批量签收
}
}else if(action == MsgActionEnum.KEEPALIVE.type){
//2.4 心跳类型的消息
}
}
/**
* 当客户端连接服务端之后(打开连接)
* 获取客户端的channel,并且放到ChannelGroup中进行管理
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
users.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
users.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
ctx.channel().close();
users.remove(ctx.channel());
}
}
package com;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* @Description: 提供手动获取被spring管理的bean对象
*/
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
// 获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
// 通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
// 通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
// 通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}