Efficient HTTP Apis

Preview:

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

Recommended