В этой статье мы рас­смот­рим основные при­емы работы с канала­ми в Go и поз­накомим­ся с при­ема­ми и прак­тиками написа­ния мно­гопо­точ­ного кода. В качес­тве прак­тично­го при­мера напишем свой быс­трый фаз­зер, который будет про­верять, есть ли на хос­те фай­лы из сло­варя.

При­нятая в Go реали­зация кон­курен­тнос­ти через обмен сооб­щени­ями меж­ду незави­симы­ми друг от дру­га потока­ми называ­ется CSP (Communicating Sequential Processes). В ее осно­ве — каналы, переда­ющие сооб­щения от одно­го потока (горути­ны) к дру­гому.

Ес­ли ты зна­ком с кон­цепци­ей сис­темных каналов (pipes), то уже при­мер­но понима­ешь, что к чему. Одна­ко, ран­тайм Go не исполь­зует средс­тва опе­раци­онной сис­темы, у него име­ется собс­твен­ная реали­зация горутин и каналов.

В саму методи­ку фаз­зинга мы здесь пог­ружать­ся не будем: все‑таки основная цель — осво­ить Go, а потому кон­цен­три­руем­ся сей­час на инс­тру­мен­тах, пре­дос­тавля­емых язы­ком прог­рамми­рова­ния. Так что реали­зация будет мак­сималь­но прос­тая: добав­ляем к име­ни хос­та сло­во из сло­варя, затем выпол­няем GET зап­рос по получив­шемуся адре­су и сох­раня­ем код отве­та.

Сло­варь мож­но взять, нап­ример, здесь, но я советую не брать очень боль­шой спи­сок, а написать свой бук­валь­но из десят­ка позиций — это­го более чем дос­таточ­но для начала.

info

Час­то модель CSP опи­сыва­ют одним утвер­жде­нием: «не реали­зовы­вай ком­муника­цию пос­редс­твом сов­мес­тно­го исполь­зования памяти; вмес­то это­го сов­мес­тно исполь­зуй память пос­редс­твом ком­муника­ции».

www

Мо­дель CSP раз­работа­на и опи­сана Чарль­зом Энто­ни Хоаром — воз­можно, ты уже слы­шал его имя в свя­зи с алго­рит­мом быс­трой сор­тиров­ки (qsort).

Соз­дадим новый модуль:

mkdir dirfuzzer && cd $_
go mod init dirfuzzer
touch main.go
 

Разделяем код на модули

В файл main.go вста­вим сле­дующий код:

package main
import (
"bufio"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
)
type Result struct {
Name string
Code int
}
// produce генерирует задания для обработки,
// комбинируя host со значениями, считанными из filename,
// и помещает их в канал outCh.
func produce(filename string, host string, outCh chan<- string) {
file, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "opening %s: %v\n", filename, err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := strings.TrimSpace(scanner.Text())
if s == "" {
continue
}
outCh <- "https://" + host + "/" + s
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "reading %s: %v\n", filename, err)
}
}
// worker получает значения из канала inCh, пока он остается открытым, выполняет обработку и помещает результаты в outCh.
func worker(c *http.Client, inCh <-chan string, outCh chan<- Result) {
for job := range inCh {
resp, err := c.Get(job)
if err != nil {
continue
}
resp.Body.Close()
io.Copy(io.Discard, resp.Body)
result := Result{
Name: job,
Code: resp.StatusCode,
}
outCh <- result
}
}
// collect получает значения из канала resultCh, пока он остается открытым, и записывает их в файл filename.
func collect(filename string, resultCh <-chan Result) {
dstFile, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "creating %s: %v\n", filename, err)
return
}
defer dstFile.Close()
writer := bufio.NewWriter(dstFile)
for r := range resultCh {
s := fmt.Sprintf("%s - %d %s\n", r.Name, r.Code, http.StatusText(r.Code))
_, err = writer.WriteString(s)
if err != nil {
fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err)
}
}
if err = writer.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err)
}
}
func main() {
// TODO
}

