HTTP и REST

Гоголев Сергей

REST – архитектурный стиль, позволяющий сделать сетевое взаимодействие удобнее, прозрачнее и стандартизованнее

Взаимодействие приложений



        repo.createIssue(title, description)
             ⇡           ⇡
             method      arguments
    

Но удалённо!

RPC (Remote Procedure Call)


// request
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "createIssue",
    "params": [title, description]
}
    

// response
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": "Ok"
}
    

RPC (Remote Procedure Call)

Клиент зависит от конкретных методов

И от порядка и количества аргументов

Фиксирует формат ответа

REST (REpresentational State Transfer)


POST /issue HTTP/1.1
Host: github.com

{
    "title": "Add js linter",
    "description": "Eslint or jscs"
}
    

HTTP/1.1 201 Created
Location: /issue/42
    

REST vs RPC

RPC опирается на методы (действия),
REST – на ресурсы (объекты, сущности)

RPC использует HTTP только как транспорт, REST – базируется на HTTP и расширяет его

Метрики взаимодействия

Latency – время необходимое пакету добраться от источника в пункт назначения (milliseconds)

Bandwidth – максимально возможный объём данных, передаваемых за единицу времени (bits per second)

На малых объёмах данных latency имеет решаюшее влияние на производительность!

Latency vs Bandwidth

Уменьшение latency главная задача оптимизации

OSI

TCP

TCP (Transmission Control Protocol)

  • Устанавливает соединение
  • Обрабатывает потери
  • Устраняет дублирование
  • Гарантирует целостность передачи

3-way handshake

Установка нового соединения
дорогая операция

Slow start

В начале нового соединения скорость передачи информации далека от максмальной

Кто виноват? Что делать?

Уменьшить объём передаваемых данных

Уменьшить количество TCP соединений

HTTP
(HyperText Transfer Protocol)

Формат запроса


POST /notes HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Host: localhost:5000
User-Agent: HTTPie/0.9.3

{
    "title": "Add js linter",
    "description": "Eslint or jscs"
}
    

Start line
Message headers



Empty line
Message body

Формат ответа


HTTP/1.1 200 OK
Content-Length: 67
Content-Type: application/json; charset=utf-8
Date: Wed, 16 Mar 2016 14:32:18 GMT
X-Powered-By: Express

{
    "createdAt": 1458138738899,
    "name": "music",
    "text": "Music to listen"
}
    

Start line
Message headers



Empty line
Message body

Ресурсы


/notes              - заметки

/notes/film         - заметка о фильмах
    
/notes/film/fav     - закладка на заметке
    
/notes/film/public  - публичность заметки
    

URL (Uniform Resource Locator)


http://localhost:50000/notes?limit=10
⇡      ⇡         ⇡    ⇡     ⇡
scheme host      port path  query
    

POST /notes?limit=10 HTTP/1.1
Host: localhost:5000
    

Методы


GET      – получение ресурса

HEAD     – получение только заголовков

POST     – создание ресурса

PUT      – обновление ресурса

PATCH    – обновление фрагмента ресурса

DELETE   – удаление ресурса
    

Коды ответа


1xx      – информационные

2xx      – успех транзакции

3xx      – перенаправления

4xx      – ошибки клиента

5xx      – ошибки сервера
    

Коды ответа


200 Ok
201 Created
204 No content

301 Moved Permanently
302 Moved Temporarily

400 Bad request
401 Unauthorized
403 Forbidden
404 Not found
409 Conflict

500 Internal Server Error
504 Gateway Timeout

    

418 I'm a teapot

April Fools' Day Request for Comments

Hyper Text Coffee Pot Control Protocol

Заголовки запроса


Accept: application/json
Accept-Encoding: gzip, deflate
Accept-Language: ru, en;q=0.7
User-Agent: HTTPie/0.9.3
    

Заголовки ответа

Content-Language: ru
Content-Length: 67
Content-Type: application/json; charset=utf-8
X-Powered-By: Express
    

httpie (CLI)

Postman (Chromium)

Stateless

Каждый следующий HTTP запрос не может опираться на отправленные данные предыдущего запроса

HTTP не хранит промежуточное состояние клиента, всё состояние целиком описывается в каждом запросе

Компрессия

// Request
Accept-Encoding: gzip, deflate
    
// Response
Content-Encoding: gzip
    

Keep-alive

Использование одного TCP-соединения для многократных HTTP-запросов

// Request (HTTP 1.0)
Connection: keep-alive
    
// Response (HTTP 1.0)
Connection: keep-alive
    
// Request (HTTP 1.1)
    
// Request (HTTP 1.1)
Connection: close
    

Кеширование: Политика


HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, no-cache
    

private – кеширование только у конечного клиента,
public – и на промежуточных серверах (CDN)

max-age – время жизни кеша в секундах

no-cache – каждый раз проверяем не изменился ли,
no-store – каждый раз запрашиваем

