性能與Netty并列的Apache NIO HttpServer庫使用詳解

apache java庫家族有一個用NIO實現(xiàn)的http server麸粮,性能跟netty并列半哟,而且更加容易使用酬滤。

這個庫依賴以下幾個jar包,其中有幾個是必須的寓涨,有幾個則在特定功能下才用的到
httpcore-4.4.3.jar
httpcore-nio-4.4.3.jar
這兩個庫是必須的盯串,是http server運行的基礎(chǔ)

httpclient-4.5.1.jar
這個庫不是必須的,但是其中有一些工具類封裝著一些常用解析http請求數(shù)據(jù)的功能缅茉,能提高生產(chǎn)力

commons-fileupload-1.4.jar
javax.servlet-api-3.1.0.jar
這兩個庫在處理上傳文件的時候要用到嘴脾,如果服務(wù)器沒有處理上傳文件請求男摧,可以不導(dǎo)入蔬墩。

以上jar文件帶的版本號可以忽略,可以下載最新版本的使用

下面講解具體的實現(xiàn)方法

HttpProcessor httpproc = HttpProcessorBuilder.create()
        .add(new ResponseDate())
        .add(new ResponseServer("apache nio http server"))
        .add(new ResponseContent())
        .add(new ResponseConnControl())
        .build();

UriHttpAsyncRequestHandlerMapper reqistry = new UriHttpAsyncRequestHandlerMapper();

reqistry.register("/test_get", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest data, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        httpExchange.getResponse().setEntity(new NStringEntity("hello world"));
        httpExchange.submitResponse();
    }
});


HttpAsyncService protocolHandler = new HttpAsyncService(httpproc, reqistry);
NHttpConnectionFactory<DefaultNHttpServerConnection> connFactory = new DefaultNHttpServerConnectionFactory(
        ConnectionConfig.DEFAULT);

IOEventDispatch ioEventDispatch = new DefaultHttpServerIODispatch(protocolHandler, connFactory);
IOReactorConfig config = IOReactorConfig.custom()
        .setIoThreadCount(2)
        .setSoTimeout(5000)
        .setConnectTimeout(5000)
        .build();
try {
    ListeningIOReactor ioReactor = new DefaultListeningIOReactor(config);
    ioReactor.listen(new InetSocketAddress("127.0.0.1", 8088));
    ioReactor.execute(ioEventDispatch);
} catch ( IOException e ) {
    e.printStackTrace();
}

上面的代碼即啟動的了http server耗拓,在瀏覽器中輸入
http://localhost:8088/test_get
就能輸出hello world

上面的代碼有幾部分需要用戶手動配置

HttpProcessor

HttpProcessor httpproc = HttpProcessorBuilder.create()
        .add(new ResponseDate())
        .add(new ResponseServer("apache nio http server"))
        .add(new ResponseContent())
        .add(new ResponseConnControl())
        .build();

這部分用來配置每個請求的響應(yīng)信息

Connection: keep-alive
Content-Length:1024
Date: Thu, 24 Sep 2020 09:37:34 GMT
Server: http-core-nio

你也可以根據(jù)自己的需求自定義實現(xiàn)拇颅,繼承HttpResponseInterceptor類即可

UriHttpAsyncRequestHandlerMapper

UriHttpAsyncRequestHandlerMapper reqistry = new UriHttpAsyncRequestHandlerMapper();

reqistry.register("/test_get", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest data, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        httpExchange.getResponse().setEntity(new NStringEntity("hello world"));
        httpExchange.submitResponse();
    }
});

這部分是最重要的,用于映射url和對應(yīng)的處理程序乔询,它并不難理解樟插,按照這個模板套用即可。

IOReactorConfig

IOReactorConfig config = IOReactorConfig.custom()
        .setIoThreadCount(2)
        .setSoTimeout(5000)
        .setConnectTimeout(5000)
        .build();

