Upload
adrian-cole
View
2.090
Download
2
Tags:
Embed Size (px)
DESCRIPTION
Developers choose HTTP for its ubiquity. HTTP's semantics are cherry-picked or embraced in the myriad of apis we develop and consume. Efficiency discussions are commonplace: Does this design imply N+1 requests? Should we denormalize the model? How do consumers discover changes in state? How many connections are needed to effectively use this api? Meanwhile, HTTP 1.1 is a choice, as opposed to constant. SPDY and HTTP/2 implementations surface, simultaneously retaining semantics and dramatically changing performance implications. We can choose treat these new protocols as more efficient versions HTTP 1.1 or buy into new patterns such as server-side push. This session walks you through these topics via an open source project from Square called okhttp. You'll understand how okhttp addresses portability so that you can develop against something as familiar as java's HTTPUrlConnection. We'll review how to use new protocol features and constraints to keep in mind along the way. You'll learn how to sandbox ideas with okhttp's mock server, so that you can begin experimenting with SPDY and HTTP/2 today!
Citation preview
Efficient HTTP ApisA walk through http/2 via okhttp
@adrianfcole
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
* Can be blamed for the http/2 defects in okhttp!
@adrianfcole
• staff engineer at Twitter• founded apache jclouds• focus on cloud computing
okhttp• HttpURLConnection compatible• designed for java and android• BDFL: Jesse Wilson from square
https://github.com/square/okhttp
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
$ curl https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’[ { "id": 1, "owner_id": 0, "name": "Able" },... { "id": 26, "owner_id": 0,
$ curl https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’{ "id": 2, "owner_id": 0, "name": "Beatific"}
$ curl -X POST https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Content-Type: application/json’ -d’{ "name": "Open-minded"}’
$ curl -X DELETE https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’
json apis are so simple
$ curl https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’[ { "id": 1, "owner_id": 0, "name": "Able" },... { "id": 26, "owner_id": 0, "name": "Zest" }
$ curl https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’{ "id": 2, "owner_id": 0, "name": "Beatific"}
$ curl -X POST https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Content-Type: application/json’ -d’{ "name": "Open-minded" }’
$ curl -X DELETE https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’
so many bytes?!
1642!
$ curl --compress https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’-H ‘Accept-encoding: gzip, deflate’[ { "id": 1, "owner_id": 0, "name": "Able" },... { "id": 26, "owner_id": 0, "name": "Zest"
$ curl https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Accept: application/json’{ "id": 2, "owner_id": 0, "name": "Beatific"}
$ curl -X POST https://apihost/things \-H ‘SecurityToken: b08c85073c1a2d02’ \-H ‘Content-Type: application/json’ -d’{ "name": "Open-minded" }’
$ curl -X DELETE https://apihost/things/2 \-H ‘SecurityToken: b08c85073c1a2d02’
now with gzip
318
java + gsonurl = new URL(“https://apihost/things”);connection = (HttpURLConnection) url.openConnection();connection.setRequestProperty(“SecurityToken”, “b08c85073c1a2d02”);connection.setRequestProperty(“Accept”, “application/json”);connection.setRequestProperty(“Accept-Encoding”, "gzip, deflate");is = connection.getInputStream();
isr = new InputStreamReader(new GZIPInputStream(is));things = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){});
okhttp + gsonclient = new OkHttpClient();url = new URL(“https://apihost/things”);connection = new OkUrlFactory(client).open(url);connection.setRequestProperty(“SecurityToken”, “b08c85073c1a2d02”);connection.setRequestProperty(“Accept”, “application/json”);connection.setRequestProperty(“Accept-Encoding”, "gzip, deflate");is = connection.getInputStream();
isr = new InputStreamReader(is); // automatic gunzipthings = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){});is.close();
We won!
• List body reduced from 1642 to 318 bytes!
• We saved some lines using OkHttp
• Concession: cpu for compression, curl is a little verbose.
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
okhttp2• A year in the making• New API, okio, samples• Bridge with v1.6; current v2.1
https://github.com/square/okhttp
okhttp 2client = new OkHttpClient();request = new Request.Builder() .url(“https://apihost/things”) .header(“SecurityToken”, “b08c85073c1a2d02”) .header(“Accept”, “application/json”); .header(“Accept-Encoding”, "gzip, deflate").build(); // look ma.. I’m immutable!
reader = client.call(request).execute().body().charStream();things = new Gson().fromJson(reader, new TypeToken<List<Thing>>(){});
• OkHttp has its own api, which has a concise way to synchronously get bytes or chars (via okio).
okhttp callclient = new OkHttpClient();request = new Request.Builder() .url(“https://apihost/things”) .header(“SecurityToken”, “b08c85073c1a2d02”) .header(“Accept”, “application/json”); .header(“Accept-Encoding”, "gzip, deflate").build();
call = client.call(request);call.enqueue(new ParseThingsCallback());call.cancel(); // changed my mind
• OkHttp also has an async api, which (also) supports cancel!
• # Explicity trust certs for your properties• new CertificatePinner.Builder()• .add(“api.my.biz”, publicKeySHA1)• .build()
• # Respond to authentication challenges• new Authenticator() {• public Request authenticate(Proxy p, Response rsp) {• return rsp.request().newBuilder()• .header(“Authorization", “Bearer …”)
• # Set your own floor for TLS• new ConnectionSpec.Builder(MODERN_TLS)• .tlsVersions(TlsVersion.TLS_1_2).build()
okhttpsecurity
okio• ByteString• Unbounded Buffer• Source/Sink abstraction
https://github.com/square/okio
// Okio is (more than) a prettier DataInputStreamBufferedSink sink = Okio.buffer(Okio.sink(file));sink.writeUtf8("Hello, java.io file!”);sink.writeLong(1000000000000000L);sink.close();
// Streaming writes with no housekeepingnew RequestBody() { @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } }—snip—
// Lazy, composable bodiesnew MultipartBuilder("123") .addPart(RequestBody.create(null, "Quick")) .addPart(new StreamingBody("Brown"))
okiosamples
samples• Starters like web crawler• How-to guide w/ recipes
https://github.com/square/okhttp/tree/master/samples
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
Hey.. List #1 is slow!
368!
Ask Ilya why!
• TCP connections need 3-way handshake.
• TLS requires up to 2 more round-trips.
• Read High Performance Browser Networking
http://chimera.labs.oreilly.com/books/1230000000545
HttpUrlConnection
• http.keepAlive - should connections should be pooled at all? Default is true.
• http.maxConnections - maximum number of idle connections to each host in the pool. Default is 5.
• get[Input|Error]Stream().close() - recycles the connection, if fully read.
• disconnect() - removes the connection.
Don’t forget to read!...
is = tryGetInputStream(connection);
isr = new InputStreamReader(is);things = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){});...
InputStream tryGetInputStream(HttpUrlConnection connection) throws IOException {try { return connection.getInputStream();} catch (IOException e) { InputStream err = connection.getErrorStream(); while (in.read() != -1); // skip err.close(); throw e;}
or just use okhttp2try (ResponseBody body = client.call(request).execute().body()) { // connection will be cleaned on close.}
client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { // body is implicitly closed on failure }
@Override public void onResponse(Response response) { // make sure you call response.body().close() when done }});
We won!
• Recycled socket requests are much faster and have less impact on the server.
• Concessions: must read responses, concurrency is still bounded by sockets.
Let’s make List #2 free
Cache-Control: private, max-age=60, s-maxage=0Vary: SecurityToken
Step 1: add a little magic to your server response
Step 2: make sure you are using a cache!
client.setCache(new Cache(dir, 10_MB));
Step 3: pray your developers don’t get clever
// TODO: stupid server sends stale data without thisnew Request.Builder().url(”https://apihost/things?time=” + currentTimeMillis());
// Limit cache durationthisMessageWillSelfDestruct = new Request.Builder() .url(“https://foo.com/weather”) .cacheControl(new CacheControl.Builder().maxAge(12, HOURS).build()) .build();
// Opt-out of response cachingnotStored = new Request.Builder() .url(“https://foo.com/private”) .cacheControl(CacheControl.FORCE_NETWORK) .build();
// Offline modeoffline = new Request.Builder() .url(“https://foo.com/image”) .cacheControl(CacheControl.FORCE_CACHE) .build();
okhttp cache recipes
We won again!
• No time or bandwidth used for cached responses
• No application-specific cache bugs code.
• Concessions: only supports “safe methods”, caching needs to be tuned.
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
http/1.1• rfc2616 - June 1999• text-based framing• defined semantics of the web
http://www.w3.org/Protocols/rfc2616/rfc2616.html
FYI: RFC 2616 is dead!
• RFC 7230-5 replaces RFC 2616.
• Checkout details on www.mnot.net/blog
spdy/3.1
http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1
• google - Sep 2013• binary framing• retains http/1.1 semantics• concurrent multiplexed streams
http/2
https://github.com/http2/http2-spec
• ietf draft 16 - June 2014• binary framing• retains http/1.1 semantics• concurrent multiplexed streams
multiplexing
header compression
flow control
priority
server push
http/2 headline features
multiplexing
header compression
server push
http/2 headline features
Looking at the whole message
Request: request line, headers, and body
Response: status line, headers, and body
http/1.1 round-trip
GZIPPED DATA
Content-Length: 318Cache-Control: private, max-age=60, s-maxage=0Vary: SecurityToken Date: Sun, 02 Feb 2014 20:30:38 GMTContent-Type: application/jsonContent-Encoding: gzip
Host: apihostSecurityToken: b08c85073c1a2d02Accept: application/jsonAccept-encoding: gzip, deflate
GET /things HTTP/1.1
HTTP/1.1 200 OK
http/2 round-trip
GZIPPED DATA
:status: 200content-length: 318cache-control: private, max-age=60, s-maxage=0vary: SecurityToken date: Sun, 02 Feb 2014 20:30:38 GMTcontent-type: application/json
:method: GET:authority: apihost:path: /thingssecuritytoken: b08c85073c1a2d02accept: application/jsonaccept-encoding: gzip, deflate
HEADERS
Stream: 3
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS
DATA
Stream: 3
Flags: END_STREAM
interleaving
HEADERS
Stream: 5
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS
DATA
Stream: 5
Flags:
DATA
Stream: 5
Flags: END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 5
Flags: END_HEADERS
DATA
Stream: 3
Flags: END_STREAM
Canceling a Stream
HEADERS
Stream: 5
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS
DATA
Stream: 5
Flags:
HEADERS
Stream: 3
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 5
Flags: END_HEADERS
DATA
Stream: 3
Flags: END_STREAM
RST_STREAM
Stream: 5
ErrorCode: CANCEL
control frames
HEADERS
Stream: 5
HEADERS
Stream: 3
DATA
Stream: 5
DATA
Stream: 3
HEADERS
Stream: 3
HEADERS
Stream: 5
SETTINGS
Stream: 0
SETTINGS
Stream: 0
DATA
Stream: 5
OkHttp/2 Architecture
Frame ReaderThread
Frame WriterLock
Control Frame Queue
multiplexing
header compression
server push
http/2 headline features
http/1.1 headers
GZIPPED DATA
Content-Length: 318Cache-Control: private, max-age=60, s-maxage=0Vary: SecurityToken Date: Sun, 02 Feb 2014 20:30:38 GMTContent-Type: application/jsonContent-Encoding: gzip
Host: apihostSecurityToken: b08c85073c1a2d02Accept: application/jsonAccept-encoding: gzip, deflate
GET /things HTTP/1.1
HTTP/1.1 200 OK
168!
195!
318
• You can gzip data, but not headers!
header compression
GZIPPED DATA
:status: 200content-length: 318cache-control: private, max-age=60, s-maxage=0vary: SecurityToken date: Sun, 02 Feb 2014 20:30:38 GMTcontent-type: application/jsoncontent-encoding: gzip
:method: GET:authority: apihost:path: /thingssecuritytoken: b08c85073c1a2d02accept: application/jsonaccept-encoding: gzip, deflate
HEADERS
Stream: 3
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS
DATA
Stream: 3
Flags: END_STREAM
8 bytes
52 bytes compressed
8 bytes
85 bytes compressed
• 161 byte overhead instead of 363 8 bytes
indexed headers!
GZIPPED DATA
:status: 200content-length: 318cache-control: private, max-age=60, s-maxage=0vary: SecurityToken date: Sun, 02 Feb 2014 20:30:39 GMTcontent-type: application/jsoncontent-encoding: gzip
:method: GET:authority: apihost:path: /thingssecuritytoken: b08c85073c1a2d02accept: application/jsonaccept-encoding: gzip, deflate
HEADERS
Stream: 3
Flags: END_HEADERS, END_STREAM
HEADERS
Stream: 3
Flags: END_HEADERS
DATA
Stream: 3
Flags: END_STREAM
8 bytes
28 bytes compressed
8 bytes
30 bytes compressed
• 82 byte overhead instead of 363 8 bytes
hpack
http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
• ietf draft 10 - Dec 2014• static and dynamic table• huffman encoding
multiplexing
header compression
server push
http/2 headline features
push promise
:method: GET:path: /things...
HEADERS
Stream: 3
HEADERS
Stream: 3
DATA
Stream: 4
:method: GET:path: /users/0...
PUSH_PROMISE
Stream: 3
Promised-Stream: 4
HEADERS
Stream: 4
push response goes into
cache
DATA
Stream: 3
Server guesses a future request or indicates a cache invalidation
okhttp + http/2
• OkHttp 2.1 supports http/2 on TLS connections.
• Works out of the box on Android.
• For Java, add jetty’s NPN to your bootclasspath.
java -Xbootclasspath/p:/path/to/npn-boot-8.1.2.v20120308.jar ...
We won!
• 1 socket for 100+ concurrent streams.
• Advanced features like flow control, cancellation and cache push.
• Dramatically less header overhead.
Getting started
• Connect to twitter or use OkHttp’s MockWebServer.
https://github.com/square/okhttp
introduction
hello http api!
hello okhttp/2!
uh oh.. scale!
hello http/2!
wrapping up
Engage!
• Work with http/2, or at least spdy!
• Investigate http/2 api practices like libchan.
• Work on okhttp with us, and try version 2!
https://github.com/docker/libchan
https://code.google.com/p/mod-spdy/source/list
https://github.com/square/okio
Thank you!
yes, twitter is hiring!yes, twitter runs http/2!
github square/okhttp@adrianfcole