Как написать прокси-сервер в go (golang) с помощью tcp-соединений

Извиняюсь перед вами, если некоторые из этих вопросов могут быть очевидны для опытных сетевых программистов. Я исследовал и читал о кодировании в сети, и мне все еще не ясно, как это сделать.

Предположим, что я хочу написать прокси-сервер tcp (в go) с соединением между некоторым TCP-клиентом и некоторым TCP-сервером. Что-то вроде этого:

enter image description here

Сначала предположим, что это соединение полупостоянное (будет закрыто после долгого долгого времени), и мне нужно, чтобы данные поступали по порядку.

Идея, которую я хочу реализовать, заключается в следующем: всякий раз, когда я получаю запрос от клиента, я хочу перенаправить этот запрос на серверный сервер и ждать (и ничего не делать), пока сервер backend не ответит мне (прокси-сервер), а затем перенаправить этот ответ клиенту (предположим, что оба TCP-соединения будут поддерживаться в общем случае).

Есть одна основная проблема, я не уверен, как ее решить. Когда я перенаправляю запрос от прокси-сервера на сервер и получаю ответ, как узнать, когда сервер отправил мне всю необходимую мне информацию, если я заранее не знаю формат данных, отправляемых с сервера на прокси (т.е. я не знаю, соответствует ли ответ с сервера формой модель длины-длины, и я не знаю если `\ r\n\указывает, что конец сообщения формирует сервер). Мне сказали, что я должен предположить, что я получаю все данные от подключения к серверу всякий раз, когда мой размер чтения из tcp-соединения равен нулю или меньше, чем размер чтения, который я ожидал. Однако это мне не кажется правильным. Причина, по которой это может быть не совсем корректно, заключается в следующем:

Предположим, что сервер по какой-то причине записывает только его сокет по одному байту за раз, но общая длина ответа на "реальный" клиент намного намного дольше. Поэтому не так ли возможно, что когда прокси-сервер считывает подключенный к серверу сокет tcp, прокси-сервер считывает только один байт, и если он достаточно быстро выполняет цикл (чтобы прочитать, прежде чем он получит больше данных), тогда прочитайте нуль и неправильно что он получил все сообщение, которое клиент хотел получить?

Один из способов исправить это может состоять в том, чтобы ждать после каждого чтения из сокета, так что прокси-сервер не работает быстрее, чем получает байты. Причина, по которой я волнуюсь, предполагает, что есть сетевой раздел, и я больше не могу разговаривать с сервером. Однако он не отключается от меня достаточно долго, чтобы отключить TCP-соединение. Таким образом, не представляется возможным, что я снова пытаюсь читать из tcp-сокета на сервер (быстрее, чем я получаю данные) и читать ноль и неправильно заключать, что все его данные и затем отправить его пакет клиенту? (помните, что обещание, которое я хочу сохранить, заключается в том, что я отправляю только целые сообщения клиенту, когда пишу в клиентское соединение. Таким образом, его незаконное рассмотрение правильного поведения, если прокси-сервер идет, снова считывает соединение позже уже написал клиенту и отправляет пропавший фрагмент позже, возможно, во время ответа другого запроса).

Код, который я написал, находится в go-playground.

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

Скажите, что у нас есть чашка, а прокси-сервер выпивает половину чашки каждый раз, когда читает с сервера, но сервер ставит только одну чайную ложку за один раз. Таким образом, если прокси-напиток быстрее, чем он получает чайные ложки, он может достигнуть нуля слишком рано и сделать вывод, что его розетка пуста и что ее нормально двигаться дальше! Это неправильно, если мы хотим гарантировать, что мы отправляем полные сообщения каждый раз. Либо эта аналогия неверна, и некоторая "магия" из TCP заставляет ее работать, либо алгоритм, который предполагает, что сокет пуст, просто неверен.

A questionчто касается подобных проблем, здесь предлагается читать до EOF. Однако я не уверен, почему это было бы правильно. Чтение EOF означает, что я получил отступы? Является ли EOF отправленным каждый раз, когда кто-то записывает кусок байтов в сокет tcp (т.е. Я беспокоюсь, что если сервер записывает один байт за раз, он отправляет 1 EOF за каждый байты)? Однако EOF может быть частью "магии" того, как TCP-соединение действительно работает? Отправляет ли сообщение EOF соединение? Если это не метод, который я хочу использовать. Кроме того, я не могу контролировать, что может делать сервер (т.е. Я не знаю, как часто он хочет писать в сокет для отправки данных в прокси-сервер, однако разумно предположить, что он записывает в сокет с некоторыми "стандартными" /нормальный алгоритм записи в сокеты "). Я просто не уверен, что чтение до EOF из сокета с сервера верное. Почему? Когда я могу даже читать EOF? Являются ли EOF частью данных или находятся в заголовке TCP?

Кроме того, идея, что я написал о том, чтобы поставить wait только epsilon ниже тайм-аута, будет работать в худшем случае или только в среднем? Я также думал, что понял, что если вызов Wait() длиннее тайм-аута, то, если вы вернетесь к подключению tcp, и у него ничего нет, тогда его безопасно двигаться дальше. Однако, если у него ничего нет, и мы не знаем, что случилось с сервером, тогда мы будем тайм-аут. Таким образом, безопасно закрыть соединение (потому что тайм-аут сделал бы это так или иначе). Таким образом, я думаю, что если вызов Wait по крайней мере до тех пор, пока тайм-аут, эта процедура действительно работает! Что думают люди?

Я также заинтересован в ответе, который может оправдать, может быть, почему этот алгоритм работает в некоторых случаях. Например, я думал, даже если сервер только пишет байты за раз, если сценарий развертывания - это узкий центр обработки данных, то в среднем, поскольку задержки очень малы, а вызов ожидания почти наверняка достаточно, Этот алгоритм не прекрасен?

Кроме того, есть ли риск того, что код, который я написал, попал в "тупик"?

package main

import (
    "fmt"
    "net"
)

type Proxy struct {
    ServerConnection *net.TCPConn
    ClientConnection *net.TCPConn
}

func (p *Proxy) Proxy() {
    fmt.Println("Running proxy...")
    for {
        request := p.receiveRequestClient()
        p.sendClientRequestToServer(request)
        response := p.receiveResponseFromServer() //<--worried about this one.
        p.sendServerResponseToClient(response)
    }
}

func (p *Proxy) receiveRequestClient() (request []byte) {
    //assume this function is a black box and that it works.
    //maybe we know that the messages from the client always end in \r\n or they
    //they are length prefixed.
    return
}

func (p *Proxy) sendClientRequestToServer(request []byte) {
    //do
    bytesSent := 0
    bytesToSend := len(request)
    for bytesSent < bytesToSend {
        n, _ := p.ServerConnection.Write(request)
        bytesSent += n
    }
    return
}

// Intended behaviour: waits until ALL of the response from backend server is obtained.
// What it does though, assumes that if it reads zero, that the server has not yet
// written to the proxy and therefore waits. However, once the first byte has been read,
// keeps writting until it extracts all the data from the server and the socket is "empty".
// (Signaled by reading zero from the second loop)
func (p *Proxy) receiveResponseFromServer() (response []byte) {
    bytesRead, _ := p.ServerConnection.Read(response)
    for bytesRead == 0 {
        bytesRead, _ = p.ServerConnection.Read(response)
    }
    for bytesRead != 0 {
        n, _ := p.ServerConnection.Read(response)
        bytesRead += n
        //Wait(n) could solve it here?
    }
    return
}

func (p *Proxy) sendServerResponseToClient(response []byte) {
    bytesSent := 0
    bytesToSend := len(request)
    for bytesSent < bytesToSend {
        n, _ := p.ServerConnection.Write(request)
        bytesSent += n
    }
    return
}

func main() {
    proxy := &Proxy{}
    proxy.Proxy()
}

Ответ 1

Если вы работаете с конкретным протоколом более высокого уровня, нет никакого "сообщения" для чтения с клиента для ретрансляции на сервер. TCP - это протокол потока, и все, что вы можете сделать, это передавать байты взад и вперед.

Хорошей новостью является то, что это удивительно легко в go, и основной частью этого прокси будет:

go io.Copy(server, client)
io.Copy(client, server)

Это, очевидно, отсутствие обработки ошибок и не закрывается чисто, но четко показывает, как обрабатывается передача данных ядра.