- PVSM.RU - https://www.pvsm.ru -
*на самом деле мы напишем только прототип протокола.
Возможно, вы встречались с подобной ситуацией – сидите в любимом мессенджере, переписываетесь с друзьями, заходите в лифт/тоннель/вагон, и интернет вроде ещё ловит, но отправить ничего не получается? Или иногда ваш провайдер связи неправильно конфигурирует сеть и 50% пакетов пропадает, и тоже ничего не работает. Возможно, вы думали в этот момент — ну ведь можно же наверное как-то сделать, чтобы при плохой связи всё равно можно было отправить тот маленький кусочек текста, который вы хотите? Вы не одни.
В этой статье я расскажу про свою идею для реализации протокола на основе UDP, который может помочь в этой ситуации.
Когда у нас плохое (мобильное) соединение, то начинает теряться большой процент пакетов (или ходить с очень большой задержкой), и протокол TCP/IP может воспринимать это как сигнал о том, что сеть перегружена, и всё начинает работать оооочень медленно, если работает вообще. Не добавляет радости тот факт, что установление соединения (особенно TLS) требует отправки и приема нескольких пакетов, и даже небольшие потери сказываются на его работе очень плохо. Также часто требуется обращение к DNS перед тем, как установить соединение — ещё пара лишних пакетов.
Итого, проблемы типичного REST API, основанного на TCP/IP при плохом соединении:
Суммарно это означает, что только для соединения с сервером нам нужно послать 3-7 пакетов, и при высоком проценте потерь соединение может занять существенное количество времени, а мы ещё даже ничего не отправили.
Идея состоит в следующем: нам требуется всего-лишь отправить один UDP-пакет на заранее зашитый IP-адрес сервера с необходимыми данными авторизации и с текстом сообщения, и получить на него ответ. Все данные можно дополнительно зашифровать (этого в прототипе нет). Если ответ в течение секунды не пришел, то считаем, что запрос потерялся и пробуем отправить его заново. Сервер должен уметь убирать дубли сообщений, поэтому повторная отправка не должна создать проблем.
Ниже перечислены (далеко не все) вещи, которые нужно продумать перед тем, как использовать что-либо подобное в «боевых» условиях:
Напишем сервер, который будет отдавать ответ по UDP и присылать в ответе номер запроса, который к нему пришел (запрос выглядит как «request-ts текст сообщения»), а также timestamp получения ответа:
// Это Go.
// Обработка ошибок убрана для краткости
buf := make([]byte, maxUDPPacketSize)
// Начинаем слушать UDP
addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("0.0.0.0:%d", serverPort))
conn, _ := net.ListenUDP("udp", addr)
for {
// Читаем из UDP, нам обязательно нужен обратный адрес
n, uaddr, _ := conn.ReadFromUDP(buf)
req := string(buf[0:n])
parts := strings.SplitN(req, " ", 2)
// Высчитываем время на сервере по сравнению с временем клиента
curTs := time.Now().UnixNano()
clientTs, _ := strconv.Atoi(parts[0])
// Тут можно сходить в базу или куда-нибудь ещё и непосредственно сохранить сообщение
// Отправляем ответ
conn.WriteToUDP([]byte(fmt.Sprintf("%d %d", curTs, clientTs)), uaddr)
}
Теперь сложная часть — клиент. Мы будем отправлять сообщения по одному и дожидаться ответа сервера перед тем, как послать следующее. Слать будем текущий timestamp и кусок текста — timestamp будет служить идентификатором запроса.
// Создаем сокеты
addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", serverIP, serverPort))
conn, _ := net.DialUDP("udp", nil, addr)
// В UDP запись и чтение будут идти независимо, поэтому используем канал для удобства.
resCh := make(chan udpResult, 10)
go readResponse(conn, resCh)
for i := 0; i < numMessages; i++ {
requestID := time.Now().UnixNano()
send(conn, requestID, resCh)
}
Код функций:
func send(conn *net.UDPConn, requestID int64, resCh chan udpResult) {
for {
// Отправляем пакет до тех пор, пока не получим ответ на своё сообщение.
conn.Write([]byte(fmt.Sprintf("%d %s", requestID, testMessageText)))
if waitReply(requestID, time.After(time.Second), resCh) {
return
}
}
}
// Ждем свой ответ, или таймаут.
// В сети пакеты могут как теряться, так и дублироваться, поэтому нужно
// проверять, что присланный ответ действительно относится к тому сообщению,
// которое мы посылали.
func waitReply(requestID int64, timeout <-chan time.Time, resCh chan udpResult) (ok bool) {
for {
select {
case res := <-resCh:
if res.requestTs == requestID {
return true
}
case <-timeout:
return false
}
}
}
// Распарсенный ответ сервера
type udpResult struct {
serverTs int64
requestTs int64
}
// Функция для чтения ответа из соединения и засовывания ответа в канал.
func readResp(conn *net.UDPConn, resCh chan udpResult) {
buf := make([]byte, maxUDPPacketSize)
for {
n, _, _ := conn.ReadFromUDP(buf)
respStr := string(buf[0:n])
parts := strings.SplitN(respStr, " ", 2)
var res udpResult
res.serverTs, _ = strconv.ParseInt(parts[0], 10, 64)
res.requestTs, _ = strconv.ParseInt(parts[1], 10, 64)
resCh <- res
}
}
Также я реализовал то же самое на основе (более-менее) стандартного REST: с помощью HTTP POST посылаем те же requestTs и текст сообщения и дожидаемся ответа, после чего переходим к следующему. Обращение делалось по доменному имени, кеширование DNS в системе не запрещалось. HTTPS не использовался, чтобы сравнение было более честным (в прототипе шифрования нет). Таймаут был выставлен в 15 секунд: в TCP/IP уже есть перепосылки потерянных пакетов, а сильно больше 15 секунд пользователь, скорее всего, ждать не станет.
При тестировании прототипа измерялись следующие вещи (всё в миллисекундах):
Делалось 100 серий по 10 запросов — симулируем ситуацию, когда нужно послать буквально несколько сообщений и после этого уже становится доступен нормальный интернет (например Wi-Fi в метро, или 3G/LTE на улице).

(то же самое в формате CSV [4])
Вот, какие выводы можно сделать из получившихся результатов:
Другими словами, в соответствующих («плохих») условиях наш протокол будет работать намного лучше, особенно при посылке первого сообщения (часто это всё, что необходимо отправить).
Прототип выложен на github: github.com/YuriyNasretdinov/instant-im [5]. Не используйте его в продакшене!
Автор: youROCK
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ip/304078
Ссылки в тексте:
[1] Источник картинки: https://android.jlelse.eu/designing-android-apps-to-handle-slow-network-speed-dedc04119aac
[2] атакам усиления: https://www.us-cert.gov/ncas/alerts/TA14-017A
[3] Network Link Conditioner: https://nshipster.com/network-link-conditioner/
[4] то же самое в формате CSV: https://github.com/YuriyNasretdinov/instant-im/blob/master/results.csv
[5] github.com/YuriyNasretdinov/instant-im: https://github.com/YuriyNasretdinov/instant-im
[6] Источник: https://habr.com/post/435026/?utm_campaign=435026
Нажмите здесь для печати.