Files
2025-12-06 11:26:05 +08:00

241 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"cmii-uav-watchdog-common/models"
)
// 错误类型常量定义
var (
ErrRequestFailed = errors.New("请求失败")
ErrResponseParsing = errors.New("响应解析失败")
ErrInvalidStatusCode = errors.New("无效的状态码")
ErrTimeout = errors.New("请求超时")
ErrCancelled = errors.New("请求被取消")
)
const (
DefaultHeartbeatURL = "http://cmii-uav-watchdog:8080/api/heartbeat"
// DefaultHeartbeatURL = "http://192.168.35.70:8990/api/heartbeat"
)
// ClientOptions HTTP客户端配置选项
type ClientOptions struct {
Timeout time.Duration // 请求超时时间
RetryCount int // 重试次数
RetryWaitTime time.Duration // 重试等待时间
MaxIdleConns int // 最大空闲连接数
IdleConnTimeout time.Duration // 空闲连接超时时间
}
// DefaultClientOptions 返回默认的客户端配置
func DefaultClientOptions() *ClientOptions {
return &ClientOptions{
Timeout: 10 * time.Second,
RetryCount: 3,
RetryWaitTime: 1 * time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
}
}
// Client HTTP客户端封装
type Client struct {
httpClient *http.Client
options *ClientOptions
heartbeatURL string // 心跳请求的URL地址
}
// NewClient 创建一个新的HTTP客户端
// 参数:
// - options: 客户端配置选项如果为nil则使用默认配置
//
// 返回:
// - *Client: HTTP客户端实例
func NewClient(options *ClientOptions, heartbeatURL string) *Client {
if options == nil {
options = DefaultClientOptions()
}
// 创建自定义的Transport
transport := &http.Transport{
MaxIdleConns: options.MaxIdleConns,
IdleConnTimeout: options.IdleConnTimeout,
}
// 创建HTTP客户端
httpClient := &http.Client{
Timeout: options.Timeout,
Transport: transport,
}
// 解析heartbeatURL
heartbeatURL = strings.TrimSpace(heartbeatURL)
if heartbeatURL == "" {
heartbeatURL = DefaultHeartbeatURL
}
return &Client{
httpClient: httpClient,
options: options,
heartbeatURL: heartbeatURL,
}
}
// GetHeartbeatURL 获取心跳请求的URL地址
func (c *Client) GetHeartbeatURL() string {
return c.heartbeatURL
}
// SendHeartbeat 发送心跳请求并处理响应
// 参数:
// - ctx: 上下文,用于取消请求
// - url: 心跳请求的URL地址
// - request: 心跳请求数据
//
// 返回:
// - *models.HeartbeatResponse: 心跳响应
// - error: 错误信息
func (c *Client) SendHeartbeat(ctx context.Context, request *models.HeartbeatRequest) (*models.HeartbeatResponse, error) {
// 将请求结构体序列化为JSON
requestBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
// 重试逻辑
var resp *http.Response
var responseBody []byte
var lastError error
for attempt := 0; attempt <= c.options.RetryCount; attempt++ {
// 如果不是第一次尝试,则等待一段时间
if attempt > 0 {
select {
case <-ctx.Done():
return nil, fmt.Errorf("%w: %v", ErrCancelled, ctx.Err())
case <-time.After(c.options.RetryWaitTime):
// 继续下一次尝试
}
}
// 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.heartbeatURL, bytes.NewBuffer(requestBody))
if err != nil {
lastError = fmt.Errorf("创建请求失败: %w", err)
continue
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "cmii-uav-watchdog-agent")
// 发送请求
resp, err = c.httpClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
lastError = fmt.Errorf("%w: %v", ErrTimeout, err)
} else if ctx.Err() == context.Canceled {
lastError = fmt.Errorf("%w: %v", ErrCancelled, err)
} else {
lastError = fmt.Errorf("%w: %v", ErrRequestFailed, err)
}
continue
}
// 确保响应体被关闭
defer resp.Body.Close()
// 使用switch检查HTTP状态码
switch resp.StatusCode {
case http.StatusOK:
// 状态码为200继续处理响应
case http.StatusBadRequest:
lastError = fmt.Errorf("%w: 状态码 %d请求无效", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
case http.StatusUnauthorized:
lastError = fmt.Errorf("%w: 状态码 %d未授权", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
case http.StatusForbidden:
lastError = fmt.Errorf("%w: 状态码 %d禁止访问", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
case http.StatusNotFound:
lastError = fmt.Errorf("%w: 状态码 %d未找到资源", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
case http.StatusInternalServerError:
lastError = fmt.Errorf("%w: 状态码 %d服务器内部错误", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
default:
lastError = fmt.Errorf("%w: 状态码 %d未知错误", ErrInvalidStatusCode, resp.StatusCode)
// 读取并丢弃响应体,避免连接泄漏
_, _ = io.Copy(io.Discard, resp.Body)
continue
}
// 读取响应体
responseBody, err = io.ReadAll(resp.Body)
if err != nil {
lastError = fmt.Errorf("读取响应体失败: %w", err)
continue
}
// 成功获取响应,跳出重试循环
lastError = nil
break
}
// 如果最后依然有错误,返回错误
if lastError != nil {
return nil, lastError
}
// 如果没有响应体,返回错误
if responseBody == nil {
return nil, fmt.Errorf("%w: 没有响应体", ErrResponseParsing)
}
// 解析响应JSON
var heartbeatResponse models.HeartbeatResponse
if err := json.Unmarshal(responseBody, &heartbeatResponse); err != nil {
return nil, fmt.Errorf("%w: %v", ErrResponseParsing, err)
}
return &heartbeatResponse, nil
}
// SendHeartbeatWithRetry 发送心跳请求并自动处理超时和重试
// 参数:
// - url: 心跳请求的URL地址
// - request: 心跳请求数据
// - timeout: 整体操作超时时间
//
// 返回:
// - *models.HeartbeatResponse: 心跳响应
// - error: 错误信息
func (c *Client) SendHeartbeatWithRetry(request *models.HeartbeatRequest, timeout time.Duration) (*models.HeartbeatResponse, error) {
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return c.SendHeartbeat(ctx, request)
}