Files
go-mobile-uiautomator/libs/request.go
2026-03-10 13:41:18 +08:00

238 lines
6.6 KiB
Go
Raw 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 libs
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
// ---------- 外部接口和类型定义 ----------
// AdbDevice 定义了通过 ADB 隧道创建设备连接的接口
type AdbDevice interface {
// CreateConnection 建立到设备的 TCP 连接
// network 通常为 "tcp"port 为设备上服务监听端口
CreateConnection(network string, port int) (net.Conn, error)
}
// HTTPResponse 封装 HTTP 响应数据
type HTTPResponse struct {
Content []byte // 响应体内容
Status int // HTTP 状态码
Reason string // 状态描述
}
// JSON 将响应体解析为指定的结构体
func (r *HTTPResponse) JSON(v interface{}) error {
return json.Unmarshal(r.Content, v)
}
// Text 返回响应体的文本内容
func (r *HTTPResponse) Text() string {
return string(r.Content)
}
// ---------- 自定义错误类型 ----------
var (
// ErrHTTPTimeout 表示 HTTP 请求超时
ErrHTTPTimeout = errors.New("HTTP 请求超时")
// ErrHTTPFailed 表示 HTTP 请求失败
ErrHTTPFailed = errors.New("HTTP 请求失败")
)
// ---------- AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求 ----------
// AdbHTTPConnection 基于 net.Conn 实现的 HTTP 连接
// 通过 ADB 端口转发直接与设备端 UIAutomator2 服务通信
type AdbHTTPConnection struct {
Conn net.Conn
}
// NewAdbHTTPConnection 创建一个新的 ADB HTTP 连接
// dev 为设备接口port 为设备端服务端口timeout 为连接超时
func NewAdbHTTPConnection(dev AdbDevice, port int, timeout time.Duration) (*AdbHTTPConnection, error) {
conn, err := dev.CreateConnection("tcp", port)
if err != nil {
return nil, fmt.Errorf("无法连接到 UIAutomator2 服务: %w", err)
}
_ = conn.SetDeadline(time.Now().Add(timeout))
return &AdbHTTPConnection{Conn: conn}, nil
}
// Close 关闭底层连接
func (c *AdbHTTPConnection) Close() error {
if c.Conn != nil {
return c.Conn.Close()
}
return nil
}
// sendRequest 将 HTTP 请求写入连接并读取响应
// 通过原始 TCP 连接发送 HTTP 报文,避免依赖标准 http.Client
func (c *AdbHTTPConnection) sendRequest(req *http.Request, timeout time.Duration) (*http.Response, error) {
// 设置读写截止时间
if timeout > 0 {
_ = c.Conn.SetDeadline(time.Now().Add(timeout))
} else {
_ = c.Conn.SetDeadline(time.Time{})
}
// 序列化 HTTP 请求为原始报文
var buf bytes.Buffer
// 请求行METHOD PATH HTTP/1.1
path := req.URL.RequestURI()
if path == "" {
path = "/"
}
fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", req.Method, path)
fmt.Fprintf(&buf, "Host: localhost\r\n")
// 设置默认请求头
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "uiautomator2")
}
if req.Header.Get("Accept-Encoding") == "" {
req.Header.Set("Accept-Encoding", "")
}
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
// 写入请求头
for k, vals := range req.Header {
for _, v := range vals {
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
}
}
// 处理请求体
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %w", err)
}
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(bodyBytes))
} else {
fmt.Fprintf(&buf, "Content-Length: 0\r\n")
}
// 请求头与请求体之间的空行
buf.WriteString("\r\n")
// 发送请求头
if _, err := c.Conn.Write(buf.Bytes()); err != nil {
return nil, fmt.Errorf("发送请求头失败: %w", err)
}
// 发送请求体
if len(bodyBytes) > 0 {
if _, err := c.Conn.Write(bodyBytes); err != nil {
return nil, fmt.Errorf("发送请求体失败: %w", err)
}
}
// 使用标准库解析 HTTP 响应
reader := bufio.NewReader(c.Conn)
resp, err := http.ReadResponse(reader, req)
if err != nil {
return nil, fmt.Errorf("读取 HTTP 响应失败: %w", err)
}
return resp, nil
}
// ---------- HttpRequest高层 HTTP 请求封装 ----------
// HttpRequest 向设备端 UIAutomator2 服务发送 HTTP 请求
// ctx 为上下文控制dev 为设备接口devicePort 为设备端服务端口
// method 为 HTTP 方法path 为请求路径
// data 为请求体数据(会被 JSON 编码timeoutSecs 为超时秒数
// printRequest 为 true 时输出 curl 风格的调试信息
func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, path string, data map[string]interface{}, timeoutSecs float64, printRequest bool) (*HTTPResponse, error) {
// 默认超时 10 秒
if timeoutSecs <= 0 {
timeoutSecs = 10.0
}
timeout := time.Duration(timeoutSecs * float64(time.Second))
// 调试模式:打印 curl 风格的请求信息
if printRequest {
now := time.Now().Format("15:04:05.000")
url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path)
if data != nil {
b, _ := json.Marshal(data)
fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b))
} else {
fmt.Printf("# HTTP 超时=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url)
}
}
// 构造 HTTP 请求
var body io.Reader
if data != nil {
b, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("JSON 编码失败: %w", err)
}
body = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, body)
if err != nil {
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
}
req.Header.Set("User-Agent", "uiautomator2")
req.Header.Set("Accept-Encoding", "")
req.Header.Set("Content-Type", "application/json")
// 建立到设备的连接
connWrapper, err := NewAdbHTTPConnection(dev, devicePort, timeout)
if err != nil {
return nil, err
}
defer connWrapper.Close()
// 发送请求并读取响应
resp, err := connWrapper.sendRequest(req, timeout)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return nil, fmt.Errorf("%w: %v", ErrHTTPTimeout, err)
}
return nil, fmt.Errorf("%w: %v", ErrHTTPFailed, err)
}
defer resp.Body.Close()
// 读取响应体
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP 请求失败: %d %s", resp.StatusCode, resp.Status)
}
response := &HTTPResponse{
Content: content,
Status: resp.StatusCode,
Reason: resp.Status,
}
if printRequest {
now := time.Now().Format("15:04:05.000")
fmt.Printf("%s 响应 >>>\n%s\n<<< 结束\n\n", now, strings.TrimRight(response.Text(), "\n"))
}
return response, nil
}