java架構模式與設計模式(二)--Request-Response網絡交互通用處理模型

一、綜述

     java 客戶端與服務端交互過程中,採用NIO通訊是異步的,客戶端基本採用同一處理範式,來進行同異步的調用處理。

     處理模型有以下幾個要素:

         1. NIO發送消息後返回的Future

         2. 每次發送請求生成的Callback ,回調對象保存有請求數據,獲取數據時阻塞線程,服務端返回時喚醒被阻塞的業務線程 並返回數據操作

         3. 一個Map 保存有請求id 與 callback實例。 一般 key= reqId, value= callback

         4. 一個TimeChecker 超時檢測線程, 用戶循環檢測map裏面的請求是否超時,超時的數據之間刪除。

   以上4個要素基本構成了目前客戶端與服務端異步通訊時的處理模式。 目前dubbo、一些mq 框架都採用此模式,理解這個模式對閱讀源碼非常重要。

 

 二、 處理流程圖

 流程說明:

        1. 業務線程操作 

              1.1  通過NIO的channel ,write數據,同時返回 future

              1.2 將req與future 組成callback實例,放入reqMap

              1.3 調用callbak的get() 方法。此時線程會被阻塞一定時間,等待被喚醒(持有callback的鎖)。

      2.  發送消息後,開始監聽返回消息

            2.1 網絡消息received事件,會觸發listenser,根據reqId從reqMap裏面獲取callback實例,放入線程池執行

           2.2  callback實例的 回調方法,標識結果已返回,設置response, 調用notifyAll()方法,喚醒在1.3 被阻塞的線程,返回

     3. TimeoutCheckerThread線程

           timeoutCheckerThread 負責輪詢reqMap,將超時的數據從map裏面刪除。超時回調刪除後,1.3步驟 被阻塞的線程睡眠醒來,就會拋出超時異常

  以上幾個步驟與流程,就是目前通用的 client 異步操作模式, 3個獨立的線程+一個Map 完成整個操作。

  在dubbo、各類mq 生產端,都是如此,部分可能有所差異。例如 dubbo的超時檢測,用了HashedWheelTimer,比輪詢效率更高,但本質不變

 

 三、 實例代碼

       代碼實例在idea 中運行通過,依賴lombock插件, netty3組件,詳細代碼請看 git:https://github.com/xujianguo1/practise/ 下的nettydemo目錄。

      對於netty的具體使用不做過多解讀,畢竟netty4、5  與netty3 的差異太大。

      Invoker接口

package com.luguo.nettydemo.client.handler;

import com.luguo.nettydemo.model.RequestMsg;
import com.luguo.nettydemo.model.AckMsg;

public interface Invoker  {
    /**
     * 同步調用,直接返回消息
     */
    public AckMsg invokeSync(RequestMsg request) throws Exception;

    /**
     * 異步調用,返回future
     */
    public SimpleFuture invokeAsyc(RequestMsg requestMsg);

    /**
     *  收到消息返回時,調用。
     */
    public void invokeAck(AckMsg ackMsg);
}

 Invoker 接口默認實現 DefaultInvoker ,單例

package com.luguo.nettydemo.client.handler;

import com.luguo.nettydemo.model.AckMsg;
import com.luguo.nettydemo.model.RequestMsg;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Slf4j
public class DefaultInvoker implements  Invoker{
    private long defaultTimeout = 1000;
    private static final Invoker invoker = new DefaultInvoker();
    private AtomicLong sequencer = new AtomicLong(0);//id序列生成器
    private RequestMap reqMap = new RequestMap(); //存放請求Id與回調


    private DefaultInvoker(){
        Thread timeoutChecker =  new Thread(new TimeoutChecker());
        timeoutChecker.setName("Timeout-Checker");
        timeoutChecker.start(); //啓動超時檢查
    }

    public static Invoker getInstance(){
        return invoker;
    }

    public void invokeAck(AckMsg msg){
        Long reqId = msg.getRequestId();
        SimpleCallback callback = this.reqMap.getCallback(reqId);
        this.reqMap.remove(reqId);
        if(callback != null){
            callback.setAckMsg(msg);
            callback.run(); //喚醒等待的線程
        }

    }
    public void invokeCallback(RequestMsg request,SimpleCallback callback){
        //NettyClient
        SimpleNettyClient client = SimpleNettyClient.getClient("local");
        request.setRequestId(sequencer.addAndGet(1));
        request.setSendTime(System.currentTimeMillis());
        if(callback != null){
            reqMap.putData(request.getRequestId(),callback);
        }
        client.write(request,callback);

    }