這一部分用于設(shè)置服務(wù)器的核心參數(shù)竿刁,它可以設(shè)置的參數(shù)相當(dāng)?shù)亩嗷拼福渲袕膽?yīng)用的角度出發(fā)setIoThreadCount是比較重要的,用于設(shè)置http server處理請求的線程數(shù)量食拜。實際上鸵熟,這個值設(shè)置成1或者2就夠了,也就是用1到2條線程處理網(wǎng)絡(luò)請求负甸,因為使用非阻塞的NIO機制流强,所以即使單線程也能處理成千上萬的請求痹届,但是這里有一個前提條件,在請求對應(yīng)的處理程序中打月,不能直接處理業(yè)務(wù)邏輯队腐,而應(yīng)該將業(yè)務(wù)邏輯提交給另外的線程池,否則一旦某個業(yè)務(wù)邏輯阻塞奏篙,將影響到整個服務(wù)器的運行柴淘。

比如我們可以這樣做

ExecutorService executorService = Executors.newFixedThreadPool(10);

reqistry.register("/test_get", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest data, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        executorService.execute(()->{
            try {
                httpExchange.getResponse().setEntity(new NStringEntity("hello world"));
                httpExchange.submitResponse();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        });
    }
});

注意,在線程池的任務(wù)中不能直接使用HttpRequest對象秘通,否則會用并發(fā)問題悠就,如果要解析HttpRequest中的參數(shù),請在線程池外完成充易。

此外梗脾,當(dāng)請求處理完畢,必須調(diào)用
httpExchange.submitResponse()
否則請求將一直處于等待狀態(tài)無法完成盹靴。

以上是服務(wù)器的基礎(chǔ)用法炸茧,也就是
httpcore-4.4.3.jar
httpcore-nio-4.4.3.jar
兩個庫中的功能。

下面如何解析http請求的參數(shù)以及處理上傳文件

處理查詢字符串

http://localhost:8088/test_get?a=1&b=2

如果我們通過查詢字符串傳遞參數(shù)給服務(wù)器稿静,服務(wù)器必須要解析這兩個參數(shù)

reqistry.register("/test_get", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest request, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        String strUrl = request.getRequestLine().getUri();
        String[] urlItems = strUrl.split("\\?");
        String queryString = "";
        if( urlItems.length >= 2) {
            queryString = urlItems[1];
        }
        //url后面的查詢字符串鍵值對
        List<NameValuePair> queryStringInfo = URLEncodedUtils.parse(queryString,Charset.forName("utf8"));
        System.out.println(queryStringInfo);
        httpExchange.submitResponse();
    }
});

因為這個庫并沒有對http消息進行深度封裝梭冠,我們只能獲得請求的url,然后自己解析字符串改备,所幸控漠,httpclient-4.5.1.jar 庫提供了工具方法幫助我們實現(xiàn)解析

List<NameValuePair> queryStringInfo = 
    URLEncodedUtils.parse(queryString,Charset.forName("utf8"));

這句代碼就是將
a=1&b=2

這樣的查詢字符串轉(zhuǎn)換成鍵值對列表,方便我們通過程序訪問悬钳。我們也可以將NameValuePair列表抓轉(zhuǎn)換成Map

Map<String,String> queryStringMap = queryStringInfo.stream()
        .collect(Collectors.toMap(NameValuePair::getName,NameValuePair::getValue));

處理post請求

reqistry.register("/test_post", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest request, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        if( request instanceof BasicHttpEntityEnclosingRequest) {
            BasicHttpEntityEnclosingRequest entityEnclosingRequest = (BasicHttpEntityEnclosingRequest)request;
            HttpEntity httpEntity = entityEnclosingRequest.getEntity();
            String postData = EntityUtils.toString(httpEntity);
            System.out.println(postData);
        }
        httpExchange.submitResponse();
    }
});

處理post請求的方法和處理get的稍有不同

 String postData = EntityUtils.toString(httpEntity)

直到這里獲得了post提交上來的數(shù)據(jù)盐捷,如果數(shù)據(jù)是json字符串,則可以通過json庫直接使用默勾。如果是x-www-form-urlencoded之類的鍵值對字符串碉渡,則可以跟處理get請求參數(shù)一樣處理,轉(zhuǎn)換成NameValuePair列表

List<NameValuePair> postInfo = 
    URLEncodedUtils.parse(postData,Charset.forName("utf8"));

處理上傳文件

處理上傳文件需要用到這兩個庫
commons-fileupload-1.4.jar
javax.servlet-api-3.1.0.jar

