/static/img.png
/static/script.js
/notes
/notes/33
/notes/33/comments
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
https://en.wikipedia.org/wiki/Main_page
ftp://64.233.165.101:3000/cats/grumpycat.html
file:///users/ivan/example.pdf?page=33
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
https://en.wikipedia.org/wiki/Main_page
ftp://64.233.165.101:3000/cats/grumpycat.html
file:///users/ivan/example.pdf?page=33
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
https://en.wikipedia.org/wiki/Main_page
ftp://64.233.165.101:3000/cats/grumpycat.html
file:///users/ivan/example.pdf?page=33
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
https://en.wikipedia.org/wiki/Main_page
ftp://64.233.165.101:3000/cats/grumpycat.html
file:///users/ivan/example.pdf?page=33
http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡ ⇡ ⇡ ⇡ ⇡
scheme host port path query
https://en.wikipedia.org/wiki/Main_page
ftp://64.233.165.101:3000/cats/grumpycat.html
file:///users/ivan/example.pdf?page=33
POST /notes HTTP/1.1 ← Request line
Accept: application/json ← Headers
Content-Length: 35
Content-Type: application/json; charset=utf-8
Host: localhost:8080
User-Agent: HTTPie/0.9.3
← Empty line
{ ← Body
"name": "films",
"text": "Films to watch"
}
HTTP/1.1 200 OK ← Status line
Content-Length: 67 ← Headers
Content-Type: application/json; charset=utf-8
Date: Wed, 16 Mar 2016 14:32:18 GMT
X-Powered-By: Express
← Empty line
{ ← Body
"createdAt": 1458138738899,
"id": 33,
"name": "films",
"text": "Films to watch"
}
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,
"id": 33,
"name": "films",
"text": "Films to watch"
}
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,
"id": 33,
"name": "films",
"text": "Films to watch"
}
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,
"id": 33,
"name": "films",
"text": "Films to watch"
}
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,
"id": 33,
"name": "films",
"text": "Films to watch"
}
GET получение ресурса
POST создание ресурса
PUT обновление ресурса
PATCH частичное изменение ресурса
DELETE удаление ресурса
HEAD запрос заголовков ресурса
OPTIONS определение возможностей сервера для ресурса
1xx информационные
2xx успех транзакции
3xx перенаправления
4xx ошибки клиента
5xx ошибки сервера
101 Switch protocol
200 Ok
201 Created
204 No content
301 Moved Permanently
304 Not modified
400 Bad request
403 Forbidden
404 Not found
500 Internal Server Error
504 Gateway Timeout
451 градус по Фаренгейту
Сам не хранит состояние клиента между запросами, всё состояние целиком описывается в каждом запросе
3 145 728 байт → gzip → 777 096 байт (-75%)
Запрос:
GET /notes/33 HTTP/1.1
Accept-Encoding: gzip, br
Ответ:
HTTP/1.1 200 OK
Content-Encoding: gzip
Ответ:
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, no-cache
private | Закешируй только у конечного клиента (в браузере) |
public | Закешируй и на промежуточных серверах (на CDN) |
max-age | Закешируй на указанное количество секунд |
no-store | Не кешируй ресурс |
no-cache | Кешируй, но каждый раз проверяй не изменился ли ресурс |
«В программировании существует две проблемы: инвалидация кеша, придумывать названия переменным и ошибка на единицу»
Ответ:
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, no-cache
ETag: d1d3c5c4cdb2568785ba1a366b7fb048
Запрос:
GET /index.css HTTP/1.1
If-None-Match: d1d3c5c4cdb2568785ba1a366b7fb048
Ответ:
HTTP/1.1 304 Not Modified
Ответ:
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
Ответ:
HTTP/1.1 304 Not Modified
Архитектурный стиль
Рой Филдинг
Architectural Styles and the Design of Network-based Software Architectures
Отлично ложится на http
Получает состояние ресурса
GET /notes
GET /notes/33
GET /notes/33/comments
GET /notes?limit=10
200 Ok
404 Not found
400 Bad request /notes?limit=ten
Создаёт новый ресурс с начальным состоянием, когда мы не знаем его ID
POST /notes HTTP/1.1
Content-Type: application/json
{
"name": "films",
"text": "..."
}
201 Created
409 Conflict
Создаёт новый ресурс с начальным состоянием, когда мы знаем его ID
PUT /notes/33 HTTP/1.1
Content-Type: application/json
{
"name": "films",
"text": "..."
}
200 Ok
204 No content
Обновляет состояние существующего ресурса целиком
PUT /notes/33 HTTP/1.1
Content-Type: application/json
{
"name": "films",
"text": "..."
}
200 Ok
204 No content
404 Not found
Удаляет существующий ресурс
DELETE /notes/33
200 Ok
204 No content
404 Not found
Обновляет состояние существующего ресурса частично
PATCH /notes/33
200 Ok
204 No content
404 Not found
Запрашивает заголовки, чтобы проверить существование ресурса
HEAD /notes/33
200 Ok
404 Not found
Запрашивает правила взаимодействия, например, доступные методы
OPTIONS /notes
204 No content
Allow: OPTIONS, GET, HEAD
POST /notes
405 Method not allowed
REST best practices
Используйте path, а не query
/api?type=message&message_id=45¬e_id=33
/notes/33/messages/45
Используйте множественное число, а не идинственное
/note
/note/33
/notes
/notes/33
Используйте только существительные, не глаголы
POST /notes/add
POST /notes
Избегайте избыточности
/note_list
/notes
/note_list/33
/notes/33
Используйте kebab-case или snake_case для разделения слов
/pullRequests
/pull-requests
/pull_requests
Используйте вложенность
/messages&message_id=45¬e_id=33
/notes/33/messages/45
Безопасный запрос не меняет состояния приложения
Идемпотентный запрос - запрос эффект которого от многократного выполнения равен эффекту от однократного выполнения
GET – да (безопасный)
OPTIONS – да (безопасный)
HEAD – да (безопасный)
POST – нет
PUT – да
DELETE – да
PATCH – нет
Связанность
POST /notes HTTP/1.1
Content-Type: application/json
{
"name": "films",
"text": "..."
}
HTTP/1.1 201 Created
Location: /notes/33
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}"
}
GET /notes HTTP/1.1
HTTP/1.1 200 Ok
Content-Type: application/hal+json
{
"notes": [
{ "name": "films" },
{ "name": "games" }
],
"_links": {
"self": { "href": "/notes" },
"next": { "href": "/notes?page=2" },
"find": { "href": "/notes/{?id}", "templated": true }
}
}
Web API Design
Brian Mulloy
Remote Procedure Call
Запрос:
{
"jsonrpc": "2.0",
"id": 1,
"method": "findNote",
"params": [33]
}
Ответ:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": 33,
"name": "films",
"text": "..."
}
}
const xhr = new XMLHttpRequest();
xhr.open('POST', '/notes');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(note));
xhr.abort();
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status === 200) {
console.log(xhr.responseText);
}
}
UNSENT 0 начальное состояние
OPENED 1 вызван open
HEADERS_RECEIVED 2 получены заголовки
LOADING 3 загружается тело
DONE 4 запрос завершён
0 → 1 → 2 → 3 → … → 3 → 4
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.getResponseHeader('Content-Type'); // text/html
xhr.timeout = 30000; // 30s
xhr.ontimeout = () => {
console.log('Try again later');
}
<form name="notes">
<input name="name">
<input name="text">
</form>
const xhr = new XMLHttpRequest();
const formData = new FormData(document.forms.notes);
formData.append('hiddenField', 'hiddenValue');
xhr.open('POST', '/notes');
xhr.send(formData); // Content-type: multipart/form-data
<form name="notes">
<input name="name">
<input name="text">
<input type="file">
</form>
const xhr = new XMLHttpRequest();
const notes = document.forms.notes;
const formData = new FormData(notes);
formData.append('file', notes.elements[3].file[0]);
xhr.open('POST', '/notes');
xhr.send(formData);
xhr.onprogress = event => { // Every 50 ms
console.log(event.loaded); // Bytes
console.log(event.total); // Content-Length || 0
};
xhr.upload.onprogress = event => { // Every 50 ms
console.log(event.loaded); // Bytes
console.log(event.total); // Content-Length || 0
};
const promise = fetch(url[, options]);
{
methtod: 'POST',
headers: {
'Accept': 'application/json'
},
body: new FormData(),
mode: 'same-origin', // cors, no-cors
cache: 'no-cache'
}
fetch('/notes')
.then(response => {
response.headers.get('Content-Type'); // application/json
response.status; // 200
return response.json();
})
.then(notes => {
console.info(notes);
})
.catch(error => {
console.error(error);
});
Нет удобной возможности следить за прогрессом
Возможность отменить запрос
есть не во всех браузерах
Same-origin Policy (SOP)
Механизм ограничения доступа к ресурсам одного источника (origin) при запросах с другого
origin = scheme + host + port
https://awesomenotes.ru/notes/33
http://notesdashboard.ru:8080/dashboards/?limit=10
https://notesdashboard.ru:8080/dashboards/?limit=10
http://notesdashboard.ru:9000/dashboards/?limit=10
GET, POST, HEAD, DELETE
Accept
Accept-Language
Content-Language
Content-Type
Cookie
Запрос:
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: *
Запрос:
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
Ответ:
HTTP/1.1 204 No content
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: accept-encoding, origin, accept-language
Access-Control-Max-Age: 60000
Запрос:
PUT /notes/films HTTP/1.1
Host: awesomenotes.com
Origin: http://notesdashboard.ru
Протокол двунаправленного соединения поверх TCP
Инициализация начинается с обычного HTTP GET запроса
Запрос:
GET /socket HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: HjqL8dt/Sx6poK1PwQbtkg=
Ответ:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: IffTcaXvslUQ/19cSA4qNIUjHJc=
Sec-WebSocket-Accept: base64(sha1(Sec-WebSocket-Key + GUID))
GUID: 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
const http = require('http');
const ws = require('ws');
const httpServer = http.createServer();
const websocketServer = new ws.Server({ server: httpServer });
websocketServer.on('connection', socket => {
socket.send('Hello, Client!');
});
httpServer.listen(8080);
const socket = new WebSocket('ws://localhost:8080/socket');
socket.onmessage = messageEvent => {
console.log(messageEvent.data); // Hello, Client!
};
socket.onopen = () => {
socket.send('Hello, Server!');
};
socket.onopen = () => {
socket.send(document.forms[0].elements[0].files[0]);
};
socket.on('message', message => {
if (message instanceof Buffer) {
// ...
}
});
Подвержен проблеме Head-of-Line Blocking
Необходимо на уровне приложения реализовывать кеширование и другие механизмы, которые в HTTP есть из коробки
High Performance Browser Networking
Ilya Grigorik