Кеширование: Инвалидация


HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, no-cache
Last-modified: Wed, 15 Nov 1995 04:58:08 GMT
    

GET /index.css HTTP/1.1
If-Modified-Since: Wed, 15 Nov 1995 04:58:08 GMT
    

Last-modified === If-Modified-Since


HTTP/1.1 304 Not Modified
    

Кеширование: Инвалидация


HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, no-cache
ETag: d1d3c5c4cdb2568785ba1a366b7fb048
    

GET /index.css HTTP/1.1
Host: urfu2015-notes.surge.sh
If-None-Match: d1d3c5c4cdb2568785ba1a366b7fb048
    

ETag === If-None-Match


HTTP/1.1 304 Not Modified
    

REST

REST

Архитектурный стиль, позволяющий сделать сетевое взаимодействие удобнее, прозрачнее и стандартизованнее

Рой Филдинг

Architectural Styles and the Design of Network-based Software Architectures

Принципы

  • Client-Server
  • Stateless
  • Cacheable
  • Layered
  • Uniform Interface

REST (REpresentational State Transfer)


PUT /notes/films HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Host: localhost:5000

{
    "title": "Films",
    "description": "Films to watch"
}
    

Ресурсы

Use path, not query

/api?type=notes&id=films
/notes/films
        

Use plurals, not singles

/note/films
/notes/films
        

Use only nouns, not verbs

POST /notes/add
POST /notes
        

Ресурсы

Avoid verbiage, use plurals

/note_list
/notes
        

Use lowercase

/pullRequests
/pull-requests
/pulls
        

Use nesting

/comments?note_id=films
/notes/films/comments
        

GET

Получает состояние ресурса в одном из представлений (JSON, XML, HTML)

GET /notes
GET /notes/films
GET /notes/films/fav
GET /notes?limit=10
    
200 Ok

404 Not found
400 Bad request /notes?limit=muahahaha
    

Не модифицирует ресурс!

POST

Создаёт новый ресурс с начальным состоянием, когда мы не знаем его ID

POST /notes
    
201 Created

409 Conflict
    

PUT

Создаёт новый ресурс с начальным состоянием, когда мы знаем его ID

PUT /notes/films
PUT /notes/films/fav
    
200 Ok
204 No content
    

PUT

Обновляет состояние существующего ресурса целиком

PUT /notes/films
PUT /notes/films/fav
    
200 Ok
204 No content

404 Not found
    

DELETE

Удаляет существующий ресурс

DELETE /notes/films
DELETE /notes/films/fav
    
200 Ok
204 No content

404 Not found
    

PATCH

Обновляет состояние существующего ресурса частично

PATCH /notes/films
    
200 Ok
204 No content

404 Not found
    

Дополнительные коды

401 Unauthorized
403 Forbidden
405 Method not allowed

500 Internal Server Error
    
POST /notes/films
PUT /notes/films
    

Связность


POST /notes HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Host: localhost:5000

{
    "title": "Films",
    "description": "Films to watch"
}
    

HTTP/1.1 201 Created
Location: /notes/films
    

Связность


GET / HTTP/1.1
Host: api.github.com
    

HTTP/1.1 200 Ok

{
    current_user_url: "https://api.github.com/user",
    gists_url: "https://api.github.com/gists{/gist_id}"
}
    

Hypertext Application Language


GET /notes HTTP/1.1
    
HTTP/1.1 200 Ok
Accept: application/hal+json

{
    "_links": {
        "self": { "href": "/notes" },
        "next": { "href": "/notes?page=2" },
        "find": { "href": "/notes/{?id}", "templated": true }
    }
}
    

HAL spec

Hypertext Application Language

HTTP/1.1 200 Ok
Accept: application/hal+json

{
    "_embedded": {
        "notes": [{
            "_links": {
                "self": { "href": "/notes/films" }
            },
            "title": "Films",
            "description": "Films to watch"
        }, {
            "_links": {
                "self": { "href": "/notes/books" }
            },
            "title": "Books",
            "description": "Books to read"
        }]
    }
}
    

HAL spec

Идемпотентность

Один и тот же запрос приводит к одному и тому же результату (состоянию)

GETда
POSTнет
PUTда
DELETEда
PATCHнет
    

Версионирование

Новая версия каждый раз, когда ломаем обратную совместимость


              https://developer.github.com/v3/
    

XMLHttpRequest

XMLHttpRequest


var xhr = new XMLHttpRequest();

xhr.open('GET', '/notes');
xhr.send();
    

xhr.open('GET', '/notes', false);
    

xhr.open('GET', '/notes', false, user, password);
    

xhr.abort();
    

xhr.onreadystatechange


xhr.onreadystatechange = function() {
    if (xhr.readyState !== 4) {
        return;
    }

    if (xhr.status !== 200) {
        console.log(xhr.status + ': ' + xhr.statusText);
    } else {
        console.log(xhr.responseText);
    }

}
    

