1. 程式人生 > >Netty的restful API 簡單實現和部署

Netty的restful API 簡單實現和部署

1 BEGIN

Netty 是一個基於NIO的客戶,伺服器端程式設計框架,使用Netty 可以確保你快速和簡單的開發出一個網路應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網路應用的程式設計開發過程,例如,TCP和UDP的socket服務開發。

Netty是一個非同步服務端網路程式設計框架,使用netty可以快速開發出需要的服務。目前有不少公司使用netty開發遊戲伺服器。Netty的高效性吸引了不少開發者的注意。

由於Netty不是一個專門的web/Restful伺服器框架,所有使用Netty開發Restful服務需要自己新增一些額外的模組。這裡簡單實現一份restful 服務,用於計算身體質量指數和基礎代謝率(Basal Metabolic Rate,簡稱BMR),並部署到VPS上。

BMI指數(身體質量指數,簡稱體質指數又稱體重指數,英文為Body Mass Index,簡稱BMI。

基礎代謝率(Basal Metabolic Rate,簡稱BMR)是指:我們在安靜狀態下(通常為靜臥狀態)消耗的最低熱量,人的其他活動都建立在這個基礎上。

2 啟動 ServerBootstrap(建立連線)

  1. 到netty 的官方網站下載netty的jar http://netty.io/,這裡使用的netty版本是4.1.6。下載的壓縮包解壓後,在/jar/all-in-one 下有nettyall-4.1.6.Final.jar 檔案,這個就是netty編譯出來的jar。

  2. 使用Eclipse新建一個工程,在工程中新建一個lib的資料夾,把上面的jar放入資料夾中,並通過Build Path把jar 加入到工程中。這個專案使用了orgJson作為json的解析庫,同樣的把org-json-20160810.jar加入工程中(org-json-Jar下載地址)。
    這裡寫圖片描述

  3. 新建一個Java類MainServer,加入 ServerBootstrap的啟動程式碼。這部分程式碼源自Netty 的Http Example,所有的Netty 服務啟動程式碼和這類似。


package com.health;
import io.netty.bootstrap.ServerBootstrap
; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; /** * 服務的主入口 * @author superzhan * */ public final class MainServer { /*是否使用https協議*/ static final boolean SSL = System.getProperty("ssl") != null; static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "6789")); public static void main(String[] args) throws Exception { // Configure SSL. final SslContext sslCtx; if (SSL) { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); } else { sslCtx = null; } // Configure the server. EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ServerInitializer(sslCtx)); Channel ch = b.bind(PORT).sync().channel(); System.err.println("Open your web browser and navigate to " + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }

3 ChannelInitializer(初始化連線)

在工程中新建一個class ServerInitializer,用於連線的初始化。

package com.health;

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.ssl.SslContext;

public class ServerInitializer extends ChannelInitializer<SocketChannel> {

    private final SslContext sslCtx;

    public ServerInitializer(SslContext sslCtx) {
        this.sslCtx = sslCtx;
    }

    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        if (sslCtx != null) {
            p.addLast(sslCtx.newHandler(ch.alloc()));
        }
        p.addLast(new HttpServerCodec());/*HTTP 服務的解碼器*/
        p.addLast(new HttpObjectAggregator(2048));/*HTTP 訊息的合併處理*/
        p.addLast(new HealthServerHandler()); /*自己寫的伺服器邏輯處理*/
    }
}

4 ChannelHandler(業務控制器)

以上兩份程式碼是固定功能的框架程式碼,業務控制器Handler才是自有發揮的部分。

  1. 需要獲取客戶端的請求uri做路由分發,不同的請求做不同的響應。
  2. 把客戶端的請求資料解析成Json物件,方便做運算。
  3. 把計算好的結果生成一個Json 資料發回客戶端。

package com.health;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;



import org.json.JSONObject;

public class HealthServerHandler extends ChannelInboundHandlerAdapter {

