如何使用TCP连接在Go(Golang)中编写代理

13
如果这些问题对于专业的网络程序员来说显而易见,我在此提前道歉。我已经研究过网络编程方面的代码,并且仍然不清楚如何做到这一点。
假设我想用go编写一个tcp代理,用于连接某个TCP客户端和某个TCP服务器。就像这样:

enter image description here

假设这些连接是半永久的(在很长一段时间后将关闭),我需要数据按顺序到达。
我想要实现的想法是:每当我从客户端收到请求时,我希望将该请求转发到后端服务器,并等待(并不执行任何操作)直到后端服务器响应给我(代理),然后将该响应转发给客户端(假设在通常情况下,两个TCP连接都将保持)。
有一个主要的问题我不确定如何解决。当我将代理请求转发到服务器并获得响应时,如果我不知道从服务器发送到代理的数据格式(即我不知道服务器的响应是否为type-length-value scheme的形式,也不知道“\r\n”是否表示来自服务器的消息的结束),那么我该如何知道服务器何时已经向我发送了所有所需的信息。有人告诉我,当我的TCP连接读取大小为零或小于期望读取大小时,我应该假设我从服务器连接获取了所有数据。然而,这对我来说似乎不正确。一般情况下可能不正确的原因如下:
假设服务器由于某种原因只以每次写入一个字节的方式写入其套接字,但是响应“真实”客户端的总长度要长得多得多。因此,当代理读取与服务器连接的TCP套接字时,代理只读取一个字节,如果它循环得足够快(在接收更多数据之前进行读取),那么就会读取零并错误地得出结论:它已经获得了客户端想要接收的所有消息。
解决这个问题的一种方法是在每次从套接字读取后等待一段时间,以便代理程序不会比它接收到的字节更快地循环。我担心的原因是,假设存在网络分区,我无法再与服务器通信。然而,与我断开连接的时间并不足以超时TCP连接。因此,难道不可能我尝试再次从TCP套接字读取服务器(比我获取数据更快),并且读取零并错误地得出结论其为全部数据,然后将其发送回客户端吗?(请记住,我想要保持的承诺是:只有当我写入客户端连接时,我才向客户端发送完整的消息。因此,如果代理程序在已经向客户端写入之后的较晚时间再次读取连接,并在稍后的时间(可能在响应其他请求期间)发送丢失的部分,那么这种行为是非法的。)
我编写的代码在go-playground.中。
我喜欢使用以下类比来解释为什么我认为这种方法不起作用:
假设我们有一杯水,代理每次从服务器读取数据时都会喝掉杯中一半的水,但服务器每次只放入一茶匙水。因此,如果代理喝得比获取茶匙的速度快,它可能会很快就把杯子喝空,并得出结论说它的套接字是空的,可以继续移动!如果我们想保证每次发送完整的消息,则这是错误的。要么,这个类比是错误的,TCP的某些“魔法”使其工作,要么假定直到套接字为空的算法就是错的。

这里有一个处理类似问题的链接建议读取直到EOF。然而,我不确定为什么这样做是正确的。读取EOF是否意味着我得到了预期的消息?每次有人向TCP套接字写入一块字节时,都会发送一个EOF吗(例如,我担心如果服务器一次只写一个字节,那么它会发送1个EOF)?然而,EOF可能是TCP连接如何实际工作的“魔法”之一?发送EOF是否会关闭连接?如果是的话,这不是我想使用的方法。此外,我无法控制服务器可能正在做什么(即我不知道它想要多频繁地写入套接字以向代理发送数据,但可以合理地假设它使用某种“标准/正常的写入算法”来写入套接字)。我只是不确定从服务器的套接字读取到EOF是否正确。为什么会这样?我甚至什么时候才能读取到EOFEOF是数据的一部分还是在TCP头中?

另外,我写的关于在超时时间以下加入“等待”(wait)的想法,这个方法只是在平均情况下有效还是在最坏情况下也有效?我也意识到,如果Wait()调用的时间比超时时间长,那么如果你返回到tcp连接并且它没有任何东西,那么就可以安全地继续执行。然而,如果它没有任何东西并且我们不知道服务器发生了什么,那么我们会超时。因此,关闭连接是安全的(因为超时会这样做)。因此,我认为如果Wait调用至少与超时时间一样长,这个过程就有效了!大家怎么看?
我还对能够解释为什么这个算法在某些情况下有效的答案感兴趣。例如,我想,即使服务器每次只写一个字节,如果部署场景是一个紧凑的数据中心,那么平均而言,由于延迟非常小而等待调用几乎肯定足够,那么这个算法不是很好吗?
此外,我写的代码有陷入“死锁”的风险吗?
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()
}

https://github.com/nf/gohttptun 可能对你有用。然而,它已经过时;在 Go v1.0 之前编写,可能已经失效。 - LoveGandhi
1
通常,代理只是对它们读取的每个数据位进行“写入”,而不尝试等待写入者“真正完成”。恐怕,如果不了解协议,就无法保持“您想要保持的承诺”。 - twotwotwo
1
此外,即使您能确保一次只“写入”整个消息(例如如果您知道长度),也不确定这对客户端会有多大改变:网络层仍然可以拆分消息,并且代表后半部分的数据包可能会延迟很长时间。 - twotwotwo
twotwotwo 的意思是,TCP 是一种流协议,而不是基于消息的协议。解决方案与您想象的同时更简单,只需同时复制流,并且稍微复杂一些;您必须考虑可能在两个连接中出现的错误组合。 - JimB
1个回答

15

除非您正在使用特定的高级协议,否则没有要从客户端读取并中继到服务器的“消息”。TCP是一种流协议,您只能来回传输字节。

好消息是在go语言中这非常容易实现,该代理的核心部分将是:

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

显然,这里缺少错误处理并且无法完全关闭,但是清楚地显示了核心数据传输的处理方式。


1
你好,你能提供一个更详细的例子来说明如何干净地关闭这个模式吗?我在很多地方都看到了这个例子,但在通常的错误情况下,如何确保它能干净地关闭呢? - Ralph Caraveo
1
@RalphCaraveo:请查看此代码片段,了解我使用的模式。 - JimB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接