TCP/IP proxy на Go

TCP/IP proxy на Go

18-10-2020 09:35:58
Я снова вернулся к любимой задаче для освоения новых языков. После написания движка для блога на Go, захотелось снова поразмять пальцы, болезный TCP/IP proxy/debugger теперь написан на Go.
Вкратце, TCP/IP proxy — это программа, которая умеет принимать соединения и «пробрасывать» их на указанный адрес. Попутно ведутся логи переданных данных. Это очень удобно при отладке различных самодельных сетевых протоколов.

В плане функциональности версия на Go, как и эрланговская, ведет три лога: двунаправленный шестнадцатеричный дамп и бинарные логи в обоих направлениях, «от» и «к» удаленному хосту. Питоновская версия бинарные логи не ведет.
Конечно, все многопоточно. И так как в Go параллельное программирование настолько просто (и безопасно), количество параллельных активностей для каждого соединения даже больше, чем в версии на Эрланге.

На Эрланге для каждого соединения работали следующие четыре потока:
двунаправленный дамп-логгер
два потока для двоичных логов принимаемых и посылаемых данных
главный поток, мультиплексирующий чтение из локального и удаленного сокетов

В версии на Go немного иначе:
двунаправленный дамп-логгер
два потока для двоичных логов принимаемых и посылаемых данных
два независимых потока для чтения из локального и удаленного сокета

Итого, 5.

В обоих случаях потоки чтения логируют данные, посылая сообщения потокам-логгерам. Конечно, нет никаких глупостей типа мьютексов или условных переменных. Проблемы согласования элегантно решаются через каналы Go.
Ниже привожу исходник. Он отличается от того, что в репозитории, наличием обильных комментариев. Для людей, не очень знакомых с Go, могут быть интересны некоторые моменты.



package main

import (
"flag"
"fmt"
"net"
"os"
"strings"
"time"
"encoding/hex"
"runtime"
)

var (
host *string = flag.String("host", "", "target host or address")
port *string = flag.String("port", "0", "target port")
listen_port *string = flag.String("listen_port", "0", "listen port")
)

func die(format string, v ...interface{}) {
os.Stderr.WriteString(fmt.Sprintf(format+"\n", v...))
os.Exit(1)
}

// Данная функция реализует поток для двунаправленного дампа.
func connection_logger(data chan []byte, conn_n int, local_info, remote_info string) {
log_name := fmt.Sprintf("log-%s-%04d-%s-%s.log", format_time(time.Now()),
conn_n, local_info, remote_info)
logger_loop(data, log_name)
}

// Данная функция реализует двоичный лог.
func binary_logger(data chan []byte, conn_n int, peer string) {
log_name := fmt.Sprintf("log-binary-%s-%04d-%s.log", format_time(time.Now()),
conn_n, peer)
logger_loop(data, log_name)
}

// Данная функция реализует поток логирования. Создает лог-файл и начинает
// принимает сообщения. Каждое сообщение - это кусок данных для помещения
// в лог. Если пришли пустые данные - выходим.
//
func logger_loop(data chan []byte, log_name string) {
f, err := os.Create(log_name)
if err != nil {
die("Unable to create file %s, %v\n", log_name, err)
}
defer f.Close() // Гарантируем закрытие файла в случае падения.
for {
b := <-data
if len(b) == 0 {
break
}
f.Write(b)
f.Sync() // На всякий случай flush'имся.
}
}

func format_time(t time.Time) string {
return t.Format("2006.01.02-15.04.05")
}

func printable_addr(a net.Addr) string {
return strings.Replace(a.String(), ":", "-", -1)
}

// Структура, в которой передаются параметры соединения. Объединено, чтобы
// не таскать много параметров.
type Channel struct {
from, to net.Conn
logger, binary_logger chan []byte
ack chan bool
}

// Функция, "качающая" данные из одного сокета и передающая их в другой.
// Попутно ведется логирование.
func pass_through(c *Channel) {
from_peer := printable_addr(c.from.LocalAddr())
to_peer := printable_addr(c.to.LocalAddr())

b := make([]byte, 10240)
offset := 0
packet_n := 0
for {
n, err := c.from.Read(b)
if err != nil {
c.logger <- []byte(fmt.Sprintf("Disconnected from %s\n", from_peer))
break
}
if n > 0 {
// Если что-то пришло, то логируем и пересылаем на выход.
c.logger <- []byte(fmt.Sprintf("Received (#%d, %08X) %d bytes from %s\n",
packet_n, offset, n, from_peer))
// Это все, что нужно для преобразования в hex-дамп. Удобно, не так ли?
c.logger <- []byte(hex.Dump(b[:n]))
c.binary_logger <- b[:n]
c.to.Write(b[:n])
c.logger <- []byte(fmt.Sprintf("Sent (#%d) to %s\n", packet_n, to_peer))
offset += n
packet_n += 1
}
}
c.from.Close()
c.to.Close()
c.ack <- true // Посылаем сообщение в главный поток, что мы закончили.
}

