package netclient import ( "context" "fmt" "io" "net/http" "strings" "sync" "time" "wartank/pkg/components/scene_net/netstat" "wartank/pkg/types" ) /* Объект сетевого соединения */ // Ответ после запроса type response struct { lstString []string err error } // NetClient -- объект сетевого соединения type NetClient struct { botNet types.ИБотСеть conn *http.Client stat *netstat.NetStat chRes chan *response // Канал ответа от http-клиента block sync.Mutex } // NewNetClient -- возвращает сетевого клиента func NewNetClient(botNet types.ИБотСеть) *NetClient { сам := &NetClient{ botNet: botNet, conn: botNet.Коннект(), stat: netstat.NewNetStat(botNet), chRes: make(chan *response, 2), } return сам } // Get -- выполняет безопасный GET-запрос в сеть func (сам *NetClient) Get(strLink string) (lstString []string, err error) { сам.block.Lock() defer сам.block.Unlock() ctxCancel, fnCancel := context.WithTimeout(сам.botNet.Кнт(), time.Second*10) defer fnCancel() defer func() { // Возможный перехват паники if _panic := recover(); _panic != nil { err = fmt.Errorf("NetClient.Get().defer(): перехвачена паника для URL(%v), panic=%v", strLink, _panic) lstString = nil time.Sleep(time.Millisecond * 250) // Чтобы не насиловать не работающую сеть } }() go сам.get(strLink) select { case <-ctxCancel.Done(): // Таймаут по ожиданию err = fmt.Errorf("NetClient.get(): таймаут ожидания ответа") сам.botNet.Отмена() return nil, err case resp := <-сам.chRes: // Получен ответ if resp.err != nil { return nil, resp.err } return resp.lstString, nil } } // Внутренний вызов для сокрытия под общей блокировкой func (сам *NetClient) get(strLink string) { resp := &response{} defer func() { if resp.err != nil { сам.stat.IncErr() } }() req, err := http.NewRequest("GET", strLink, nil) if err != nil { resp.err = fmt.Errorf("NetClient.get(): при создании запроса, err=\n\t%w", err) return } req.Header.Set("User-Agent", "Mozilla Firefox 94.1") httpResp, err := сам.conn.Do(req) if err != nil { resp.err = fmt.Errorf("NetClient.get(): при выполнении GET-запроса, err=\n\t%w", err) return } defer сам.closeGetBody(strLink, httpResp, resp) if httpResp.StatusCode != http.StatusOK { resp.err = fmt.Errorf("NetClient.get(): code=%v, status=%v", httpResp.StatusCode, httpResp.Status) return } binData, err := io.ReadAll(httpResp.Body) if err != nil { resp.err = fmt.Errorf("NetClient.get(): при чтении тела ответа, err=\n\t%w", err) return } if len(binData) == 0 { resp.err = fmt.Errorf("NetClient.get(): пустое тело ответа, err=\n\t%w", err) return } lenData := len(binData) + len(strLink) сам.stat.AddByte(lenData) lstString := strings.Split(string(binData), "\n") if len(lstString) == 0 { resp.err = fmt.Errorf("NetClient.get(): lstString is empty") return } resp.lstString = lstString } // Вызывается по завершению вызова, закрывает тело запроса func (сам *NetClient) closeGetBody(strLink string, httpResp *http.Response, resp *response) { defer func() { if _panic := recover(); _panic != nil { // log._rintf("NetClient.closeGetBody(): strLink='%v', panic=%v\n", strLink, _panic) сам.botNet.Отмена() } }() err := httpResp.Body.Close() if err != nil { _err := fmt.Errorf("NetClient.closeGetBody(): ошибка при закрытии запроса URL(%q), err=\n\t%w", strLink, err) if resp.err != nil { // Есть и ошибка в закрытии тела запроса и внутренняя resp.err = fmt.Errorf("NetClient.closeGetBody(): двойная ошибка при закрытии запроса URL(%q), err=\n\t%w\n\tinternal err=\n\t%w", strLink, _err, resp.err) } resp.lstString = nil сам.stat.IncErr() } сам.chRes <- resp }