是否可以使用特定的TCPConn发出HTTP请求?

时间:2019-10-04 00:50:42

标签: http go tcp

我正在尝试建立一种在运行Go的中央服务器与一系列IoT设备(也运行Go)的中央服务器之间进行通信的方法。

对于每个设备,它都通过持久的TCPConn连接到中央服务器。这些设备在路由器后面。中央服务器保存该连接并通过该连接发送/接收消息。现在,它已经可以正常工作了。

但是,现在消息传递变得非常复杂,以至于有必要使用HTTP提供的实用程序而不是纯TCP提供的实用程序。

我试图编写一个返回上述连接的http.Transport版本。但是,我无法从Dial / DialContext函数提供并返回有效的连接。

IoT设备

func main() {

    http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    tcpAddr, err := net.ResolveTCPAddr("tcp", "###.###.###.###:8533")
    if err != nil {
        panic(err)
    }

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        panic(err)
    }

    err = conn.SetKeepAlive(true)
    if err != nil {
        panic(err)
    }

    err = conn.SetKeepAlivePeriod(time.Second * 10)
    if err != nil {
        panic(err)
    }

    fmt.Println("Listening")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

中央服务器

func main() {
    tcpAddr, err := net.ResolveTCPAddr("tcp", port)
    if err != nil {
        panic(err)
    }
    listener, err := net.ListenTCP("tcp", tcpAddr)

    if err != nil {
        panic(err)
    }

    conn, err := listener.AcceptTCP()
    if err != nil {
        panic(err)
    }
    fmt.Println("Received conn, attempting to send HTTP through connection")

    dialFunc := func(network, addr string) (net.Conn, error) {
        return conn, nil
    }

    t := http.Transport{
        Dial: dialFunc,
    }

    client := http.Client{
        Transport: &t,
    }

    fmt.Println("Making request")
    res, err := client.Get("http://www.shouldNotMatter.com:8080/foo") // HANGS HERE
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println("Received response")
    defer res.Body.Close()

    if res.StatusCode == http.StatusOK {
        bodyBytes, err := ioutil.ReadAll(res.Body)
        if err != nil {
            panic(err)
        }
        bodyString := string(bodyBytes)
        fmt.Println(bodyString)
    } else {
        fmt.Println(res)
    }
}

使用调试器查看它的挂起位置后,似乎在pconn往返过程中卡在了select语句中。 https://golang.org/src/net/http/transport.go?s=3397:10477

中的第2420行

2 个答案:

答案 0 :(得分:3)

创建一个通过拨号方法返回现有连接的类型:

type connDialer struct {
    c net.Conn
}

func (cd connDialer) Dial(network, addr string) (net.Conn, error) {
    return cd.c, nil
}

在运输工具中使用拨盘method value

client := http.Client{Transport: &http.Transport{Dial: connDialer{c}.Dial}}

其中c是现有的net.Conn

the playground上尝试一下(它仅在一个请求下起作用。当客户端拨打第二个连接时,它将失败。)

总体方法是脆弱的。考虑使用WebSocket,gRPC或旨在支持双向通信的其他协议。

答案 1 :(得分:0)

您错过了客户端代码上的代码。客户端与服务器建立了空闲连接,并且不执行任何操作,因此连接肯定会挂断。您需要将连接传递到HTTP服务器。这可以通过使用net/http.Serve并将net.Listener传递给它来实现。

type connListener struct {
    conn net.Conn
    ch chan struct{}
}
func (c connListener) Accept() (Conn, error) {
    if c.conn != nil {
        conn := c.conn
        c.conn = nil
        return conn, nil
    }
    <-c.ch
    return nil, errors.New("listener closed")
}
func (c connListener) Close() error {
    close(c.ch)
    return nil
}
func (c connListener) Addr() net.Addr {
    return c.conn.LocalAddr()
}

// call it like this
http.Serve(connListener{conn, make(chan struct{})}, nil)

顺便说一句,您是否让客户端连接到服务器,然后反向连接,从而使客户端的行为类似于HTTP服务器,而服务器的行为类似于HTTP客户端?您可能想在Google上“反向http”以获取有关此信息。