当 JSON 作为响应发送但使用纯字符串时,Go 并发 TCP 服务器挂起

Posted

技术标签:

【中文标题】当 JSON 作为响应发送但使用纯字符串时,Go 并发 TCP 服务器挂起【英文标题】:Go concurrent TCP server hangs when JSON is sent as the response but works with plain string 【发布时间】:2021-11-06 18:10:47 【问题描述】:

我正在尝试在 Go 中实现一个并发 TCP 服务器,并在 linode 中找到了这个很好的解释 article,它通过示例代码清楚地解释了。客户端和服务器的示例代码 sn-ps,包括在下面。

并发 TCP 服务器,其中为每个 TCP 客户端创建一个新的 go-routine。

package main

import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strconv"
        "strings"
)

var count = 0

func handleConnection(c net.Conn) 
        fmt.Print(".")
        for 
                netData, err := bufio.NewReader(c).ReadString('\n')
                if err != nil 
                        fmt.Println(err)
                        return
                

                temp := strings.TrimSpace(string(netData))
                if temp == "STOP" 
                        break
                
                fmt.Println(temp)
                counter := strconv.Itoa(count) + "\n"
                c.Write([]byte(string(counter)))
        
        c.Close()


func main() 
        arguments := os.Args
        if len(arguments) == 1 
                fmt.Println("Please provide a port number!")
                return
        

        PORT := ":" + arguments[1]
        l, err := net.Listen("tcp4", PORT)
        if err != nil 
                fmt.Println(err)
                return
        
        defer l.Close()

        for 
                c, err := l.Accept()
                if err != nil 
                        fmt.Println(err)
                        return
                
                go handleConnection(c)
                count++
        


TCP客户端代码sn-p

package main

import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strings"
)

func main() 
        arguments := os.Args
        if len(arguments) == 1 
                fmt.Println("Please provide host:port.")
                return
        

        CONNECT := arguments[1]
        c, err := net.Dial("tcp", CONNECT)
        if err != nil 
                fmt.Println(err)
                return
        

        for 
                reader := bufio.NewReader(os.Stdin)
                fmt.Print(">> ")
                text, _ := reader.ReadString('\n')
                fmt.Fprintf(c, text+"\n")

                message, _ := bufio.NewReader(c).ReadString('\n')
                fmt.Print("->: " + message)
                if strings.TrimSpace(string(text)) == "STOP" 
                        fmt.Println("TCP client exiting...")
                        return
                
        

上述并发 TCP 服务器和客户端工作没有任何问题。当我更改 TCP 服务器以发送 JSON 响应而不是文本响应时,问题就出现了。当我换行时:

counter := strconv.Itoa(count) + "\n"
c.Write([]byte(string(counter)))

res, err := json.Marshal(IdentitySuccessMessageType: "newidentity", Approved: "approved")
if err != nil 
    fmt.Printf("Error: %v", err)

c.Write(res)

服务器挂起并且没有向客户端发送任何响应。奇怪的是,当我用 Ctrl+C 强行关闭服务器时,服务器将响应发送给客户端。对这种奇怪的行为有任何想法吗?这就像服务器持有响应并在它存在时发送它。

【问题讨论】:

【参考方案1】:

该套接字教程,就像许多其他破坏设计的套接字教程一样,根本没有解释什么是应用程序协议或为什么需要它。它只是说:

在本例中,您实现了一个基于 TCP 的非官方协议。

这个“非官方”协议是最基本的:消息由换行符分隔 (\n)。

除了学习有关套接字的基础知识之外,您不应该在任何环境中使用这样的套接字。

您需要一个应用程序协议来构建消息(以便您的客户端和服务器可以识别部分和连接的消息)。

所以简短的回答是:在您的 JSON 之后发送 \n。长答案:不要像这样使用准系统套接字,使用应用程序协议,例如 HTTP。

【讨论】:

非常感谢您的回答。它与最后的“\ n”一起使用。这是我的大学项目,我必须直接使用套接字来学习基础知识。 通过\n 分隔消息的TCP 协议是一个很好且完整的协议,如果它足以胜任这项工作,就没有任何问题。毕竟,HTTP 1 也用 \r\n\r\n 分隔标头。 @rustyx 我称它为“初级”,而不是“不好”。这是一个协议,只是不是一个非常有用的协议。但是它有一些问题:它会阻止您发送换行符,这在格式化的 JSON 中很常见。 @CodeCaster 也可以通过那种协议传递 JSON。 play.golang.org/p/z_fvEdmb4OM 恕我直言,这是一个非常好的协议,只要您了解与预先宣布长度的协议的差异。 @mh-cbon 您现在已经更改了协议,并添加了“JSON 已编码发送,接收方必须对其进行解码”。我想说的是,博客甚至没有描述他们协议的定义——没有一句话解释\n 的作用或为什么需要它。【参考方案2】:

注意数据竞争。您正在从没有同步机制的不同例程中写入和读取counter 变量。没有良性数据竞争。

您的实现还不会成功,因为您没有同时测试客户端查询。

通过使用-race 标志构建程序来启用竞争检测器,例如go run -race . / go build -race .

我已使用 atomic package 函数修复了数据争用问题。

在下面的代码中,我已将您的代码调整为使用bufio.Scanner 而不是bufio.Reader,仅用于演示目的。


    input := bufio.NewScanner(src)
    output := bufio.NewScanner(c)
    for input.Scan() 
        text := input.Text()
        fmt.Fprintf(c, "%v\n", text)
        isEOT := text == "STOP"

        if !output.Scan() 
            fmt.Fprintln(os.Stderr, output.Err())
            return
        
        message := output.Text()
        fmt.Print("->: " + message)
        if isEOT 
            fmt.Println("All messages sent...")
            return
        
    

我还调整了 main 序列以模拟 2 个连续的客户端,使用我在此过程中重置的预定义缓冲区输入。


    input := `hello
world!
STOP
nopnop`
    test := strings.NewReader(input)

    go serve(arguments[1])
  
    test.Reset(input)
    query(arguments[1], test)

    test.Reset(input)
    query(arguments[1], test)

我在您的客户端中添加了一个简单的重试器,它可以帮助我们编写简单的代码。

    c, err := net.Dial("tcp", addr)
    for 
        if err != nil 
            fmt.Fprintln(os.Stderr, err)
            <-time.After(time.Second)
            c, err = net.Dial("tcp", addr)
            continue
        
        break
    

整个程序被组装到一个文件中,不是很好阅读输出,但更容易传输和执行。

https://play.golang.org/p/keKQsKA3fAw

在下面的示例中,我演示了如何使用 json marshaller / unmarshaller 来交换结构化数据。

    input := bufio.NewScanner(src)
    dst := json.NewEncoder(c)
    output := json.NewDecoder(c)
    for input.Scan() 
        text := input.Text()
        isEOT := text == "STOP"

        err = dst.Encode(text)
        if err != nil 
            fmt.Fprintln(os.Stderr, err)
            return
        

        var tmp interface
        err = output.Decode(&tmp)
        if err != nil 
            fmt.Fprintln(os.Stderr, err)
            return
        
        fmt.Printf("->: %v\n", tmp)
        if isEOT 
            fmt.Println("All messages sent...")
            return
        
    

但是!请注意,最后一个版本对恶意用户很敏感。与bufio.Scannerbufio.Reader 不同,它不检查线路上读取的数据量。所以它可能会累积数据直到OOM

对于服务器端来说尤其如此,在

defer c.Close()
        defer atomic.AddUint64(&count, ^uint64(0))
        input := json.NewDecoder(c)
        output := json.NewEncoder(c)
        fmt.Print(".")
        for 
            var netData interface
            input.Decode(&netData)
            fmt.Printf("%v", netData)
            count := atomic.LoadUint64(&count)
            output.Encode(count)
            if x, ok := netData.(string); ok && x == "STOP" 
                break
            
        

https://play.golang.org/p/LpIu4ofpm9e

在您的最后一段代码中,正如 CodeCaster 所回答的那样,不要忘记使用适当的分隔符框住您的消息。

【讨论】:

以上是关于当 JSON 作为响应发送但使用纯字符串时,Go 并发 TCP 服务器挂起的主要内容,如果未能解决你的问题,请参考以下文章

Dojo xhrget 响应

权威证明问题中的 Go-ethereum 私有网络:调用合约方法但没有响应

当 HTML 作为 JSON 请求的参数发送时,具有多个 CSS 类的 HTML 不会呈现

将大型响应 Json 作为多部分数据或多个部分发送

使用 Twig 生成纯 JSON 响应是不是合适?

将 MySQL blob 内容作为 json 响应发送