首先需要實現(xiàn)一個繼承自RequestContext的類型

public class FileUploadRequestContext implements RequestContext {
    HttpEntity httpEntity;

    public FileUploadRequestContext(HttpEntity httpEntity) {
        this.httpEntity = httpEntity;
    }

    @Override
    public String getCharacterEncoding() {text
        return "utf8";
    }

    @Override
    public String getContentType() {
        return httpEntity.getContentType().getValue();
    }

    @Override
    public int getContentLength() {
        return (int)httpEntity.getContentLength();
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return httpEntity.getContent();
    }
}

然后以如下方式使用

reqistry.register("/test_upload_file", new HttpAsyncRequestHandler<HttpRequest>(){
    @Override
    public HttpAsyncRequestConsumer<HttpRequest> processRequest(HttpRequest request, HttpContext context) throws HttpException, IOException {
        return new BasicAsyncRequestConsumer();
    }

    @Override
    public void handle(HttpRequest request, HttpAsyncExchange httpExchange, HttpContext context) throws HttpException, IOException {
        if( request instanceof BasicHttpEntityEnclosingRequest) {
            BasicHttpEntityEnclosingRequest entityEnclosingRequest = (BasicHttpEntityEnclosingRequest)request;
            HttpEntity httpEntity = entityEnclosingRequest.getEntity();
            DiskFileItemFactory factory = new DiskFileItemFactory();
            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setFileSizeMax(1024 * 1024 * 1024);
            try {
                List<FileItem>  fileItems = upload.parseRequest(new FileUploadRequestContext(httpEntity));
                for(FileItem fileItem : fileItems) {
                    //普通數(shù)據(jù)字段
                    if( fileItem.isFormField()) {
                        String key = fileItem.getFieldName();
                        String value = fileItem.getString();
                    } else {
                        //文件字段
                        try(  FileOutputStream file = new FileOutputStream("pic.jpg") ) {
                            file.write(fileItem.get());
                            file.flush();
                        }
                    }
                }
            } catch (FileUploadException e) {
                e.printStackTrace();
            }
        }
        httpExchange.submitResponse();
    }
});

其中

List<FileItem>  fileItems = 
    upload.parseRequest(new FileUploadRequestContext(httpEntity));

這句代碼將 httpEntity 轉(zhuǎn)換成 FileItem 列表母剥,F(xiàn)ileItem有可能是普通的post數(shù)據(jù)字段滞诺,也可能是文件字段,我們可以通過
fileItem.isFormField()
來判別环疼,如果值為true則表示普通數(shù)據(jù)字段习霹,否則是文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炫隶,一起剝皮案震驚了整個濱河市淋叶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌等限,老刑警劉巖爸吮,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芬膝,死亡現(xiàn)場離奇詭異,居然都是意外死亡形娇,警方通過查閱死者的電腦和手機锰霜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桐早,“玉大人癣缅,你說我怎么就攤上這事『逶停” “怎么了友存?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長陶衅。 經(jīng)常有香客問我屡立,道長,這世上最難降的妖魔是什么搀军? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任膨俐,我火速辦了婚禮,結(jié)果婚禮上罩句,老公的妹妹穿的比我還像新娘焚刺。我一直安慰自己,他們只是感情好门烂,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布乳愉。 她就那樣靜靜地躺著,像睡著了一般屯远。 火紅的嫁衣襯著肌膚如雪蔓姚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天氓润,我揣著相機與錄音赂乐,去河邊找鬼薯鳍。 笑死咖气,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挖滤。 我是一名探鬼主播崩溪,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼斩松!你這毒婦竟也來了伶唯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤惧盹,失蹤者是張志新(化名)和其女友劉穎乳幸,沒想到半個月后瞪讼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡粹断,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年符欠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓶埋。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡希柿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出养筒,到底是詐尸還是另有隱情曾撤,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布晕粪,位于F島的核電站挤悉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏巫湘。R本人自食惡果不足惜尖啡,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剩膘。 院中可真熱鬧衅斩,春花似錦、人聲如沸怠褐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奈懒。三九已至奠涌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間磷杏,已是汗流浹背溜畅。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留极祸,地道東北人慈格。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像遥金,于是被迫代替她去往敵國和親浴捆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348