实现思路概述

 

首先是客户端与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);
      }

}