【Lolttery】專案開發日誌 (一) 微服務框架搭建

晦若晨曦發表於2017-12-14

公司最新的專案Lolttery已經開始動工了。

因為微服務很火,之前專門研究了一陣子。決定在新專案中採用微服務結構。在此部落格開始記錄學習和開發的問題和進步。

採用Netty+Spring+mybatis的核心框架,內部通訊使用socket tcp通訊。協議為json。同時用Spring MVC做對外的http介面。資料庫採用Mysql+Redis。

唉……反正說來說去伺服器+web端都是我自己一個人的活,用什麼技術完全不用討論啊……

個人經驗也不是很豐富,本系列部落格作為學習日誌和踩坑筆記,歡迎各路大神拍磚指正。


##第一篇:實現netty服務 框架的骨架是netty服務,netty是優秀的非同步網路處理框架,通過各種Handle可以適應不同的網路協議。同時又不依賴於tomcat等中介軟體,是實現微服務的合適選擇。

實現netty服務基本照搬了官網的原始碼。

在啟動器中包含了spring的初始化,netty的啟動和服務的註冊。

/**
 * 啟動器
 * Created by shizhida on 16/3/15.
 */

public class Bootstrap {

    private int port = 2333;

    public static String serverName = "";

    private Logger logger = LoggerFactory.getLogger(Bootstrap.class);

    public Bootstrap(int port){
        //在初始化啟動器時獲取spring的上下文。
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        //將上下文加入到一個全域性的變數中便於使用
        Application.setApplicationContext(context);
        this.port = port;
    }

    /**
     * 在redis中註冊本服務,以便被客戶端獲取
     * @param host
     * @param serverName
     * @return
     */
    public Bootstrap register(String host,String serverName){
        RedisDao redisDao = new RedisDao("localhost");
        Map<String,String> info = new HashMap<>();
        info.put("ip",host);
        info.put("port",port+"");
        this.serverName = serverName;
        redisDao.getJedis().hmset(serverName,info);
        return this;
    }

    /**
     * netty啟動
     * @throws Exception
     */
    public void run() throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(
                                    new JsonDecoder(),
                                    new DispatchHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();

            //儲存至全域性變數,便於關閉服務
            Application.future = f;

            logger.info("start service bin port " + port);

            f.channel().closeFuture().sync();

        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        try {
            new Bootstrap(2333).register("locahost","server").run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

完成啟動器之後,是協議的解析

為了避免出現粘包、半包等情況,協議採用4byte報文長度+json字串的方式進行傳輸。

json包括header、body和foot三個部分,header包含了serviceName,serviceCode等請求資訊,body為請求的引數,foot包含了來源、校驗碼等相關資訊

自行編寫了解析器如下,json解析使用阿里的fastjson庫

	
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //若可讀取長度不足4位元組,不進行讀取
        if(in.readableBytes()<4)
        {
            logger.debug("長度不足,可讀取長度為:" + in.readableBytes());
            return;
        }
        byte[] byte_length = new byte[4];
 
        in.readBytes(byte_length);
        //讀取報文長度
        int length = BUtil.bytes2int(byte_length,0);
        //若可讀取長度小於約定長度,則不讀取
        if(in.readableBytes()<length)
        {
            logger.debug("可讀取長度小於約定長度,約定:"+length+" 可讀取"+in.readableBytes());
            in.resetReaderIndex();
            return;
        }
        logger.debug("約定讀取資料長度:" + length);
        byte[] data = new byte[length];
 
        in.readBytes(data);
 
        String json  = new String(data);
        logger.debug("讀取到的字元資料:"+new String(data));
 
        JSONObject object = JSON.parseObject(json);
        //組裝request
        XORequest request = new XORequest(object);
 
        out.add(request);
    }
複製程式碼

然後是服務的分發。

一個服務中也可以細分為多個業務。設計上使用serviceName和serviceCode來確定一個具體的業務。serviceName用於服務註冊,可以獲取到服務的ip和埠資訊。serviceCode用於服務內部的業務具體劃分。

利用spring框架的功能,服務分發可以做的很簡單: netty處理:


    public void channelRead(ChannelHandlerContext ctx, Object msg) { 
        //分發並處理請求
        XORequest request = (XORequest) msg;
        logger.info("-------------request start ------------");
        logger.info("request to "+request.getServiceName());
        logger.info("request at "+new Date(request.getRequestDate()));
        logger.info("request for "+request.getServiceCode());
        XOResponse result = dispatcher.dispatch(request);

        //組裝處理結果
        byte[] json = result.toString().getBytes();
        ByteBuf byteBuf = ctx.alloc().buffer(json.length);
        byteBuf.writeBytes(json);

        //傳送給客戶端
        final ChannelFuture f = ctx.writeAndFlush(byteBuf); 
                f.addListener(new ChannelFutureListener() {
                        public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
                logger.info("request process done");
            }
        });
    }

複製程式碼

分發服務:(dispatcher)

public XOResponse dispatch(XORequest request) { String service_code = request.getServiceCode(); XOService service = Application.applicationContext.getBean(service_code,XOService.class); return service.run(request); }


預設所有的服務都實現XOService介面,利用spring的Service("serviceCode")註解就可以簡單的實現服務的分發。

通過兩層分發器、通用協議和服務介面。在這裡就實現了業務邏輯與框架功能的高度分離。

實現業務邏輯只需要新增更多的XOService介面的例項,就可以擴充套件業務邏輯。

 

在每個服務上依賴此框架,實現一個Bootstrap啟動器,並新增Service等業務邏輯程式碼即可完成一個新的服務。
複製程式碼

相關文章