【问题标题】:How to write a proxy in go (golang) using tcp connections如何使用 tcp 连接在 go (golang) 中编写代理
【发布时间】:2014-09-25 06:35:27
【问题描述】:

如果其中一些问题对于专业网络程序员来说可能是显而易见的,我在此道歉。我已经研究并阅读了有关网络编码的信息,但我仍然不清楚如何做到这一点。

假设我想用一些 TCP 客户端和一些 TCP 服务器之间的连接编写一个 tcp 代理(正在运行)。像这样的:

首先假设这些连接是半永久的(会在很长一段时间后关闭),我需要数据按顺序到达。

我想要实现的想法如下:每当我收到来自客户端的请求时,我想将该请求转发到后端服务器并等待(并且什么都不做)直到后端服务器响应我(代理) 然后将该响应转发给客户端(假设在常见情况下两个 TCP 连接都将保持)。

我不知道如何解决一个主要问题。当我将请求从代理转发到服务器并获得响应时,如果我事先不知道从服务器发送到的数据的格式,我怎么知道服务器何时向我发送了我需要的所有信息代理(即我不知道来自服务器的响应是否为type-length-value scheme 的形式,也不知道`\r\n\ 是否表示来自服务器的消息结束)。有人告诉我,只要我从 tcp 连接的读取大小为零或小于我预期的读取大小,我就应该假设我从服务器连接获取所有数据。但是,这对我来说似乎不正确。通常它可能不正确的原因如下:

假设服务器出于某种原因一次只向其套接字写入一个字节,但对“真实”客户端的响应总长度要长得多。因此,当代理读取连接到服务器的 tcp 套接字时,代理是否可能只读取一个字节并且如果它循环足够快(在接收更多数据之前进行读取),那么读取零并且不正确得出的结论是它得到了客户端打算接收的所有消息?

解决此问题的一种方法可能是在每次从套接字读取之后等待,这样代理的循环速度不会超过它获取字节的速度。我担心的原因是,假设有一个网络分区,我不能再与服务器交谈。但是,它与我断开连接的时间不足以使 TCP 连接超时。因此,我是否有可能再次尝试从 tcp 套接字读取到服务器(比我获取数据的速度更快)并读取零并错误地断定它的所有数据然后将其打包发送到客户端? (请记住,我要遵守的承诺是,当我写入客户端连接时,我只向客户端发送整个消息。因此,如果代理运行,考虑正确的行为是非法的,在它之后再次读取连接已经写入客户端,并在稍后发送丢失的块,可能是在不同请求的响应期间)。

我写的代码在go-playground.

我喜欢用来解释为什么我认为这种方法不起作用的类比如下:

假设我们有一个杯子,代理每次从服务器读取数据时都会喝一半的杯子,但服务器一次只放 1 茶匙。因此,如果代理喝水的速度比它喝茶匙的速度快,它可能会过早地达到零,并得出结论认为它的套接字是空的并且可以继续前进!如果我们想保证每次都发送完整的消息,这是错误的。要么,这个类比是错误的,TCP 的一些“魔法”使它起作用,要么假设套接字为空的算法完全错误。

此处处理类似问题的question 建议阅读到EOF。但是,我不确定为什么这是正确的。阅读EOF 是否意味着我收到了缩进的消息?每次有人将一大块字节写入 tcp 套接字时是否发送EOF(即我担心如果服务器一次写入一个字节,它会发送 1 个EOF 每个字节)?但是,EOF 可能是 TCP 连接如何真正工作的一些“魔法”?发送EOF 会关闭连接吗?如果它不是我想要使用的方法。此外,我无法控制服务器可能在做什么(即我不知道它想多久写入一次套接字以将数据发送到代理,但是,假设它使用一些“标准”写入套接字是合理的/正常写入套接字的算法”)。我只是不相信从服务器的套接字读取直到EOF 是正确的。为什么会呢?我什么时候可以阅读EOFEOFs 是数据的一部分还是在 TCP 标头中?

另外,我写的关于将 wait 放在超时后的 epsilon 的想法,在最坏的情况下会起作用还是仅在平均情况下起作用?我也在想,我意识到如果 Wait() 调用比超时时间长,那么如果你返回到 tcp 连接并且它没有任何东西,那么继续前进是安全的。但是,如果它没有任何内容并且我们不知道服务器发生了什么,那么我们将超时。所以关闭连接是安全的(因为无论如何超时都会这样做)。因此,我认为如果等待调用的时间至少与超时时间一样长,那么这个过程确实有效!人们怎么看?

我也对一个可以证明为什么该算法在某些情况下有效的答案感兴趣。比如我在想,即使服务器一次只写一个字节,如果部署的场景是一个紧张的数据中心,那么平均来说,因为延迟非常小,等待调用几乎肯定足够了,那么不会这个算法不行吗?

另外,我编写的代码是否存在陷入“死锁”的风险?

package main

import (
    "fmt"
    "net"
)

type Proxy struct {
    ServerConnection *net.TCPConn
    ClientConnection *net.TCPConn
}

func (p *Proxy) Proxy() {
    fmt.Println("Running proxy...")
    for {
        request := p.receiveRequestClient()
        p.sendClientRequestToServer(request)
        response := p.receiveResponseFromServer() //<--worried about this one.
        p.sendServerResponseToClient(response)
    }
}

func (p *Proxy) receiveRequestClient() (request []byte) {
    //assume this function is a black box and that it works.
    //maybe we know that the messages from the client always end in \r\n or they
    //they are length prefixed.
    return
}

func (p *Proxy) sendClientRequestToServer(request []byte) {
    //do
    bytesSent := 0
    bytesToSend := len(request)
    for bytesSent < bytesToSend {
        n, _ := p.ServerConnection.Write(request)
        bytesSent += n
    }
    return
}

// Intended behaviour: waits until ALL of the response from backend server is obtained.
// What it does though, assumes that if it reads zero, that the server has not yet
// written to the proxy and therefore waits. However, once the first byte has been read,
// keeps writting until it extracts all the data from the server and the socket is "empty".
// (Signaled by reading zero from the second loop)
func (p *Proxy) receiveResponseFromServer() (response []byte) {
    bytesRead, _ := p.ServerConnection.Read(response)
    for bytesRead == 0 {
        bytesRead, _ = p.ServerConnection.Read(response)
    }
    for bytesRead != 0 {
        n, _ := p.ServerConnection.Read(response)
        bytesRead += n
        //Wait(n) could solve it here?
    }
    return
}

func (p *Proxy) sendServerResponseToClient(response []byte) {
    bytesSent := 0
    bytesToSend := len(request)
    for bytesSent < bytesToSend {
        n, _ := p.ServerConnection.Write(request)
        bytesSent += n
    }
    return
}

func main() {
    proxy := &Proxy{}
    proxy.Proxy()
}

【问题讨论】:

  • github.com/nf/gohttptun 可能对您有用。但是,它很旧;在 Go v1.0 之前编写,可能已损坏。
  • 一般来说,代理通常只为Read 的每一位数据执行Write,而不是等待作者“真正完成”。不幸的是,我不认为在不知道协议的情况下可以遵守“你想要遵守的承诺”。
  • 此外,即使您可以确保一次只Write 整条消息(就像您知道长度一样),但不确定这会对您的客户端产生多大影响:网络层可以仍然将消息拆分,表示后半部分的数据包可能会延迟很长时间。
  • twotwotwo 的意思是,tcp 是一个流协议,而不是基于消息的。解决方案同时比你想象的要简单,只是同时复制流,而且稍微复杂一点;您必须考虑使用 2 个连接时可能出现的错误组合。

标签: sockets networking tcp proxy go


【解决方案1】:

除非您正在使用特定的更高级别的协议,否则没有“消息”可以从客户端读取以中继到服务器。 TCP 是一种流协议,您所能做的就是来回穿梭字节。

好消息是,这非常容易,这个代理的核心部分将是:

go io.Copy(server, client)
io.Copy(client, server)

这显然是缺少错误处理,并且没有干净地关闭,但清楚地显示了核心数据传输是如何处理的。

【讨论】:

  • 嗨,你能提供一个更详细的例子来说明你完全关闭这个模式的意思吗?我到处都看到了这个例子,但是如何确保在通常的错误情况下干净地关闭它?
  • @RalphCaraveo:请参阅 this gist 了解我使用的模式
猜你喜欢
  • 2018-08-27
  • 1970-01-01
  • 2015-05-02
  • 2012-09-08
  • 2010-12-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多