性能与Netty并列的Apache NIO HttpServer库使用详解

apache java库家族有一个用NIO实现的http server,性能跟netty并列,而且更加容易使用。

这个库依赖以下几个jar包,其中有几个是必须的,有几个则在特定功能下才用的到
httpcore-4.4.3.jar
httpcore-nio-4.4.3.jar
这两个库是必须的,是http server运行的基础

httpclient-4.5.1.jar
这个库不是必须的,但是其中有一些工具类封装着一些常用解析http请求数据的功能,能提高生产力

commons-fileupload-1.4.jar
javax.servlet-api-3.1.0.jar
这两个库在处理上传文件的时候要用到,如果服务器没有处理上传文件请求,可以不导入。

以上jar文件带的版本号可以忽略,可以下载最新版本的使用

下面讲解具体的实现方法

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();

这部分用来配置每个请求的响应信息

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

你也可以根据自己的需求自定义实现,继承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和对应的处理程序,它并不难理解,按照这个模板套用即可。

IOReactorConfig

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

这一部分用于设置服务器的核心参数,它可以设置的参数相当的多,其中从应用的角度出发setIoThreadCount是比较重要的,用于设置http server处理请求的线程数量。实际上,这个值设置成1或者2就够了,也就是用1到2条线程处理网络请求,因为使用非阻塞的NIO机制,所以即使单线程也能处理成千上万的请求,但是这里有一个前提条件,在请求对应的处理程序中,不能直接处理业务逻辑,而应该将业务逻辑提交给另外的线程池,否则一旦某个业务逻辑阻塞,将影响到整个服务器的运行。

比如我们可以这样做

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();
            }
        });
    }
});

注意,在线程池的任务中不能直接使用HttpRequest对象,否则会用并发问题,如果要解析HttpRequest中的参数,请在线程池外完成。

此外,当请求处理完毕,必须调用
httpExchange.submitResponse()
否则请求将一直处于等待状态无法完成。

以上是服务器的基础用法,也就是
httpcore-4.4.3.jar
httpcore-nio-4.4.3.jar
两个库中的功能。

下面如何解析http请求的参数以及处理上传文件

处理查询字符串

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

如果我们通过查询字符串传递参数给服务器,服务器必须要解析这两个参数

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 库提供了工具方法帮助我们实现解析

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

这句代码就是将
a=1&b=2

这样的查询字符串转换成键值对列表,方便我们通过程序访问。我们也可以将NameValuePair列表抓转换成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提交上来的数据,如果数据是json字符串,则可以通过json库直接使用。如果是x-www-form-urlencoded之类的键值对字符串,则可以跟处理get请求参数一样处理,转换成NameValuePair列表

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

处理上传文件

处理上传文件需要用到这两个库
commons-fileupload-1.4.jar
javax.servlet-api-3.1.0.jar

首先需要实现一个继承自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) {
                    //普通数据字段
                    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 转换成 FileItem 列表,FileItem有可能是普通的post数据字段,也可能是文件字段,我们可以通过
fileItem.isFormField()
来判别,如果值为true则表示普通数据字段,否则是文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343