    private SimpleFuture invokeFuture(RequestMsg request){
        CallbackFuture callbackFuture = new CallbackFuture();
        callbackFuture.setRequestMsg(request);
        invokeCallback(request,callbackFuture);
        return callbackFuture;
    }
    public AckMsg invokeSync(RequestMsg request) throws Exception {
       SimpleFuture future = invokeFuture(request);
       return future.get(defaultTimeout);
    }

    public SimpleFuture invokeAsyc(RequestMsg requestMsg) {
        SimpleFuture future=invokeFuture(requestMsg);
        return future;
    }

    /**
     * 超時檢測器
     */
    private class TimeoutChecker implements Runnable{
        public void run(){
            while(true){
                try{
                    long now = System.currentTimeMillis();
                    for(Long reqId:reqMap.requestMap.keySet()){
                        SimpleCallback callback = reqMap.getCallback(reqId);
                        if(callback.getRequestMsg().getSendTime() +defaultTimeout<now){//已經超時了
                            reqMap.remove(reqId); //刪除超時的數據
                            log.warn("remove Timeout key="+reqId);
                        }
                    }
                    Thread.sleep(1000);
                }catch (Exception e){
                    log.error(e.getMessage(),e);
                }
            }
        }
    }
    private class RequestMap{
        /**
         * requestMap  key=請求Id, value = 爲消息與處理
         */
        private Map<Long,SimpleCallback> requestMap =new ConcurrentHashMap<Long,SimpleCallback>();

        public SimpleCallback getCallback(Long requestId){
            return requestMap.get(requestId);
        }
        public void putData(Long requestId,SimpleCallback callback){
            requestMap.put(requestId,callback);
        }
        public void remove(Long requestId){
            requestMap.remove(requestId);
        }
    }

}
CallbackFuture 返回與回調對象
package com.luguo.nettydemo.client.handler;

import com.luguo.nettydemo.model.AckMsg;
import com.luguo.nettydemo.model.RequestMsg;
import lombok.extern.slf4j.Slf4j;
import org.jboss.netty.channel.ChannelFuture;

@Slf4j
public class CallbackFuture implements SimpleCallback,SimpleFuture {
    private RequestMsg reqMsg;
    private AckMsg ackMsg;
    private ChannelFuture future;
    private boolean isDone = false;
    public synchronized void run() { //回調方法被執行,表名已經完成了
        isDone = true;
        this.notifyAll();
    }
    public void setRequestMsg(RequestMsg msg){
        this.reqMsg = msg;
    }
    public RequestMsg getRequestMsg(){
        return reqMsg;
    }

    public void setAckMsg(AckMsg ack) {
        this.ackMsg = ack;
    }

    public SimpleFuture getFuture(ChannelFuture future) {
        this.future = future;
        return this;
    }

    public synchronized  AckMsg get(long timeout) throws InterruptedException{

         long sendTime = this.reqMsg.getSendTime();
         while(!isDone){
             long leftTime = timeout -(System.currentTimeMillis()-sendTime);
             if(leftTime <0){//拋出一個超時
                 throw new RuntimeException("Request timeout ! seqId:"+reqMsg.getRequestId());
             }else{
                 log.info(this.reqMsg.getRequestId()+"需要睡眠時間:"+leftTime);
                 this.wait(leftTime);
             }
         }
        return ackMsg;
    }

    public boolean isDone() {
        return false;
    }

}
ClientReceiveHandler: netty 的消息接收處理,會執行回調的run方法,喚醒等待的線程。
package com.luguo.nettydemo.client.handler;

