Содержание статьи
В заключительной статье о конкурентности в Go разберем инструкцию select, сигнальные каналы и инструменты пакета context. С их помощью удобно обмениваться сообщениями, управлять дедлайнами и отменять запущенные операции. В качестве практического примера реализуем простой механизм крекинга хеша MD5: будем подбирать пароль из словаря, пока его хеш не совпадет с нужным.
Словари можно взять, например, в репозитории SecLists. Заранее выбери любое слово не из первых строк словаря и запиши его MD5-хеш. Например, для пароля netadmin он будет таким:
07ded64f812afb8da181014a9d087728
Это пригодится для экспериментов с кодом.
Создай новый модуль:
mkdir brutemd5 && cd $_go mod init brutemd5
touch main.go
Вставь в main. такой код:
package mainimport ( "bufio" "crypto/md5" "encoding/hex" "fmt" "os" "runtime" "strings" "sync")type PipelineConfig struct { JobCh chan string ResultCh chan string DoneCh <-chan struct{} ErrorCh chan<- error SrcFileName string TargetHash *[md5.Size]byte}// Функция produce генерирует задания для обработки, считывая значения из cfg.SrcFileName, и помещает их в канал cfg.JobCh; завершается по окончании чтения файлаfunc produce(cfg *PipelineConfig) { file, err := os.Open(cfg.SrcFileName) if err != nil { cfg.ErrorCh <- fmt.Errorf("opening %s: %w", cfg.SrcFileName, err) return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { s := strings.TrimSpace(scanner.Text()) if s == "" { continue } select { case <-cfg.DoneCh: return case cfg.JobCh <- s: } } if err := scanner.Err(); err != nil { cfg.ErrorCh <- fmt.Errorf("reading %s: %w", cfg.SrcFileName, err) }}// Функция worker читает слова из cfg.JobCh, вычисляет MD5-хеш и сравнивает его с cfg.TargetHash; при совпадении помещает слово в cfg.ResultCh и завершается; при отсутствии совпадений завершаетсяfunc worker(cfg *PipelineConfig) { for { select { case job, ok := <-cfg.JobCh: if !ok { return } if md5.Sum([]byte(job)) == *cfg.TargetHash { cfg.ResultCh <- job return } case <-cfg.DoneCh: return } }}// Функция collect ожидает чтения либо закрытия cfg.ResultCh, выводит значение в консоль и завершаетсяfunc collect(cfg *PipelineConfig) { pwd, ok := <-cfg.ResultCh if !ok { // Канал закрыт, данные не поступили fmt.Println("\rNo match found!") } else { // Принято сообщение из канала fmt.Println("\rMatched successfully: ", pwd) }}func main() { // TODO}Здесь мы определили горутины, из которых соберем конвейер обработки. Чтобы не передавать в функции кучу аргументов, используем структуру PipelineConfig: в ней храним все параметры конвейера. Канал JobCh нужен для передачи сообщений, которые надо обработать, то есть слов, считанных из словаря. В ResultCh отправим слово, чей хеш совпал с искомым, если такое слово найдется.
В ErrorCh отправляем сообщения об ошибках. DoneCh — сигнальный канал: через него передадим всем горутинам команду сразу завершить работу. Строковая переменная SrcFileName хранит имя файла словаря, из которого мы читаем данные, а TargetHash — указатель на хеш, с которым ищем совпадение.
Функция produce( генерирует задания для дальнейшей обработки: читает строки из списка, валидирует их и отправляет в канал. Сама обработка — вычисление хешей — в задачи этой функции не входит и на ее код не влияет.
Функция worker( обрабатывает задания: вычисляет хеш, сравнивает его с целевым значением и, если они совпадают, отправляет слово в канал результатов и завершается. Обрати внимание: когда ты параллельно запускаешь несколько воркеров, совпадение найдет один из них и завершится, но остальные об этом не узнают и продолжат гриндить данные, пока те не закончатся. Поэтому нам нужен отдельный сигнал завершения, чтобы передать всем горутинам «стоп».
Функция collect( читает результаты и выводит их в консоль. Здесь мы ждем только один результат: так устроена задача. Поэтому цикла нет — просто ждем первое сообщение или закрытие канала, смотря что случится раньше.
info
О чтении из канала мы говорили в предыдущей статье, но вкратце напомню, если ты вдруг забыл: val, блокирует текущую горутину, пока из канала ch не придет значение val. Если канал закроют, val получит zero value для типа канала, а ok станет false. Мы используем проверку if !, чтобы разделить две ситуации — получение сообщения и закрытие канала — и выполнить нужные действия.
Как и в предыдущей статье, собираем конвейер обработки из отдельных горутин: одна генерирует задания, пул горутин параллельно обрабатывает их, а еще одна собирает результаты:
func main() { const srcFileName = "dict.txt" var maxWorkers = runtime.GOMAXPROCS(0) // Целевой хеш — аргумент запуска if len(os.Args) <= 1 { fmt.Fprintf(os.Stderr, "Target MD5 hash not specified\n") os.Exit(1) } hashStr := strings.TrimSpace(os.Args[1]) // Валидация и преобразование if len(hashStr) != 32 { fmt.Fprintf(os.Stderr, "MD5 hash must be 32 characters long\n") os.Exit(1) } var hashBytes [md5.Size]byte _, err := hex.Decode(hashBytes[:], []byte(hashStr)) if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } // Канал с заданиями jobCh := make(chan string, maxWorkers) // Канал с результатами resultCh := make(chan string) // Канал с ошибками errCh := make(chan error, 3) // Канал для сигнализации о завершении doneCh := make(chan struct{}) conf := &PipelineConfig{ JobCh: jobCh, ResultCh: resultCh, DoneCh: doneCh, ErrorCh: errCh, SrcFileName: srcFileName, TargetHash: &hashBytes, } // Группа конвейера обработки var pipelineWg sync.WaitGroup // Группа пула воркеров var workerWg sync.WaitGroup // Запускаем конвейер обработки pipelineWg.Go(func() { produce(conf) close(jobCh) }) pipelineWg.Go(func() { collect(conf) close(doneCh) }) // Запускаем обработчики for range maxWorkers { workerWg.Go(func() { worker(conf) }) } // Закрываем канал результатов по завершении обработчиков go func() { workerWg.Wait() close(resultCh) }() // Закрываем канал ошибок по завершении конвейера go func() { pipelineWg.Wait() close(errCh) }() // Сбор ошибок hadError := false for err := range errCh { if err != nil { fmt.Fprintf(os.Stderr, "error %v\n", err) hadError = true } } if hadError { os.Exit(1) }}Чтобы сэкономить место, я не буду заново разбирать, как работает конвейер. Если нужно — вернись к предыдущей статье. Кратко напомню последовательность запуска: мы инициализировали нужные каналы и структуру с настройками.
- Горутина
produce(для наглядности я буду называть горутины по именам соответствующих функций) запускается и работает, пока не прочитает файлsrcFileName. - Горутина
collectзапускается и работает, пока не закроется каналresultChили пока она не получит результат вычислений. - Горутины
workerзапускаются и работают, пока не закроется каналjobCh.
В отдельной горутине ждем, когда завершатся все воркеры, и только потом закрываем канал с результатами. Так воркер, который еще работает, не попытается записать в уже закрытый канал.
Еще в одной отдельной горутине ждем завершения конвейера и закрываем канал ошибок.
Тем временем поток выполнения идет дальше: main( заходит в бесконечный цикл чтения из канала ошибок errCh и остается там, пока канал не закроется. После этого по значению флага hadError выбирается код возврата.
И последовательность остановки:
- После чтения файла
produceзавершается, а каналjobChзакрывается. - Цикл чтения из
jobChвнутриworkerзавершается, когда канал закрывается. После этогоworker(возвращает управление. В отдельной горутине мы ждем, пока завершатся все воркеры, и закрываем канал) resultCh. - Чтение из
resultChвнутриcollectтоже завершается: либо мы получили искомое значение, либо все воркеры отработали и каналresultChзакрылся. После возврата изcollect(закрываем канал) doneCh— это самый важный момент, но о нем чуть позже.
В отдельной горутине ждем, пока остановится конвейер, то есть завершатся produce и collect, а затем закрываем канал errCh.
После закрытия errCh главная горутина выходит из цикла чтения.
Сигнальный канал и select
Разберем код функции worker( подробнее:
func worker(cfg *PipelineConfig) { for { select { case job, ok := <-cfg.JobCh: if !ok { return } // ...вычисление хеша... case <-cfg.DoneCh: return } }}
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
