基於Netty的Android系統IM簡單實現原理

bluetooth發表於2021-09-09

基於Netty的Android系統IM簡單實現原理

基於Netty的Android系統IM簡單實現原理
最近在開發MobIM,實現了訊息傳輸和群等功能的IM功能。SDK功能包小,而功能全面。可以與原來的系統進行無縫整合。
自己抽空也實現了一套IM Server和IMClient的業務通訊模式。沒有實現複雜的UI介面,實現簡單的登入註冊,發訊息,收訊息。伺服器端與客戶端都使用Netty通訊。
Netty基於非阻塞(nio),事件驅動的網路應用程式框架和工具。
透過Netty面對大規模的併發請求可以處理的得心用手。用來替代原來的bio網路應用請求框架。

BIO通訊即平時使用的基於Socket,ServerSocket的InputStream和OutStream。
Netty神奇的地方在於是否是阻塞的。

while(true){//主執行緒死迴圈等待新連線到來
 Socket socket = serverSocket.accept();//為新的連線建立新的執行緒,客戶端與伺服器上的執行緒數1:1
 executor.submit(new ConnectIOnHandler(socket));

在BIO模型中,伺服器透過ServerSocket來開啟監聽,每當有請求的時候開啟一個執行緒來接受處理和維持狀態。這種思想在低併發,小吞吐的應用還可以應付,一旦遇到大併發,大吞吐的請求,必然歇菜。執行緒和客戶端保持著1:1的對應關係,維持著執行緒。維持那麼的多的執行緒,JVM必然不堪重負,伺服器必然崩潰,當機。
而在非阻塞的Netty中,卻可以應付自如。從容應對。Tomcat就是基於BIO的網路通訊模式(Tomcat可以透過一定配置,改成非阻塞模式),而JBoss卻是基於非阻塞的NIO實現。
NIO的網路通訊模式很強勁,但是上手卻一點都不容易。其中解決和牽扯到好多網路問題。如:網路延時,TCP的粘包/拆包,網路故障等一堆一堆的問題。而Netty呢,針對nio複雜的程式設計難題而進行一系列的封裝實現,提供給廣大開發者一套開源簡單,方便使用的API類庫,甚至青出於藍而勝於藍,甚至幾乎完美的解決CPU突然飆升到100%的bug : (其實也沒有真正的解決,只是把復現的機率降到了最低而已)。
用Netty來實現IM實在太合適了。可以在最短的時間裡整出一套思路清晰,架構簡明的IM通訊底層模型。提下需求,底層用JSON 字串String進行通訊,物件透過JSON序列化成JSON String。收到JSON資料後再反序列化成物件。
首先,我們先看伺服器是怎麼實現的。

private static final StringDecoder DECODER = new StringDecoder();    private static final StringEncoder ENCODER = new StringEncoder();
...         //boss執行緒監聽埠,worker執行緒負責資料讀寫
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();        //輔助啟動類
        ServerBootstrap bootstrap = new ServerBootstrap();        try {            //設定執行緒池
            bootstrap.group(bossGroup, workerGroup);            //設定socket工廠
        bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.handler(new LoggingHandler(LogLevel.INFO));            //設定管道工廠
            bootstrap.childHandler(new ChannelInitializer() {                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {                    //獲取管道
                    ChannelPipeline pipe = socketChannel.pipeline();                    
                    // Add the text line codec combination first,
                    pipe.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));                    // the encoder and decoder are static as these are sharable
                    //字串編碼器
                    pipe.addLast(DECODER);                    //字串解碼器
                    pipe.addLast(ENCODER);                    //業務處理類
                    pipe.addLast(new IMServerHandle());
                }
            });            //繫結埠
            // Bind and start to accept incoming connections.
            ChannelFuture f = bootstrap.bind(port).sync();            if (f.isSuccess()) {
                Log.debug("server start success... port: " + port + ", main work thread: "
                        + Thread.currentThread().getId());
            }            ////等待服務端監聽埠關閉
            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {            //優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

以上是Netty伺服器啟動的程式碼。其中需要注意childHandler方法。需要把我們要新增的業務處理handler來新增到這裡。透過ChannelPipeline 新增ChannelHandler。而處理字串的就在IMServerHandle裡實現。IMServerHandle繼承了SimpleChannelInboundHandler類。其中泛型T就是要轉換成的物件。客戶端與伺服器端通訊是本質上透過位元組碼byte[]通訊的,而透過StringDecoder 和StringEncoder工具類對byte[]進行轉換,在IMServerHandle中獲取到String進行處理即可。
看下IMServerHandle的實現方式。

/***
 * 面向IM通訊操作的業務類
 * @author xhj
 *
 */public class IMServerHandle extends SimpleChannelInboundHandler {    /**
     * user操作業務類
     */
    private UserBiz userBiz = new UserBiz();    /***
     * 訊息操作的業務類
     */
    private IMMessageBiz immessagebiz = new IMMessageBiz();    
    /***
     * 處理接受到的String型別的JSON資料
     */
    @Override    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(" get msg >> "+msg);        //把JSON資料進行反序列化
         Request req = JSON.parseObject(msg, Request.class);
         Response respon = new Response();
         respon.setSendTime(System.currentTimeMillis());         //判斷是否是合法的請求
         if(req != null ) {
             System.out.println("the req method >> "+req.getMethod());             //獲取操作型別
             if(req.getMethod() == IMProtocol.LOGIN) {                 //獲取要操作的物件
                 User user = JSON.parseObject(req.getBody(),User.class);                 //設定返回資料的操作型別
                 respon.setMethod(IMProtocol.LOGIN);                 //執行業務操作
                 boolean bl = userBiz.login(user);                 if(bl) {//檢驗使用者有效
                     //設定響應資料
                     respon.setBody("login ok");                     //設定狀態
                     respon.setStatus(0);                     //登入成功將連線channel儲存到Groups裡
                     ChannelGroups.add(ctx.channel());                     //將使用者的uname和channelId進行繫結,伺服器向指定使用者傳送訊息的時候需要用到uname和channelId
                     ChannelGroups.putUser(user.getUname(), ctx.channel().id());                     //傳送廣播通知某人登入成功了
                     userBiz.freshUserLoginStatus(user);
                 } else {//使用者密碼錯誤
                     //設定錯誤描述
                     respon.setErrorStr("pwd-error");                     //設定狀態描述碼
                     respon.setStatus(-1);
                 }                 //將Response序列化為json字串
                 msg = JSON.toJSONString(respon);                 //傳送josn字串資料,注意後面一定要加"rn"
                 ctx.writeAndFlush(msg+"rn");
             } else if(req.getMethod() == IMProtocol.SEND) {
                 IMMessage immsg = JSON.parseObject(req.getBody(), IMMessage.class);
                 immsg.setSendTime(System.currentTimeMillis());     c

透過IMServerHandle可以十分方便的處理獲取到的String字串。處理完後,可以直接透過ChannelHandlerContext的writeAndFlush方法傳送資料。
再看下Netty客戶端如何實現。

private BlockingQueue requests = new LinkedBlockingQueue();   /**
    * String字串解碼器
    */private static final StringDecoder DECODER = new StringDecoder();   /***
    * String字串編碼器
    */private static final StringEncoder ENCODER = new StringEncoder();   /**
    * 客戶端業務處理Handler
    */
   private IMClientHandler clientHandler ;   /**
    * 新增傳送請求Request
    * @param request
    */
   public void addRequest(Request request) {       try {
           requests.put(request);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }   /**
    * 是否繼續進行執行
    */
   private boolean run = true;   public void run() {       //遠端IP
       String host = "172.20.10.7";       //埠號
       int port = 10000;       //工作執行緒
       EventLoopGroup workerGroup = new NioEventLoopGroup();       try {           //輔助啟動類
           Bootstrap b = new Bootstrap(); // (1)
           //設定執行緒池
           b.group(workerGroup); // (2)
           //設定socket工廠 不是ServerSocket而是Socket
           b.channel(NioSocketChannel.class); // (3)
           b.handler(new LoggingHandler(LogLevel.INFO));           //設定管道工廠
           b.handler(new ChannelInitializer() {               public void initChannel(SocketChannel ch) throws Exception {
                   ChannelPipeline pipe = ch.pipeline();                   // Add the text line codec combination first,
                   pipe.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));                   // the encoder and decoder are static as these are sharable
                   //字串解碼器
                   pipe.addLast(DECODER);                   //字串編碼器
                   pipe.addLast(ENCODER);
                   clientHandler = new IMClientHandler();                   //IM業務處理類
                   pipe.addLast(clientHandler);
               }
           });           // Start the client.
           ChannelFuture f = b.connect(host, port).sync(); // (5)

           Channel ch = f.channel();
           ChannelFuture lastWriteFuture = null;           while(run) {               //將要傳送的Request轉化為JSON String型別
               String line = JSON.toJSONString(requests.take());               if(line != null && line.length() > 0) {//判斷非空
                   // Sends the received line to the server.
                   //傳送資料到伺服器
                   lastWriteFuture = ch.writeAndFlush(line + "rn");
               }
           }           // Wait until all messages are flushed before closing the channel.
           //關閉寫的埠
           if (lastWriteFuture != null) {
               lastWriteFuture.sync();
           }
       } catch(Exception ex){
           ex.printStackTrace();
       } finally {           //優雅的關閉工作執行緒
           workerGroup.shutdownGracefully();
       }
   }   /**
    * 增加訊息監聽接受介面
    * @param messgeReceivedListener
    */
   public void addMessgeReceivedListener(MessageSender.MessgeReceivedListener messgeReceivedListener) {
       clientHandler.addMessgeReceivedListener(messgeReceivedListener);
   }   /***
    *  移除訊息監聽介面
    * @param messgeReceivedListener
    */
   public void remove(MessageSender.MessgeReceivedListener messgeReceivedListener) {
       clientHandler.remove(messgeReceivedListener);
   }

Netty的client端實現和Server實現方式大同小異。比Server端要簡要些了。少一個NIOEventLoop。在Bootstrap 的handle方法中增加ChannelInitializer初始化監聽器,並增加了IMClientHandler的監聽操作。其中IMClientHandler具體處理伺服器返回的通訊資訊。
透過ChannelFuture 獲取Channel,透過Channel在一個迴圈裡傳送請求。如果訊息佇列BlockingQueue非空的時候,獲取Request併傳送。以上傳送,如何接受資料呢?接受到的json被反序列化直接變成了物件Response,對Response進行處理即可。
定義了一個訊息接受到的監聽介面。

public static interface MessgeReceivedListener {    public void onMessageReceived(Response msg);    public void onMessageDisconnect();    public void onMessageConnect();
}

在介面onMessageReceived方法裡直接對獲取成功的響應進行處理。

而伺服器端對某個客戶端進行傳送操作,把Channel新增到ChannelGroup裡,將uname和channelid對應起來。需要對某個使用者傳送訊息的時候透過uname獲取channelid,透過channelid從ChannelGroup裡獲取channel,透過channel傳送即可。
具體操作如下:

public void transformMessage(IMMessage message) {
        
        Channel channel = ChannelGroups.getChannel(ChannelGroups.getChannelId(message.getTo()));        if(channel != null && channel.isActive()) {
            Response response = new Response();
            response.setBody(JSON.toJSONString(message));
            response.setStatus(0);
            response.setMethod(IMProtocol.REV);
            response.setSendTime(System.currentTimeMillis());
            channel.writeAndFlush(JSON.toJSON(response)+"rn");
        }
        
    }

ChannelGroups的程式碼實現:

public class ChannelGroups {    private static final Map userList = new ConcurrentHashMap();    
    private static final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup("ChannelGroups",
            GlobalEventExecutor.INSTANCE);    public static void putUser(String uname,ChannelId id) {
        userList.put(uname,id);
    }

透過以上程式碼解析應該對IM的通訊模式有了比較全面的認識。具體實現過程可以下載原始碼進行檢視。歡迎大家反饋提出問題。

圖片描述
執行效果圖。

原文連結:http://www.apkbus.com/blog-942559-77186.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3402/viewspace-2812353/,如需轉載,請註明出處,否則將追究法律責任。

相關文章