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则表示普通数据字段,否则是文件。