編寫HTTP 服務器與客戶端
Vert.x讓編寫非阻塞的HTTP 服務器與客戶端變得非常輕松。
創建HTTP 服務器
缺省狀況:
HttpServer server = vertx.createHttpServer();
配置HTTP 服務器
創建時也可以傳入HttpServerOptions實例:
HttpServerOptions options = new HttpServerOptions().setMaxWebsocketFrameSize(1000000); HttpServer server = vertx.createHttpServer(options);
開始監聽
讓服務器開始監聽,可以使用listen方法中的一個。
下面的例子里,服務器將監聽配置項指定的主機和端口:
HttpServer server = vertx.createHttpServer();
server.listen();
如果在listen方法中指定主機或者端口,將忽略配置項中所指定的:
HttpServer server = vertx.createHttpServer();
server.listen(8080, "myhost.com");
缺省的主機是0.0.0.0
,這意味着“監聽所有可用的地址”;缺省的端口號是80
。
實際的綁定是異步的,也就是說很可能對listen 方法的調用已經返回了,在這之后又過了一段時間監聽才開始。
如果想在服務器開始監聽時得到通知,可以在調用listen
方法時提供一個handler:
HttpServer server = vertx.createHttpServer(); server.listen(8080, "myhost.com", res -> { if (res.succeeded()) { System.out.println("Server is now listening!"); } else { System.out.println("Failed to bind!"); } });
請求到達時收到通知
設置requestHandler 可以讓你達成這個目的:
HttpServer server = vertx.createHttpServer(); server.requestHandler(request -> { // Handle the request in here });
處理請求
請求到達時,請求處理器會被調用;傳入的參數是一個HttpServerRequest對象的實例,它表示服務端的HTTP 請求。
請求頭( the headers of the request)被全部讀取后,handler會被調用。
如果請求帶有body ,服務器有可能會在請求handler被調用后才收到它。
從服務端的請求對象那,你可以拿到uri 、path 、param 、headers 等一些其他玩意。
每個服務端的請求對象都與一個服務端響應對象(server response object)相關聯。response方法可以讓你拿到HttpServerResponse對象的引用。
下面有個簡單例子,服務器處理了請求,並且以“hello world”回應之。
vertx.createHttpServer().requestHandler(request -> { request.response().end("Hello world"); }).listen(8080);
請求版本
version方法可以獲得請求的HTTP協議版本。
請求method
method方法可以獲得請求的HTTP method。(即 GET, POST, PUT, DELETE, HEAD, OPTIONS這些等)
請求URI
uri方法可以獲得請求的URI。
注意,這是HTTP請求中實際傳遞的,它幾乎總是相對URI。
URI的定義見 Section 5.1.2 of the HTTP specification - Request-URI。
請求路徑
path方法可以獲得URI的path 部分。
例如,請求URI是這樣的:
a/b/c/page.html?param1=abc¶m2=xyz
其path是:
/a/b/c/page.html
請求query
query方法可以獲得URI的query 部分。
請求頭
headers方法可以獲得HTTP 請求頭。
由於HTTP協議允許請求頭里一個key有多個值,所以headers方法的返回值是一個MultiMap對象(類似一個map,但是同一個key可以對應多個值)。
另外,這里的key不區分大小寫,所以你可以像下面這樣:
MultiMap headers = request.headers(); // Get the User-Agent: System.out.println("User agent is " + headers.get("user-agent")); // You can also do this and get the same result: System.out.println("User agent is " + headers.get("User-Agent"));
請求參數
params方法可以獲得HTTP請求的參數。
這個方法返回的也是個MultiMap對象。
請求URI上的參數,跟在path 后面。看下例的URI:
/page.html?param1=abc¶m2=xyz
參數是下面這樣的:
param1: 'abc' param2: 'xyz
注意這些參數是從URI里取得的。如果你要以multi-part/form-data
格式的請求體傳遞HTML 表單,那不會出現在這里。
遠程地址
remoteAddress方法可以獲得請求發送者的地址。
絕對URI
HTTP請求里傳入的通常是相對URI,用absoluteURI方法可以取得絕對URI 。
結束handler
當整個請求,包括消息體都被讀取后,endHandler將被調用。
從請求體中讀取數據
HTTP 請求通常會帶一個包含我們想要的數據的消息體。就像我們前面提到的,當請求頭到達時,request handler會被調用;而那時候消息體還不存在。
這是因為通常消息體都比較大(例如,上傳文件的時候),所以我們不會將整個消息體緩沖到內存里,真這么做可能會導致內存耗盡。
調用請求的handler可以接收消息體。每次消息體過來了一部分,handler都會被調用。
request.handler(buffer -> { System.out.println("I have received a chunk of the body of length " + buffer.length()); });
handler會收到一個Buffer參數。handler可能會被調用多次,這個取決於消息體的尺寸。
某些情況下(比如消息體很小時),你可能會想把body整個加載到內存里,可以像下面這樣:
Buffer totalBuffer = Buffer.buffer(); request.handler(buffer -> { System.out.println("I have received a chunk of the body of length " + buffer.length()); totalBuffer.appendBuffer(buffer); }); request.endHandler(v -> { System.out.println("Full body received, length = " + totalBuffer.length()); });
這種情況很常見,所以Vert.x提供了bodyHandler。bodyHandler只會在收到整個body后被調用一次:
request.bodyHandler(totalBuffer -> { System.out.println("Full body received, length = " + totalBuffer.length()); });
Pumping requests
請求對象是一個ReadStream,所以你可以把請求體pump (想象下水泵的作用,類似於一個帶動力的管道)到任意的WriteStream實例。
更多細節參考streams and pumps 一節。
處理HTML 表單
Content type為application/x-www-form-urlencoded
或multipart/form-data
時都可以提交HTML 表單。
對於url encoded 的表單,表單屬性是編碼在url 中的,就像url 的query 部分。
至於multi-part 表單,則是編碼在消息體中;除非已經讀取了整個消息體,否則它們是不可用的。
Multi-part 表單可以包含上傳的文件。
如果你想獲取一個multi-part 表單的屬性,那你應該在讀取消息體之前調用setExpectMultipart而且傳入參數應為true ,這樣才能讓Vert.x了解你的意圖。其后你要通過formAttributes方法來獲取表單屬性:
server.requestHandler(request -> { request.setExpectMultipart(true); request.endHandler(v -> { // The body has now been fully read, so retrieve the form attributes MultiMap formAttributes = request.formAttributes(); }); });
處理表單上傳的文件
Vert.x可以處理文件上傳。
要接收上傳的文件需要setExpectMultipart,然后設置請求的uploadHandler。
每當有上傳文件到達服務器時,此handler都將被調用。
傳入handler的是一個HttpServerFileUpload實例。
server.requestHandler(request -> { request.setExpectMultipart(true); request.uploadHandler(upload -> { System.out.println("Got a file upload " + upload.name()); }); });
上傳的文件可能會很大,所以我們不提供加載整個上傳文件到緩沖區這種可能導致內存耗盡的操作,取而代之的是,你可以按塊接收數據:
request.uploadHandler(upload -> { upload.handler(chunk -> { System.out.println("Received a chunk of the upload of length " + chunk.length()); }); });
上傳對象是ReadStream的實例,所以你可以將請求消息體pump 給任意的WriteStream實例。查看streams and pumps 一節可以獲取更多細節。
如果你只是想將上傳的文件寫入磁盤,可以使用streamToFileSystem方法:
request.uploadHandler(upload -> { upload.streamToFileSystem("myuploads_directory/" + upload.filename()); });
警告:生產系統中,你應該仔細檢查上傳文件的名稱,以防惡意客戶端妄為。參考安全指南一節獲取更多細節。
發送響應(Sending back responses)
服務端的響應對象是HttpServerResponse 類的實例。可以用請求的response 方法獲取。
響應對象可以給HTTP 客戶端應答。
設置狀態碼和消息
響應的缺省狀態碼是200
,表示OK
。
setStatusCode可以設置狀態碼。
通過setStatusMessage方法也可以自己指定狀態消息。
如果你不指定狀態消息,將根據狀態碼使用缺省的消息。
寫HTTP 響應
write方法將數據寫入響應。
響應結束前可以多此調用write方法,以多種方式調用:
可以是buffer:
HttpServerResponse response = request.response();
response.write(buffer);
可以是字符串,這時候會使用UTF-8編碼字符串:
HttpServerResponse response = request.response();
response.write("hello world!");
可以是指定編碼格式的字符串,這種情況下將以指定編碼格式將字符串編碼:
HttpServerResponse response = request.response();
response.write("hello world!", "UTF-16");
寫響應是個異步操作,當此操作進入隊列后,它會立即返回。
如果你只是將一個單獨的字符串或buffer寫入響應,可以單獨調用帶參數的end方法。
第一次對write方法的調用結果的響應頭(response headers)會寫入響應。因此,如果你不使用HTTP 分塊(chunking),這必須在寫入響應前設置Content-Length
頭信息;反之則無需擔心。
結束HTTP 響應
一旦你決定結束HTTP 響應,應調用end方法。
下面幾種方式都可以。
無參數,只是簡單地結束:
HttpServerResponse response = request.response(); response.write("hello world!"); response.end();
也可以像write
方法一樣帶一個字符串或buffer 參數,這樣的效果類似於調用write 方法后接着調用無參的end 方法。
HttpServerResponse response = request.response(); response.end("hello world!");
關閉底層連接
close方法用來關閉底層的TCP 連接。
響應結束時,Vert.x會自動關閉非keep-alive 的連接。
缺省狀況下,keep-alive 連接不會被自動關閉。如果你希望在空閑一段時間后自動關閉keep-alive 連接,可以使用使用setIdleTimeout方法。
設置響應頭
headers方法可以拿到響應頭對象,你可以把頭信息直接加進去:
HttpServerResponse response = request.response(); MultiMap headers = response.headers(); headers.set("content-type", "text/html"); headers.set("other-header", "wibble");
或者用putHeader方法。
HttpServerResponse response = request.response();
response.putHeader("content-type", "text/html").putHeader("other-header", "wibble");
添加頭信息必須在寫入響應消息體之前。
HTTP 響應的分塊和 trailers(這是啥?)
Vert.x支持 HTTP Chunked Transfer Encoding。
這種模式允許分塊寫入HTTP 響應體;通常用於寫入較大的消息體,且事先並不知道其大小。
設置分塊模式如下:
HttpServerResponse response = request.response();
response.setChunked(true);
缺省是不分塊的。在分塊模式下,每次對write方法的調用都會寫出一個新的HTTP 塊。
分塊模式下,你也可以將HTTP response trailers寫入響應對象。它們實際上被寫在響應的最后一個塊里。
trailers方法可以拿到trailers對象,然后你可以往里添加trailer:
HttpServerResponse response = request.response(); response.setChunked(true); MultiMap trailers = response.trailers(); trailers.set("X-wibble", "woobble").set("X-quux", "flooble");
或者用putTrailer方法。
HttpServerResponse response = request.response();
response.setChunked(true);
response.putTrailer("X-wibble", "woobble").putTrailer("X-quux", "flooble");
對磁盤或類路徑里的文件提供直接的文件服務
如果你在完成一個Web 服務器,提供文件服務的方式之一是以AsyncFile的方式打開它並把它pump 到HTTP 響應中去。
或者你也可以用readFile讀入文件,然后直接寫入響應中。
另外,Vert.x還提供了一種方法讓你支持文件服務。這種方式由操作系統支持,所以很可能發生在系統層面,不會經過用戶空間而直接從文件傳輸到socket。
那么怎么做到呢,調用sendFile即可;通常對大文件會更有效,而小文件有可能變慢。。
這里有個例子供參考:
vertx.createHttpServer().requestHandler(request -> { String file = ""; if (request.path().equals("/")) { file = "index.html"; } else if (!request.path().contains("..")) { file = request.path(); } request.response().sendFile("web/" + file); }).listen(8080);
發送文件時異步的,而且很可能方法調用已經返回而它還未結束。如果你想得到通知,可以用帶回調handler的版本:sendFile。
參考 serving files from the classpath一節獲取關於類路徑解析的限制以及如何禁用這一選項的細節。
注意:如果你在使用HTTPS 時用到
sendFile
,拷貝會發生在用戶空間里;否則文件將被內核直接從磁盤復制到socket,而我們沒有任何機會應用加密。
警告:如果你直接使用Vert.x編寫web 服務器,要小心用戶訪問其他文件路徑。更安全的方式是使用Vert.x Web。
當需要的只是文件片段時,通過指定的起始處,可以像下面這樣:
vertx.createHttpServer().requestHandler(request -> { long offset = 0; try { offset = Long.parseLong(request.getParam("start")); } catch (NumberFormatException e) { // error handling... } long end = Long.MAX_VALUE; try { end = Long.parseLong(request.getParam("end")); } catch (NumberFormatException e) { // error handling... } request.response().sendFile("web/mybigfile.txt", offset, end); }).listen(8080);
如果你想發送文件的部分是從某處到結尾,那並不需要提供長度,可以像下面這樣:
vertx.createHttpServer().requestHandler(request -> { long offset = 0; try { offset = Long.parseLong(request.getParam("start")); } catch (NumberFormatException e) { // error handling... } request.response().sendFile("web/mybigfile.txt", offset); }).listen(8080);
Pumping responses
服務器端的響應是一個WriteStream實例,所以你可以把任意的ReadStream對象pump 到這里。例如:AsyncFile、NetSocket、WebSocket、HttpServerRequest。
下面有個例子,很簡單,收到PUT 請求時把請求消息體作為響應返回。這里用到了pump,所以即使HTTP 請求體比可用內存都大很多也是能正常工作的:
vertx.createHttpServer().requestHandler(request -> { HttpServerResponse response = request.response(); if (request.method() == HttpMethod.PUT) { response.setChunked(true); Pump.pump(request, response).start(); request.endHandler(v -> response.end()); } else { response.setStatusCode(400).end(); } }).listen(8080);
HTTP 壓縮
Vert.x對HTTP 壓縮提供開箱即用(out of the box)的支持。
這意味着在消息體被發送給客戶端前會被自動壓縮。
如果客戶端不支持壓縮,那將會發送未壓縮的響應體過去。
這樣,不管支持HTTP 壓縮與否,這些客戶端都能被處理。
setCompressionSupported 可以啟用壓縮。
缺省情況下不會啟用這個特性。
啟用了HTTP 壓縮后,服務器會檢查客戶端是否包含Accept-Encoding
(這個頭信息包含支持的壓縮算法)。通常使用deflate 和gzip ,這兩種Vert.x都支持。
如果服務端找到了這樣一個頭信息,它會使用一種支持的壓縮算法自動對響應體進行壓縮,然后發給客戶端。
注意,壓縮也許能夠減少網絡流量的消耗,但是會消耗更多的CPU 資源。
創建HTTP 客戶端
可以用缺省選項創建一個HTTP 客戶端實例:
HttpClient client = vertx.createHttpClient();
如果你想指定某些配置,可以這樣:
HttpClientOptions options = new HttpClientOptions().setKeepAlive(false); HttpClient client = vertx.createHttpClient(options);
生成請求
http 客戶端很靈活,有數種方法可以生成請求。
很多時候,你會希望用一個http 客戶端生成很多請求到同一個主機/端口。為了避免每次都要重復配置主機/端口號,可以給客戶端配置缺省的主機/端口號:
HttpClientOptions options = new HttpClientOptions().setDefaultHost("wibble.com"); // Can also set default port if you want... HttpClient client = vertx.createHttpClient(options); client.getNow("/some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); });
或者你也可以為每次請求指定主機/端口號。
HttpClient client = vertx.createHttpClient(); // Specify both port and host name client.getNow(8080, "myserver.mycompany.com", "/some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }); // This time use the default port 80 but specify the host name client.getNow("foo.othercompany.com", "/other-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); });
以上兩種方式支持所有不同方式生成的請求。
沒有請求體的簡單請求
有時候,你可能想發出沒有消息體的請求,通常是GET、OPTIONS和HEAD請求。
最簡單的方式就是使用請求方法為前綴,后面加Now
的系列方法,如getNow
。
這些方法生成http 請求並把它們發送出去,你可以提供handler以便響應返回時調用。
HttpClient client = vertx.createHttpClient(); // Send a GET request client.getNow("/some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }); // Send a GET request client.headNow("/other-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); });
生成一般的請求
有時候直到運行時才能知道請求方式,對於這種情況,我們也提供了通用目的的請求方法:request
HttpClient client = vertx.createHttpClient(); client.request(HttpMethod.GET, "some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }).end(); client.request(HttpMethod.POST, "foo-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }).end("some-data");
寫請求消息體
有時候你會想發出帶消息體的請求,或者也許你想寫入一些請求頭信息。
那么你可以調用專門的請求方法如post或者通用的請求方法如request。
這些方法並不會立即發出請求,它們會返回HttpClientRequest的實例,你可以用此對象寫消息體或消息頭。
下面有些例子:
HttpClient client = vertx.createHttpClient(); HttpClientRequest request = client.post("some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }); // Now do stuff with the request request.putHeader("content-length", "1000"); request.putHeader("content-type", "text/plain"); request.write(body); // Make sure the request is ended when you're done with it request.end(); // Or fluently: client.post("some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }).putHeader("content-length", "1000").putHeader("content-type", "text/plain").write(body).end(); // Or event more simply: client.post("some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }).putHeader("content-type", "text/plain").end(body);
若指定編碼,那這些方法會將字符串以指定的格式編碼后寫入緩沖,否則將使用UTF-8 編碼:
request.write("some data"); // Write string encoded in specific encoding request.write("some other data", "UTF-16"); // Write a buffer Buffer buffer = Buffer.buffer(); buffer.appendInt(123).appendLong(245l); request.write(buffer);
如果你只需要往請求中寫一個字符串/buffer,調用一次end
方法即可。
request.end("some simple data"); // Write buffer and end the request (send it) in a single call Buffer buffer = Buffer.buffer().appendDouble(12.34d).appendLong(432l); request.end(buffer);
在寫請求時,對write
方法的第一次調用會導致頭信息被寫出來。
實際的寫是異步的,可能在(write)方法調用返回后還未發生。
帶有請求體的未分塊HTTP 請求需要設置Content-Length
頭信息。
因此,如果你不使用分塊的HTTP,則必須在寫入請求前設置Content-Length
頭信息;否則就太晚了。
如果你調用了含參的end
方法,那么在寫請求前,Vert.x會自動計算並設置Content-Length
頭信息。
如果你使用分塊的HTTP,那么Content-Length
並不是必要的,所以你也不用提前計算大小了。
寫請求頭
headers方法獲得的對象可以用來設置請求頭,這是一個multi-map :
MultiMap headers = request.headers(); headers.set("content-type", "application/json").set("other-header", "foo");
這是MultiMap的實例,提供了增加、設置、刪除條目的操作。Http 頭信息允許在同一個鍵值上設置多個值。
putHeader方法也可用於寫入請求頭。
request.putHeader("content-type", "application/json").putHeader("other-header", "foo");
如果你希望寫入一些請求頭信息,那么必須在寫請求體前做。
結束HTTP 請求
當你完成HTTP 請求時,需要用一個end操作結束它。
結束一個請求會導致頭信息被寫入(如果它們還沒被寫),然后請求被標記為完成。
有幾種結束請求的辦法。可以用無參的end 方法:
request.end();
或者在調用end
方法時傳入一個字符串/buffer,這樣做類似於在調用無參end
方法前先調用一次write
方法。
request.end("some-data"); // End it with a buffer Buffer buffer = Buffer.buffer().appendFloat(12.3f).appendInt(321); request.end(buffer);
分塊的HTTP 請求
Vert.x支持請求的HTTP Chunked Transfer Encoding 。
這種方式允許分塊寫HTTP 請求體,通常用於較大的請求體以流的方式傳送給服務器時;因為這種時候無法預知請求體的大小。
setChunked可用於設置分塊模式。
分塊模式下,每次對write 方法的調用都會產生一個新塊用於寫入。這種模式無需設置Content-Length
頭信息。
request.setChunked(true); // Write some chunks for (int i = 0; i < 10; i++) { request.write("this-is-chunk-" + i); } request.end();
請求超時
setTimeout方法用來設置超時的時長。
如果到超時為止,請求都沒有返回任何數據;那么將會有一個異常產生並被傳入異常handler(如果有提供),之后請求將被關閉。
處理異常
通過在HttpClientRequest實例上設置異常handler,你可以處理與之相關的異常:
HttpClientRequest request = client.post("some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }); request.exceptionHandler(e -> { System.out.println("Received exception: " + e.getMessage()); e.printStackTrace(); });
狀態碼為2xx的響應不會在這處理,你需要拿到HttpClientResponse對象的狀態碼后處理:
HttpClientRequest request = client.post("some-uri", response -> { if (response.statusCode() == 200) { System.out.println("Everything fine"); return; } if (response.statusCode() == 500) { System.out.println("Unexpected behavior on the server side"); return; } }); request.end();
重要:
XXXNow
系列方法不能處理異常。
為客戶端請求指定handler
創建客戶端請求對象時,你可以先不指定handler;而是之后像下面這樣調用handler方法設置:
HttpClientRequest request = client.post("some-uri"); request.handler(response -> { System.out.println("Received response with status code " + response.statusCode()); });
把請求當成流(stream)使用
HttpClientRequest實例同樣是WriteStream對象,這意味着你可以把任意的ReadStream實例pump 到它這里。
例如,將磁盤上的文件pump 到http 請求的消息體中:
request.setChunked(true); Pump pump = Pump.pump(file, request); file.endHandler(v -> request.end()); pump.start();
處理http 響應
不管你是通過請求方法設置了handler,還是直接為HttpClientRequest指定了handler,你都會在handler中接收到參數:一個HttpClientResponse實例。
通過statusCode和statusMessage方法你可以查詢狀態碼和狀態信息。
client.getNow("some-uri", response -> { // the status code - e.g. 200 or 404 System.out.println("Status code is " + response.statusCode()); // the status message e.g. "OK" or "Not Found". System.out.println("Status message is " + response.statusMessage()); });
把響應當成流使用
HttpClientResponse實例也是ReadStream對象,這意味着你可以把它pump 到任意的WriteStream實例中去。
響應頭與trailers
Http 響應也可以帶一些頭信息。要獲得頭信息可以使用headers方法。
返回對象是一個MultiMap。
String contentType = response.headers().get("content-type"); String contentLength = response.headers().get("content-lengh");
分塊的HTTP 響應也可能包含tarilers,在響應體的最后一個塊那。
可以使用trailers方法獲得trailers ,它也是一個MultiMap對象。
讀取請求體
響應頭都被讀取后,響應handler 將被調用。
如果響應帶有消息體,你很可能在消息頭被讀取后一陣子才收到它。我們不會等到整個消息體都到了才去調用響應handler ;否則如果消息體很大,我們將會等太久或者直接內存溢出。
每次有消息體的部分到達時,handler都會被調用,有一個表示這部分消息體的Buffer會作為參數傳入:
client.getNow("some-uri", response -> { response.handler(buffer -> { System.out.println("Received a part of the response body: " + buffer); }); });
如果你知道消息體不是很大並且想在處理前先聚起來,可以自己手動完成:
client.getNow("some-uri", response -> { // Create an empty buffer Buffer totalBuffer = Buffer.buffer(); response.handler(buffer -> { System.out.println("Received a part of the response body: " + buffer.length()); totalBuffer.appendBuffer(buffer); }); response.endHandler(v -> { // Now all the body has been read System.out.println("Total response body length is " + totalBuffer.length()); }); });
還有一種便利的方法是使用bodyHandler ,它會在響應被完全讀取時調用,參數是整個消息體。
響應結束handler
當整個響應體都被讀取后,endHandler將被調用;如果沒有響應體,那么頭信息被讀取並且響應handler被調用后,endHandler也會被調用。
從響應中讀取cookies
cookies方法可以從響應中獲得cookie列表。
或者你也可以自行解析Set-Cookie
頭信息。
100-Continue handling
根據 HTTP 1.1 specification ,客戶端可以設置Expect: 100-Continue
這樣一個頭信息並在發送剩余的請求體前把它發出去。
服務器(在遇到這種情況時)可以回應以一個暫時的狀態Status: 100 (Continue)
,這表示客戶端可以把剩余的消息體送過來了。
這種方式讓服務器在大量數據發送之前可以做認證並接受/拒絕請求。否則如果請求可能不被接受,那發送大量數據會造成帶寬浪費;並且服務器讀取數據再丟棄的行為也很不經濟。
Vert.x允許你在客戶端請求對象上設置continueHandler。
如果服務器發回一個Status: 100 (Continue)
響應,這個handler將被調用。
這個和sendHead
合起來用於發送請求頭。
下面有個例子:
HttpClientRequest request = client.put("some-uri", response -> { System.out.println("Received response with status code " + response.statusCode()); }); request.putHeader("Expect", "100-Continue"); request.continueHandler(v -> { // OK to send rest of body request.write("Some data"); request.write("Some more data"); request.end(); });
Vert.x的http 服務器可以配置成收到Expect: 100-Continue
請求頭時自動發回一個 100 Continue 響應。
設置setHandle100ContinueAutomatically選項即可。
如果你更希望手動選擇是否發送continue 響應,那這個屬性應該設置成false
(這是缺省選擇);然后你可以檢查頭信息並調用writeContinue 以便客戶端繼續發送消息體:
httpServer.requestHandler(request -> { if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) { // Send a 100 continue response request.response().writeContinue(); // The client should send the body when it receives the 100 response request.bodyHandler(body -> { // Do something with body }); request.endHandler(v -> { request.response().end(); }); } });
你也可以通過直接發送失敗(failure )狀態碼來拒絕請求:這時候要買消息體被忽略,要么連接被關閉(100-Continue 是一個性能提示,並不能成為邏輯上的協議約束):
httpServer.requestHandler(request -> { if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) { // boolean rejectAndClose = true; if (rejectAndClose) { // Reject with a failure code and close the connection // this is probably best with persistent connection request.response() .setStatusCode(405) .putHeader("Connection", "close") .end(); } else { // Reject with a failure code and ignore the body // this may be appropriate if the body is small request.response() .setStatusCode(405) .end(); } } });
在客戶端啟用壓縮
Http 客戶端支持HTTP 壓縮。
這意味着客戶端可以讓遠程服務器了解這一點,並能夠處理壓縮過的響應體。
Http 服務器可以自由選擇壓縮算法來壓縮消息體,也可以不壓縮就發送。所以對服務器而言,這只是一個提示,它可以忽略的。
在告知服務器客戶端是否支持壓縮時,有一個請求頭Accept-Encoding
,其值為所支持的壓縮算法。有不止一種壓縮算法受到支持。在Vert.x里,頭信息會是這樣的:
Accept-Encoding: gzip, deflate
服務器將從中選擇一種。通過檢查Content-Encoding
頭信息,你可以了解服務器是否有壓縮消息體。
如果響應體是用gzip算法壓縮的,那么響應將包含下面的頭信息:
Content-Encoding: gzip
要啟用壓縮,可以在創建客戶端時設置setTryUseCompression 選項。
Pooling and keep alive
Http 的keep alive 技術允許一個連接被多個請求使用。這在你向同一個服務器發出多次請求時更有效。
Http 客戶端支持連接池,你可以復用連接。
要使連接池技術正常生效,keep live 屬性必須為true ,你可以在配置客戶端時調用setKeepAlive方法設置它。缺省值是true 。
若keep alive 是開啟的,Vert.x會為每個HTTP/1.0 請求增加Connection: Keep-Alive
這樣一個頭信息。若keep alive 被禁用,Vert.x會為每個HTTP/1.1 請求增加Connection: Close
這樣一個頭信息,這意味着響應完成后請求將被關閉。
調用setMaxPoolSize 方法可以為服務器設置連接池的大小。
在啟用連接池后,生成新請求時若已建立的連接數小於連接池的大小,則Vert.x會創建一個新連接;反之則把請求加入隊列。
Keep alive 的連接不會被客戶端自動關閉。你可以通過關閉客戶端實例來關閉它們。
或者你也可以調用setIdleTimeout 設置一個空閑超時-任意連接如果超過這個時間未被使用將被關閉。記住空閑超時的計時單位是秒而不是毫秒。
管道(Pipe-lining)
客戶端還支持同一連接上的請求管道式發送。
Pipe-lining 的意思是同一個連接上,在前一個請求的響應返回之前就發出另一個請求。Pipe-lining 並不是對所有的請求都適用。
調用setPipelining 方法可以啟用pipe-lining 。缺省情況下這個特性是關閉的。
Pipe-lining 啟用時,發起請求時不會等待之前(請求)的響應返回。
Http 客戶端慣用法
Http 客戶端可用於Verticle,也可以嵌入別的程序使用。
用於Verticle 時,此Verticle 應該只使用自己的客戶端實例。
說的更普遍一點,一個客戶端不應該被不同的Vert.x上下文共享,否則可能導致不可預測的行為。
例如,一個keep-alive 連接將在打開它的請求上下文中調用客戶端的handler ,后續的請求將使用同樣的上下文。
當這種情況發生時,Vert.x 會檢測到並打印一條警告日志:
Reusing a connection with a different context: an HttpClient is probably shared between different Verticles
Http 客戶端可以被嵌入一個非Vert.x 線程(例如單元測試、普通的main
方法):客戶端handler 將被不同的Vert.x線程、上下文調用,這時候上下文會被按需創建。生產環境中不推薦這種用法。
服務器共享
當數個HTTP 服務器監聽同一個端口時,Vert.x會使用round-robin 策略來精確調度請求的處理。
讓我們在Verticle 中創建一個HTTP 服務器:
io.vertx.examples.http.sharing.HttpServerVerticle
vertx.createHttpServer().requestHandler(request -> { request.response().end("Hello from server " + this); }).listen(8080);
這個服務將監聽8080 端口。那么,當這個Verticle 被如下多次實例化:vertx run io.vertx.examples.http.sharing.HttpServerVerticle -instances 2
時,會發生什么呢?如果多個Verticle 被綁定在同一端口,你將收到socket 異常。幸運的是,Vert.x已經為你處理好了這件事。當你在同一主機同一端口上部署另一個服務器時,因為已經存在一個服務器,所以實際並不會創建一個新服務器去監聽這個端口,綁定socket 的動作只會發生一次。接收到請求時會遵循round robin 策略調用服務器handler。
讓我們想象下面這個客戶端:
vertx.setPeriodic(100, (l) -> { vertx.createHttpClient().getNow(8080, "localhost", "/", resp -> { resp.bodyHandler(body -> { System.out.println(body.toString("ISO-8859-1")); }); }); });
Vert.x連續地將請求分配給服務器中的一個:
Hello from i.v.e.h.s.HttpServerVerticle@1
Hello from i.v.e.h.s.HttpServerVerticle@2
Hello from i.v.e.h.s.HttpServerVerticle@1
Hello from i.v.e.h.s.HttpServerVerticle@2
...
因此服務器可以擴展到可用的CPU核心上,而每個Verticle 實例仍然是嚴格地單線程運行。這樣你就不需要為了利用上多核機器的處理能力煞費苦心了,像負載均衡這種玩意完全不需要。
在Vert.x中使用HTTPS
Vert.x中,http 服務器和客戶端可以像net 服務器那樣,以同樣的方式配置成使用HTTPS 。
更多細節請參考 configuring net servers to use SSL 。
WebSockets
WebSockets是這樣一種技術:它使你可以在HTTP 服務器和HTTP 客戶端(典型的如瀏覽器)建立全雙工的類socket(a full duplex socket-like) 連接。
服務器和客戶端兩邊,Vert.x都支持WebSockets 。
服務端的WebSocket
有兩種方式在服務端處理WebSocket。
WebSocket handler
第一種方式是為服務器實例提供一個websocketHandler 。
當WebSocket 連接建立時,這個handler將被調用,參數是一個ServerWebSocket 實例。
server.websocketHandler(websocket -> { System.out.println("Connected!"); });
也可以調用reject 方法拒絕WebSocket 。
server.websocketHandler(websocket -> { if (websocket.path().equals("/myapi")) { websocket.reject(); } else { // Do something } });
升級到WebSocket
第二種處理Websocket 的方式通過處理客戶端發送的HTTP 升級請求來實現,在服務器端調用請求對象的upgrade 方法。
server.requestHandler(request -> { if (request.path().equals("/myapi")) { ServerWebSocket websocket = request.upgrade(); // Do something } else { // Reject request.response().setStatusCode(400).end(); } });
The server WebSocket
通過ServerWebSocket 實例,你可以獲取到發起WebSocket 握手的請求的這些信息:headers、path、query、URI。
客戶端的WebSocket
HttpClient 也支持WebSocket 。
通過websocket 系列方法並提供handler,你可以創建到服務器的WebSocket 連接。
連接建立的時候,handler會被調用,傳入的參數是一個WebSocket 實例:
client.websocket("/some-uri", websocket -> { System.out.println("Connected!"); });
往WebSocket 寫消息
writeBinaryMessage 方法可以往WebSocket 里寫入一條二進制WebSocket 消息:
Buffer buffer = Buffer.buffer().appendInt(123).appendFloat(1.23f);
websocket.writeBinaryMessage(buffer);
setMaxWebsocketFrameSize 方法可以設置websocket 幀的最大尺寸,如果WebSocket 消息的大小超出了這個值,Vert.x 會在發送前將其分割成多個WebSocket 幀。
往WebSocket 里寫入幀
一個WebSocket 消息可能由多個幀組成。這種情況下,第一幀要么是二進制的要么是文本的,后面會跟着零或多個后續幀。
消息中的最后一幀將被標記為*final *。
為了發送一個多幀的消息,你可以用WebSocketFrame.binaryFrame 、WebSocketFrame.textFrame 、WebSocketFrame.continuationFrame 創建幀,然后通過writeFrame 方法將其寫入WebSocket 。
下面是個二進制幀的例子:
WebSocketFrame frame1 = WebSocketFrame.binaryFrame(buffer1, false); websocket.writeFrame(frame1); WebSocketFrame frame2 = WebSocketFrame.continuationFrame(buffer2, false); websocket.writeFrame(frame2); // Write the final frame WebSocketFrame frame3 = WebSocketFrame.continuationFrame(buffer2, true); websocket.writeFrame(frame3);
很多時候,你只想發送一個單幀的websocket 消息,為此我們提供了一組便捷的方法:writeFinalBinaryFrame 、writeFinalTextFrame 。
看例子:
websocket.writeFinalTextFrame("Geronimo!"); // Send a websocket messages consisting of a single final binary frame: Buffer buff = Buffer.buffer().appendInt(12).appendString("foo"); websocket.writeFinalBinaryFrame(buff);
從WebSocket 中讀取幀
通過frameHandler 可以從WebSocket 中讀取幀。
當某一幀到達時,幀handler 將被調用,傳入參數是WebSocketFrame 類的實例:
websocket.frameHandler(frame -> { System.out.println("Received a frame of size!"); });
關閉WebSocket
當你處理完后,可以調用close 方法關閉WebSocket 連接。
流式WebSocket
WebSocket 實例也是ReadStream 和WriteStream 類的對象,所以pump 技術在此也可以使用。
將WebSocket 當作write stream 或read stream 用時,只能是在未分割成多幀的二進制幀的WebSocket 連接里。
Verticle 的自動清理
如果你在Verticle 里創建了http 服務器和客戶端,則在卸載Verticle 時,它們將被自動關閉。
作者:半枚荔枝
鏈接:https://www.jianshu.com/p/06cb26aab9e8
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。