import com.luguo.nettydemo.model.AckMsg;
import lombok.extern.slf4j.Slf4j;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Slf4j
public class ClientReceiveHandler extends SimpleChannelHandler {
    private static  Executor executor = Executors.newCachedThreadPool();
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        final AckMsg ackMsg = (AckMsg) e.getMessage();
        try {
            this.executor.execute(new Runnable() {
                @Override
                public void run() {  //在線程池中執行回調方法
                    DefaultInvoker.getInstance().invokeAck(ackMsg);
                }
            });
        } catch (Exception ex) {
            String msg = "ack callback execute fail \r\n";
            log.error(msg + ex.getMessage(), ex);
        }
    }
}
SimpleNettyClient :netty的客戶端啓動,初始化連接服務端線程池。 write方法,將req與callback 放入map
@Slf4j
public class SimpleNettyClient {
    private ClientBootstrap bootstrap;
    private ChannelPool channelPool;
    private static Map<String, SimpleNettyClient> clientMap= new ConcurrentHashMap<String, SimpleNettyClient>();
    public SimpleNettyClient(){
        bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory( Executors.newCachedThreadPool(),
                Executors.newCachedThreadPool()));
        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() throws Exception {
                ChannelPipeline pipeline= Channels.pipeline();
                pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(
                        Integer.MAX_VALUE, 0, 4, 0, 4));
                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                pipeline.addLast("jsonDecoder", new JsonDecoder(AckMsg.class));
                pipeline.addLast("jsonEncoder", new JsonEncoder(RequestMsg.class));
                pipeline.addLast("handler", new ClientReceiveHandler());
                return pipeline;
            }
        });
        channelPool = new ChannelPool(Constants.channelPoolSize);
    }

    private Channel connect(){
        ChannelFuture future = bootstrap.connect(new InetSocketAddress(Constants.host,
                Constants.port));
        // 等待連接創建成功
        if (future.awaitUninterruptibly(3000,
                TimeUnit.MILLISECONDS)) {
            if (future.isSuccess()) {
                log.info("Client is conneted to " + Constants.host + ":" + Constants.port);
            } else {
                log.warn("Client is not conneted to " + Constants.host + ":"
                        + Constants.port);
            }
        }
        return future.getChannel();
    }


    public static SimpleNettyClient getClient(String clientName){
        SimpleNettyClient client = clientMap.get(clientName);//這裏可以擴展,進行負載均衡算法選擇目標
        if(client==null){
            synchronized (clientMap){
                if( clientMap.get(clientName)==null){//二次檢查
                    client = new SimpleNettyClient();
                    clientMap.put(clientName,client);
                    return client;
                }
                return clientMap.get(clientName);
            }

        }else{
            return client;
        }
    }
    public SimpleFuture write(RequestMsg requestMsg, SimpleCallback callback){
        Channel channel = this.channelPool.get();
        if(channel==null){
            channel = connect();
        }
        ChannelFuture future = channel.write(requestMsg);
        this.channelPool.released(channel);
//        if(requestMsg.getMsgType() ==1){
//            future.addListener(new ChannelFutureListener(){
//                public void operationComplete(ChannelFuture channelFuture) throws Exception {
//                    if(channelFuture.isSuccess()){
//                        return;
//                    }else{
//                        //可以添加 寫異常的返回
//                    }
//                }
//            });
//        }
        if(callback != null){
            callback.setRequestMsg(requestMsg);
            return callback.getFuture(future);
        }
        return null;
    }


    private class ChannelPool {
        private ArrayBlockingQueue<Channel> channels;


        public ChannelPool(int poolSize) {
            this.channels = new ArrayBlockingQueue<Channel>(poolSize);
            for (int i = 0; i < poolSize; i++) {
                channels.add(connect());
            }
        }

        public Channel get(){
            try{
               return this.channels.take();
            }catch (Exception e){

            }
            return null;
        }

        /**
         * 同步釋放netty channel
         */
        public void released(Channel ch) {
            channels.add(ch);
        }
    }

}

以上即爲一個通用的客戶端處理流程。 啓動代碼與req,ack,encoder、decoder 相關代碼未貼出,在github上面下載即可。git:https://github.com/xujianguo1/practise/ 下的nettydemo目錄。

測試時,先執行StartServer,啓動服務端監聽程序, 然後啓動StartClientTest 客戶端程序。

 

四、總結

      網絡客戶端通訊,處理流程基本相同。

    各大開源框架都是在該模式下實現功能的擴充,理解該模式,能很好的閱讀各種存在客戶端與服務端通訊的開源代碼。

   例如:1. 接入註冊中心,實現server的發現,同時執行路由策略,實現負載均衡。

           2. 發送與接收時,執行統計功能。

          3. 在不關心返回 的場景(例如:mq的異步發送),上層會接入distruptor框架,提升發送性能

本文來自

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章