// Данная функция обслуживает соединение. Запускает необходимые потоки и ждет
// их завершения.
func process_connection(local net.Conn, conn_n int, target string) {
// Соединяемся к удаленном сокету, куда будем пересылать данные.
remote, err := net.Dial("tcp", target)
if err != nil {
fmt.Printf("Unable to connect to %s, %v\n", target, err)
}

local_info := printable_addr(remote.LocalAddr())
remote_info := printable_addr(remote.RemoteAddr())

// Засекаем начальное время.
started := time.Now()

// Создаем каналы для обмена с логгерами.
logger := make(chan []byte)
from_logger := make(chan []byte)
to_logger := make(chan []byte)

// Канал для получения подтверждений от качающих потоков.
ack := make(chan bool)

// Запускаем логгеры.
go connection_logger(logger, conn_n, local_info, remote_info)
go binary_logger(from_logger, conn_n, local_info)
go binary_logger(to_logger, conn_n, remote_info)

logger <- []byte(fmt.Sprintf("Connected to %s at %s\n", target,
format_time(started)))

// Запускаем качающие потоки.
go pass_through(&Channel{remote, local, logger, to_logger, ack})
go pass_through(&Channel{local, remote, logger, from_logger, ack})

// Ждем подтверждения об их завершении.
<-ack
<-ack

// Вычисляем длительность соединения.
finished := time.Now()
duration := finished.Sub(started)
logger <- []byte(fmt.Sprintf("Finished at %s, duration %s\n",
format_time(started), duration.String()))

// Посылаем логгерам команды закругляться. Мы тут не ждем от них
// подтверждения, так как они и так завершатся рано или поздно, а они нам
// более не нужны.
logger <- []byte{}
from_logger <- []byte{}
to_logger <- []byte{}
}

func main() {
// Просим Go использовать все имеющиеся в системе процессоры.
runtime.GOMAXPROCS(runtime.NumCPU())
// Разбираем командную строку (несложно, не правда ли?)
flag.Parse()
if flag.NFlag() != 3 {
fmt.Printf("usage: gotcpspy -host target_host -port target_port -listen_post=local_port\n")
flag.PrintDefaults()
os.Exit(1)
}
target := net.JoinHostPort(*host, *port)
fmt.Printf("Start listening on port %s and forwarding data to %s\n",
*listen_port, target)

ln, err := net.Listen("tcp", ":"+*listen_port)
if err != nil {
fmt.Printf("Unable to start listener, %v\n", err)
os.Exit(1)
}
conn_n := 1
for {
// Ждем новых соединений.
if conn, err := ln.Accept(); err == nil {
// Запускаем поток обработки соединения.
go process_connection(conn, conn_n, target)
conn_n += 1
} else {
fmt.Printf("Accept failed, %v\n", err)
}
}
}

Повторюсь, каждое соединения обслуживается пятью потоками. И сделал я это не ради прикола. Просто мне показалось, что логически есть явно независимые подзадачи, которые было бы логично запустить параллельно. Если б я писал все на C++/boost, я б скорее всего замутил все одном потоке для каждого соединения (а может быть и вся программа была бы чисто однопотоковой через какие-нибудь изощренные библиотеки мультиплексирования), и не исключено, что на C++ в итоге еще и работало бы быстрее, несмотря на один поток. Но я хочу сказать не об этом. Go подталкивает на многопоточное программирование (а не отталкивает, как C++, даже на стероидах нового стандарта). Так или иначе, будут задачи, где удобная многопоточность станет ключевым фактором.

Запустить можно так (требуется как минимум Go релиз 1):

go run gotcpspy.go -host pop.yandex.ru -port 110 -local_port 8080

Выведется:

Start listening on port 8080 and forwarding data to pop.yandex.ru:110

Затем, если в другом окне ввести:

telnet localhost 8080

и ввести, например, "USER test" "ENTER" и "PASS none" "ENTER", то будут созданы три лога (дата в имени будет, конечно, другая).

Общий лог log-2012.04.20-19.55.17-0001-192.168.1.41-49544-213.180.204.37-110.log:

Connected to pop.yandex.ru:110 at 2012.04.20-19.55.17
Received (#0, 00000000) 38 bytes from 192.168.1.41-49544
00000000 2b 4f 4b 20 50 4f 50 20 59 61 21 20 76 31 2e 30 |+OK POP Ya! v1.0|
00000010 2e 30 6e 61 40 32 36 20 48 74 6a 4a 69 74 63 50 |.0na@26 HtjJitcP|
00000020 52 75 51 31 0d 0a |RuQ1..|
Sent (#0) to [--1]-8080
Received (#0, 00000000) 11 bytes from [--1]-8080
00000000 55 53 45 52 20 74 65 73 74 0d 0a |USER test..|
Sent (#0) to 192.168.1.41-49544
Received (#1, 00000026) 23 bytes from 192.168.1.41-49544
00000000 2b 4f 4b 20 70 61 73 73 77 6f 72 64 2c 20 70 6c |+OK password, pl|
00000010 65 61 73 65 2e 0d 0a |ease...|
Sent (#1) to [--1]-8080
Received (#1, 0000000B) 11 bytes from [--1]-8080
00000000 50 41 53 53 20 6e 6f 6e 65 0d 0a |PASS none..|
Sent (#1) to 192.168.1.41-49544
Received (#2, 0000003D) 72 bytes from 192.168.1.41-49544
00000000 2d 45 52 52 20 5b 41 55 54 48 5d 20 6c 6f 67 69 |-ERR [AUTH] logi|
00000010 6e 20 66 61 69 6c 75 72 65 20 6f 72 20 50 4f 50 |n failure or POP|
00000020 33 20 64 69 73 61 62 6c 65 64 2c 20 74 72 79 20 |3 disabled, try |
00000030 6c 61 74 65 72 2e 20 73 63 3d 48 74 6a 4a 69 74 |later. sc=HtjJit|
00000040 63 50 52 75 51 31 0d 0a |cPRuQ1..|
Sent (#2) to [--1]-8080
Disconnected from 192.168.1.41-49544
Disconnected from [--1]-8080
Finished at 2012.04.20-19.55.17, duration 5.253979s

Двоичный лог исходящих данных log-binary-2012.04.20-19.55.17-0001-192.168.1.41-49544.log:

USER test
PASS none

Двоичный лог входящих данных log-binary-2012.04.20-19.55.17-0001-213.180.204.37-110.log:

+OK POP Ya! v1.0.0na@26 HtjJitcPRuQ1
+OK password, please.
-ERR [AUTH] login failure or POP3 disabled, try later. sc=HtjJitcPRuQ1

Теперь измерим производительность. Прокачаем файл напрямую, а потом через эту программу.

Качаем напрямую (файл размером около 72MB):

time wget http://www.erlang.org/download/otp_src_R15B01.tar.gz
...
Saving to: `otp_src_R15B01.tar.gz'
...
real 1m2.819s

Теперь закачаем через программу, предварительно запустив ее:

go run gotcpspy.go -host=www.erlang.org -port=80 -listen_port=8080

Качаем:

time wget http://localhost:8080/download/otp_src_R15B01.tar.gz
...
Saving to: `otp_src_R15B01.tar.gz.1'
...
real 0m56.209s

На всякий случай, можно сравнить результаты:

diff otp_src_R15B01.tar.gz otp_src_R15B01.tar.gz.1

У меня файлы одинаковые, значит все работает верно.

Теперь время. Я повторял эксперимент несколько раз (на Mac Air), и, что удивительно, закачка через программу всегда была не то, чтобы медленнее, а даже немного быстрее. Например, напрямую — 1m2.819s, через программу — 0m56.209s. Единственное объяснение, что wget возможно работает в один поток, а программа принимает данные из локального и удаленного сокета в два потока, что может давать небольшое ускорение. Но, разница все равно минимальна, и возможно на другой машине или сети ее будет не видно, но главное, что работает как минимум не медленнее, чем напрямую, несмотря на создание в процессе передачи весьма массивных логов.

Итак, пока среди трех вариантов такой программы, на Питоне, Эрланге и Go, последняя мне нравится больше всего.

Как мне показалось, неплохой эксперимент с параллельностью в Go.


САМОЕ ОБСУЖДАЕМОЕ

...
Титан в воздухе
18-10-2020 09:35:58
Это не самолет – это Мрия!!!...
...
Лучшие бюджетные ноутбуки на 2020 год
18-10-2020 09:35:59
Как выбрать лучший ноутбук в 2020 году...
...
Технологические тенденции в 2020 году
18-10-2020 09:35:59
Технологи будущего уже сегодня...
...
Самостоятельная поездка автомобиля под управлением искусственного интеллекта
18-10-2020 09:35:58
Самый далекий перезд без водителя в истории искуственного интеллекта....
...
Ford Bronco наконец дебютирует 9 июля
18-10-2020 09:35:58
После задержек, связанных с коронавирусом, у долгожданной Бронко Форда официально объявлена ​​дата....
...
Tesla Semi готова к «массовому производству»
18-10-2020 09:35:58
Но генеральный директор не указал точные сроки начала производства электрического грузовика....
...
25 самых продаваемых легковых автомобилей, грузовиков и внедорожников 2020 года (пока)
18-10-2020 09:35:58
Хотя пандемия коронавируса привела к хаосу в продажах автомобилей, мы подсчитали рейтинг самых продаваемых в первом квартале....
...
Электрический Mustang
18-10-2020 09:35:58
Ford представляет электрический Mustang с «потрясающим» ускорением...
...
Cамые большие дизайнерские моменты 2018 года
18-10-2020 09:35:59
Мы попросили дизайнеров рассказать нам, что они считают самой важной вещью, которая произошла в отрасли в этом году....
...
Airtable
18-10-2020 09:35:59
Простая в использовании система управления реляционными базами данных...

НАШИ РЕКОМЕНДАЦИИ

Toyota Supra в 2021 году
Toyota Supra в 2021 году
Toyota Supra 2020 года не получит дооснащения, чтобы соответствовать увеличению мощности в 2021 году...
9 различных вариантов использования console log
9 различных вариантов использования console log
Каждый из нас использовал console.logдля отладки больше, чем нам хотелось бы признать....
Расслабление, снятие стресса и развитие сознания
Расслабление, снятие стресса и развитие сознания
Иногда в жизни бывают дни.......
Послушай других и сделай наооборот
Послушай других и сделай наооборот
Основатель Tesla и SpaceX воплотил в жизнь одну идею, противоречащую общепринятому мнению, и это помогло ему заработать миллиарды....
Красный флаг или работа не Вашей мечты.
Красный флаг или работа не Вашей мечты.
В последнее время развелось (были всегда) много организаций на рынке - которые надо обходить стороной....


ИНТЕРЕСНОЕ

Понимание карты и набора в JavaScript
Понимание карты и набора в JavaScript
Эта статья была изначально написана для DigitalOcean ....
Lazareth Wazuma от Феррари
Lazareth Wazuma от Феррари
Lazareth Wazuma V8F Quad – Engine By Ferrari...
20 самых важных секретов настоящих отношений
20 самых важных секретов настоящих отношений
Не простые отношения между женщинами и мужчинами...
Работа в Швеции
Работа в Швеции
Компании в Швеции переходят на 6-часовые рабочие дни и добиваются удивительных результатов...


ЛУЧШИЕ РЕЙТИНГИ

Набор массы
Набор массы
Базовые принципы для новичков
Ежедневные 15-минутные прогулки способны кардинально изменить ваше тело
Ежедневные 15-минутные прогулки способны кардинально изменить ваше тело
Всем известно, как положительно влияют на организм...
Правила для наращивания мышечной массы
Правила для наращивания мышечной массы
Зная интенсивность физических упражнений...
Раскачать грудь
Раскачать грудь
Обычно грудные растут хорошо у тех...

АКТУАЛЬНОЕ

Будущее уже с нами Galaxy Fold
CEO продвижение
Отношения с мужчинами
Раздевалка
Лишь плохие начальники ожидают от своих подчиненных постоянной занятости
Как научиться читать быстрее
Выбирай того, кто ежедневно пишет тебе «С добрым утром»
США скрывают правду о пришельцах
Свечение от ракеты SpaceX американцы приняли за НЛО
Путешествие из Австрии в Италию

ЧИТАЙТЕ ТАКЖЕ

18-10-2020 09:35:59 (120052)
Какую одежду носят манхэттенские модницы летом
Возможно эта новость тебе еще неизвестна
18-10-2020 09:35:59 (120050)
Время сгибаемых смартфонов еще не пришло
Эксперт по технологиям издания Mashable Стэн Шредер написал колонку...
18-10-2020 09:35:59 (120049)
Успешные стартапы, которые начинали как сторонние проекты
Apple, Facebook, Google, SpaceX ...
18-10-2020 09:35:59 (120051)
Надо стараться быть с теми, кто к нам хорошо относится
Маленький гимназист очень плохо учился...
18-10-2020 09:35:59 (120044)
Стопроцентная диета для похудения или питание наоборот
Сделай все наоборот....
18-10-2020 09:35:59 (120046)
Невероятная 12-месячная трансформацией тела
Звезда фитнеса из Сиднея Софи Аллен рассказывает о своей трансформации..