xhr.readyState


UNSENT              0  начальное состояние
OPENED              1  вызван open
HEADERS_RECEIVED    2  получены заголовки
LOADING             3  загружается тело
DONE                4  запрос завершён
    

                0123 → … → 34
    

xhr.setRequestHeader


xhr.setRequestHeader('Content-Type', 'application/json');
    

xhr.getResponseHeader


xhr.getResponseHeader('Content-Type');
    

xhr.getAllResponseHeaders();

// Cache-Control: max-age=31536000
// Content-Type: text/html
    

xhr.timeout


xhr.timeout = 30000; // 30s
    

xhr.ontimeout = function () {
  console.log('Try again later');
}
    

FormData


<form name="notes">
  <input name="title" value="">
  <input name="description" value="">
</form>
    

var formData = new FormData(document.forms.notes);

formData.append("createdAt", new Date());

var xhr = new XMLHttpRequest();
xhr.open("POST", "/notes");
xhr.send(formData); // multipart/form-data.
    

FormData


<form name="notes">
  <input name="title" value="">
  <input name="description" value="">
  <input type="image" value="">
</form>
    

var image = document.querySelector('input[type="image"]')

formData.append("picture", image.files[0]);

var xhr = new XMLHttpRequest();
xhr.open("POST", "/notes");
xhr.send(formData); // multipart/form-data.
    

Download progress


xhr.onprogress = function(event) { // Every 50 ms
  console.log(event.loaded); // Bytes
  console.log(event.total); // Content-Length || 0
}
    

Upload progress


xhr.upload.onprogress = function(event) {}
xhr.upload.onload = function() {}
xhr.upload.onerror = function() {}
    

CORS

Cross Origin Resurce Sharing

Same origin policy

Origin = scheme + host + port

CORS: Простые запросы

Простые методы
GET, POST, HEAD, DELETE

Простые заголовки
Accept
Accept-Language
Content-Language
Content-Type
Cookie

CORS: Простые запросы

GET /notes HTTP/1.1
Host: awesomenotes.com
Origin: http://notesdashboard.ru
    
HTTP/1.1 200 Ok
Content-Type: text/html
Access-Control-Allow-Origin: http://notesdashboard.ru
    
HTTP/1.1 200 Ok
Content-Type: text/html
Access-Control-Allow-Origin: *
    

CORS: Сложные запросы

PUT /notes/films HTTP/1.1
Host: awesomenotes.com
Origin: http://notesdashboard.ru
    
OPTIONS /notes/films HTTP/1.1
Host: awesomenotes.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept-encoding
    

preflight

HTTP/1.1 204 No content
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: accept-language, origin, accept-encoding
Access-Control-Max-Age: 60000
    

CORS

Same-origin policy

HTTP access control (CORS)

Cross-Origin Resource Sharing

Fetch


let promise = fetch(url[, options]);
    

Fetch


{
    methtod: 'POST',
    headers: {
        'Accept': 'application/json'
    },
    body: new FormData(), // 'foo=bar&lorem=ipsum'
    mode: 'same-origin', // cors
    cache: 'no-cache', // default, no-store, reload, force-cache
}
    

Fetch


fetch('/notes')
  .then(function(response) {
    console.log(response.headers.get('Content-Type'));
    // text/html

    console.log(response.status);
    // 200

    return response.text();
   })
  .catch(console.error);
    

Fetch

response.arrayBuffer()
response.blob()
response.formData()
response.json()
response.text()
    
response.url
response.statusText
response.type // basic, cors
    
fetch.spec.whatwg.org

Fetch

github/fetch

caniuse.com

HTTP2

Binary

Binary

Binary

Быстрее парсинг

Меньше размер пакета

Разделение заголовка и данных на фреймы

Streams and Multiplexing

Одно TCP-соединение

Bi-directional

Streams

Параллельные запросы без блокировки

Параллельные ответы без блокировки

Конкатенация, спрайты и шардинг доменов уходят в прошлое

Prioritization

Prioritization

  • Клиент уведомляет о приоритетах,
    сервер строит дерево приоритетов
  • Веса приоритетов и зависимости
    могут динамически меняться
  • Могут зависеть от предыдущих визитов

Prioritization

Chromium – weight based


                        HTML	256
                        CSS     220
                        scripts	183
                        images	110
    

Firefox – dependencies based

Header compression

Header compression

Header compression

Меньше размер пакета

Устраняет дублирование данных в потоке

Нивелируем влияние TCP slow-start

HTTP2 в браузерах

HTTP2 в nginx


                   2015-09-22 nginx-1.9.5

HTTP2 в node.js

indutny/node-spdy

molnarg/node-http2

HTTP

High Performance Browser Networking

What Web Developer Should Know About HTTP

HTTP The Definitive Guide

HTTP caching

REST

REST API Tutorial

Web API Design

XMLHttpRequest, WebSocket, SSE

learn.javascript.ru/ajax