    private static final AsciiString CONTENT_TYPE = new AsciiString("Content-Type");
    private static final AsciiString CONTENT_LENGTH = new AsciiString("Content-Length");
    private static final AsciiString CONNECTION = new AsciiString("Connection");
    private static final AsciiString KEEP_ALIVE = new AsciiString("keep-alive");

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {

        if (msg instanceof FullHttpRequest) {
            FullHttpRequest req = (FullHttpRequest) msg;//客戶端的請求物件
            JSONObject responseJson = new JSONObject();//新建一個返回訊息的Json物件

            //把客戶端的請求資料格式化為Json物件
            JSONObject requestJson = null;
            try{
               requestJson = new JSONObject(parseJosnRequest(req));
            }catch(Exception e)
            {
                ResponseJson(ctx,req,new String("error json"));
                return;
            }

            String uri = req.uri();//獲取客戶端的URL

            //根據不同的請求API做不同的處理(路由分發),只處理POST方法
            if (req.method() == HttpMethod.POST) {
                if(req.uri().equals("/bmi"))
                { 
                    //計算體重質量指數
                    double height =0.01* requestJson.getDouble("height");
                    double weight =requestJson.getDouble("weight");
                    double bmi =weight/(height*height);
                    bmi =((int)(bmi*100))/100.0;
                    responseJson.put("bmi", bmi +"");

                }else if(req.uri().equals("/bmr"))
                {
                    //計算基礎代謝率
                    boolean isBoy = requestJson.getBoolean("isBoy");
                    double height = requestJson.getDouble("height");
                    double weight = requestJson.getDouble("weight");
                    int age = requestJson.getInt("age");
                    double bmr=0;
                    if(isBoy)
                    {
                        //66 + ( 13.7 x 體重kg ) + ( 5 x 身高cm ) - ( 6.8 x 年齡years )
                        bmr = 66+(13.7*weight) +(5*height) -(6.8*age);

                    }else
                    {
                        //655 + ( 9.6 x 體重kg ) + ( 1.8 x 身高cm ) - ( 4.7 x 年齡years )
                        bmr =655 +(9.6*weight) +1.8*height -4.7*age;
                    }

                    bmr =((int)(bmr*100))/100.0;
                    responseJson.put("bmr", bmr+"");
                }else {
                    //錯誤處理
                    responseJson.put("error", "404 Not Find");
                }

            } else {
                //錯誤處理
                responseJson.put("error", "404 Not Find");
            }

            //向客戶端傳送結果
            ResponseJson(ctx,req,responseJson.toString());
        }
    }

    /**
     * 響應HTTP的請求
     * @param ctx
     * @param req
     * @param jsonStr
     */
    private void ResponseJson(ChannelHandlerContext ctx, FullHttpRequest req ,String jsonStr)
    {

        boolean keepAlive = HttpUtil.isKeepAlive(req);
        byte[] jsonByteByte = jsonStr.getBytes();
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(jsonByteByte));
        response.headers().set(CONTENT_TYPE, "text/json");
        response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

        if (!keepAlive) {
            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            response.headers().set(CONNECTION, KEEP_ALIVE);
            ctx.write(response);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 獲取請求的內容
     * @param request
     * @return
     */
    private String parseJosnRequest(FullHttpRequest request) {
        ByteBuf jsonBuf = request.content();
        String jsonStr = jsonBuf.toString(CharsetUtil.UTF_8);
        return jsonStr;
    }
}

5 執行測試

再Eclipse 中直接Run as Java Application,就可以開啟Netty的服務。Netty 的服務不需要放到任何容器中,可以單獨執行。

這裡使用google 的PostMan測試。
這裡寫圖片描述

這裡寫圖片描述

6 匯出jar

netty服務向外釋出的時候不能只在Eclipse 中Run as Application。 所以釋出服務的時候需要匯出可執行的jar,然後執行這個jar檔案。

在Eclipse中右擊當前工程->Export->在Export視窗中選擇Java選項下的Runnable Jar File ->Next -> 在視窗中選擇匯出的檔案路徑和啟動的入口類 -> Finish。

開啟命令列終端,切換到當前目錄,執行 java -jar Server.jar ,便可以啟動服務了(Server.jar是匯出來的jar)。可以通過PostMan來測試。

7 把程式碼部署在VPS上

實際使用的Restful服務需要部署在某臺伺服器上。在這裡我把Server.jar 部署在搬瓦工VPS上。VPS安裝的系統是Centos 6 x86 minimal。系統本身沒有安裝JDK,需用通過yum 命令安裝openjdk。

  1. yum list java* 列出所有的openjdk版本。這裡通過yum 安裝jdk1.8 yum install java-1.8.0-openjdk.i686, 通過java -version 命令檢視openjdk版本。

  2. Server.jar 可以通過sftp上傳的vps伺服器上,這裡使用Cyberduck這款mac系統下的軟體。或者可以通過Linux 的SCP 命令把Jar上傳到vps 伺服器上,具體操作可以參照http://blog.csdn.net/marujunyy/article/details/8809481

  3. Linux 的命令列視窗實際上的單任務模式的,如果直接執行jar,關掉視窗之後,jar也會自動關掉,不會長期駐留伺服器上。 這時候需要用到 Screen 這個多視窗管理軟體(可通過yum安裝)。

  4. screen -S HealthServer 新開一個視窗,執行 java -jar Server.jar 執行服務,這樣服務就可以作為一個獨立的任務執行單獨執行。可以通過快捷鍵ctl+a+d切換回主視窗。

  5. screen -ls命令可以列出當前所有的視窗任務。screen -r HealthServer命令切換到服務執行的視窗。

8 The End

最終的介面可以通過PostMan來做測試。

9 參考資料