性能与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(){
    @Override
    public HttpAsyncRequestConsumer 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 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(){
    @Override
    public HttpAsyncRequestConsumer 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(){
    @Override
    public HttpAsyncRequestConsumer 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(){
    @Override
    public HttpAsyncRequestConsumer 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 queryStringInfo = URLEncodedUtils.parse(queryString,Charset.forName("utf8"));
        System.out.println(queryStringInfo);
        httpExchange.submitResponse();
    }
});

因为这个库并没有对http消息进行深度封装,我们只能获得请求的url,然后自己解析字符串,所幸,httpclient-4.5.1.jar 库提供了工具方法帮助我们实现解析

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

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

这样的查询字符串转换成键值对列表,方便我们通过程序访问。我们也可以将NameValuePair列表抓转换成Map

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

处理post请求

reqistry.register("/test_post", new HttpAsyncRequestHandler(){
    @Override
    public HttpAsyncRequestConsumer 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 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(){
    @Override
    public HttpAsyncRequestConsumer 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  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  fileItems = 
    upload.parseRequest(new FileUploadRequestContext(httpEntity));

这句代码将 httpEntity 转换成 FileItem 列表,FileItem有可能是普通的post数据字段,也可能是文件字段,我们可以通过
fileItem.isFormField()
来判别,如果值为true则表示普通数据字段,否则是文件。

你可能感兴趣的:(性能与Netty并列的Apache NIO HttpServer库使用详解)