Клиент-Сервер






Зубаиров Тимур

План

  1. HTTP
  2. Протоколы
    • REST
    • RPC
    • GraphQl
  3. Инструменты на клиенте
  4. CORS
  5. Websocket

HTTP

HyperText Transfer Protocol

HTTP

Ресурс


/static/img.png

/static/script.js

/notes

/notes/33

/notes/33/comments
    

Адрес ресурса - URI

(Uniform Resource Identifier)



http://awesomenotes.com:5000/notes?limit=10&sortBy=name
⇡         ⇡              ⇡    ⇡        ⇡
scheme   host           port path     query
    

Адрес ресурса - URI

(Uniform Resource Identifier)



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

Адрес ресурса - URI

(Uniform Resource Identifier)



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

Адрес ресурса - URI

(Uniform Resource Identifier)



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

Адрес ресурса - URI

(Uniform Resource Identifier)



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

Адрес ресурса - URI

(Uniform Resource Identifier)



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"
}
    

Пользовательские заголовки (Custom headers)

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

    

418 I'm a teapot

Hyper Text Coffee Pot Control Protocol

451 Unavailable For Legal Reasons

451 градус по Фаренгейту

Stateless

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

Сжатие данных


        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
    

Проблемы HTTP 1.1

  1. Head-of-Line blocking
  2. Ограниченное количество параллельных запросов (6 запросов)
  3. Повторяющийся блок заголовков

HTTP/2

  1. Head-of-Line blocking
    Multiplexing
  2. Ограниченное количество параллельных потоков
    Multiplexing
  3. Повторяющийся блок заголовков
    Сжатие заголовков (HPACK)
  4. Бинарный
  5. Server push
  6. Head-of-Line blocking на уровне TCP

HTTP/3 (HTTP/2 поверх QUIC)

  1. Head-of-Line blocking на уровне TCP
    UDP
  2. Multiplexing на уровень QUIC
  3. HPACK -> QPACK

План

  1. HTTP
  2. Протоколы
    • REST
    • RPC
    • GraphQl
  3. Инструменты на клиенте
  4. CORS
  5. Websocket

REST

REpresentational State Transfer

Архитектурный стиль

Рой Филдинг

Architectural Styles and the Design of Network-based Software Architectures

REST (основные принципы)

  1. Клиент-сервер
  2. Stateless
  3. Механизм кэширования
  4. Идентификация ресурсов
  5. Самодостаточные сообщения
  6. HATEOAS (связность ресурсов)

REST

Отлично ложится на http

  1. Клиент-сервер
  2. Stateless
  3. Механизм кеширования
  4. Идентификация ресурсов
  5. Самодостаточные сообщения
  6. HATEOAS (связность ресурсов)

GET

Получает состояние ресурса

GET /notes
GET /notes/33
GET /notes/33/comments
GET /notes?limit=10
    
200 Ok

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

POST

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

POST /notes HTTP/1.1
Content-Type: application/json

{
    "name": "films",
    "text": "..."
}
    
201 Created

409 Conflict
    

PUT

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

PUT /notes/33 HTTP/1.1
Content-Type: application/json

{
    "name": "films",
    "text": "..."
}
    
200 Ok
204 No content
    

PUT

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

PUT /notes/33 HTTP/1.1
Content-Type: application/json

{
    "name": "films",
    "text": "..."
}
    
200 Ok
204 No content
404 Not found
    

DELETE

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

DELETE /notes/33
    
200 Ok
204 No content

404 Not found
    

PATCH

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

PATCH /notes/33
    
200 Ok
204 No content

404 Not found
    

HEAD

Запрашивает заголовки, чтобы проверить существование ресурса

HEAD /notes/33
    
200 Ok

404 Not found
    

OPTIONS

Запрашивает правила взаимодействия, например, доступные методы

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&note_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&note_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}"
}

Hypertext Application Language

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

REST API Tutorial

Минусы REST

  • Не протокол
  • Ограниченный словарь
  • Размазанные данные
  • Привязка к HTTP
  • Один запрос - один ресурс (нет batching-a)

RPC

Remote Procedure Call

jsonrpc


Запрос:
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "findNote",
    "params": [33]
}

Ответ:
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "id": 33,
        "name": "films",
        "text": "..."
    }
}

jsonrpc

  • Протокол
  • Неограниченный словарь
  • Все данные в одном месте
  • Может использовать любой транспорт
  • Поддерживает batching

GraphQL

  • Самодокументируемый
  • Типизированный
  • Сервер присылает только запрошенные данные
  • Больше сложностей на сервере

GraphQL



Лекция

Официальный сайт

Документалка 😀

План

  1. HTTP
  2. Протоколы
    • REST
    • RPC
    • GraphQl
  3. Инструменты на клиенте
  4. CORS
  5. Websocket

XMLHttpRequest

XMLHttpRequest

const xhr = new XMLHttpRequest();

xhr.open('POST', '/notes');

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(note));

xhr.abort();

xhr.onreadystatechange

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

    if (xhr.status === 200) {
        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'); // text/html
    

xhr.timeout

xhr.timeout = 30000; // 30s

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

FormData

<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
    

FormData

<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
};

Using XMLHttpRequest

Using FormData Objects

Fetch

Fetch

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

options

{
    methtod: 'POST',
    headers: {
        'Accept': 'application/json'
    },
    body: new FormData(),
    mode: 'same-origin', // cors, no-cors
    cache: 'no-cache'
}
    

Promise

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);
    });
    

Нет удобной возможности следить за прогрессом

Возможность отменить запрос
есть не во всех браузерах

This API is so Fetching!

План

  1. HTTP
  2. Протоколы
    • REST
    • RPC
    • GraphQl
  3. Инструменты на клиенте
  4. CORS
  5. Websocket

Cross Origin Resource Sharing

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
    

Same-origin policy

HTTP access control (CORS)

Cross-Origin Resource Sharing

WebSockets

Протокол двунаправленного соединения поверх 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 есть из коробки

Writing WebSocket servers

High Performance Browser Networking
Ilya Grigorik

Клиент-Сервер 2

gRPC

protobuf

GraphQL