Vert.x3 Core手册 [Part 3. HTTP 服务器与客户端]

本文依照 知识共享许可协议(署名-非商业性使用-禁止演绎) 发布。


编写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&param2=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&param2=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-urlencodedmultipart/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 到这里。例如:AsyncFileNetSocketWebSocketHttpServerRequest

下面有个例子,很简单,收到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实例。

通过statusCodestatusMessage方法你可以查询状态码和状态信息。

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 握手的请求的这些信息:headerspathqueryURI

客户端的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 时,它们将被自动关闭。


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

推荐阅读更多精彩内容