Как видишь, мы логичес­ки рас­пре­дели­ли фраг­менты кода меж­ду отдель­ными изо­лиро­ван­ными фун­кци­ями в соот­ветс­твии с ролями, которые на этот код воз­ложены.

Фун­кция produce() генери­рует задания для пос­леду­ющей обра­бот­ки. Она вычиты­вает стро­ки из спис­ка и фор­миру­ет URL, по которым будут осу­щест­влять­ся попыт­ки соеди­нения. Сама обра­бот­ка лежит вне зоны ответс­твен­ности этой фун­кции и никак не вли­яет на ее код. Дру­гие час­то исполь­зуемые име­на для такого кода: generate, source.

Фун­кция worker() обра­баты­вает задания. В дан­ном слу­чае, выпол­няет­ся GET зап­рос к сер­веру и сох­ранение кода отве­та. Как ты видишь, фун­кция «не зна­ет» ничего ни о про­исхожде­нии заданий, ни о даль­нейшей судь­бе резуль­татов — это к ней не отно­сит­ся. Дру­гие час­то встре­чающиеся име­на для такой роли: process, handle.

Фун­кция collect() вычиты­вает резуль­таты и сох­раня­ет их в файл. По ана­логии с пре­дыду­щими она ничего не зна­ет и не дол­жна знать о том, отку­да и каким обра­зом эти резуль­таты взя­лись. У нее есть толь­ко одна чет­ко очер­ченная зона ответс­твен­ности — соб­рать и сох­ранить. Дру­гие час­то встре­чающиеся име­на: consume, sink, save.

По­доб­ное раз­деление воп­лоща­ет прин­цип пос­тро­ения прог­рам­мных сис­тем loose couping, high cohesion (один из вари­антов перево­да: «сла­бая свя­зан­ность, силь­ное сцеп­ление»). Сог­ласно это­му прин­ципу, сис­тема стро­ится из отдель­ных ком­понен­тов, которые минималь­но зависят друг от дру­га (это и есть loose coupling). При этом каж­дый такой ком­понент выпол­няет чет­ко очер­ченную роль, и код, ответс­твен­ный за ту или иную фун­кцию, скон­цен­три­рован внут­ри «сво­его» ком­понен­та, а не раз­мазан по дру­гим (а это — high cohesion).

Та­кой под­ход облегча­ет понима­ние работы сис­темы, поз­воля­ет при­менять модуль­ное тес­тирова­ние, улуч­шает пов­торное исполь­зование кода и дает воз­можность в будущем наращи­вать воз­можнос­ти.

Ес­ли мы захотим, к при­меру, сох­ранять резуль­таты в дру­гом фор­мате или вооб­ще писать их не в файл, а, ска­жем, отправ­лять по сети на дру­гой сер­вер — нам нуж­но толь­ко заменить ком­понент collect, не тро­гая осталь­ные и даже не читая их код. Это общий прин­цип, он при­меним не толь­ко к отдель­ным фун­кци­ям или клас­сам, но и к любым ком­понен­там прог­рам­мной сис­темы в широком понима­нии это­го сло­ва.

info

Раз­деление боль­шого монолит­ного кус­ка кода, реша­юще­го ту или иную задачу, на отдель­ные ком­понен­ты, называ­ется модуля­риза­цией (modularization). Не знаю, как тебе, а мне это труд­но даже про­изнести, поэто­му я пред­почитаю родс­твен­ное понятие — деком­позиция. Хотя, конеч­но, оно боль­ше про под­ход к решению задачи, нежели про струк­туриро­вание реали­зации это­го решения.

 

Каналы

За­пус­кать эти фун­кции будем как отдель­ные горути­ны. При этом мы отка­жем­ся от «клас­сичес­кого» под­хода к обме­ну информа­цией через сов­мес­тно исполь­зуемые объ­екты, защищен­ные мьютек­сами. Вмес­то это­го вос­поль­зуем­ся ка­нала­ми — спе­циаль­ным инс­тру­мен­том ком­муника­ции меж­ду горути­нами. Каналы потоко­безо­пас­ны по опре­деле­нию и не тре­буют исполь­зования мьютек­сов или иных при­мити­вов син­хро­низа­ции.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии