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ù)字段习霹,否則是文件。