草庐IT

sockets - 在封闭的net.Conn上写入,但返回nil错误

coder 2023-06-27 原文

对话很便宜,所以我们在这里输入简单的代码:

package main

import (
    "fmt"
    "time"
    "net"
)

func main() {
    addr := "127.0.0.1:8999"

    // Server
    go func() {
        tcpaddr, err := net.ResolveTCPAddr("tcp4", addr)
        if err != nil {
            panic(err)
        }
        listen, err := net.ListenTCP("tcp", tcpaddr)
        if err != nil {
            panic(err)
        }
        for  {
            if conn, err := listen.Accept(); err != nil {
                panic(err)
            } else if conn != nil {
                go func(conn net.Conn) {
                    buffer := make([]byte, 1024)
                    n, err := conn.Read(buffer)
                    if err != nil {
                        fmt.Println(err)
                    } else {
                        fmt.Println(">", string(buffer[0 : n]))
                    }
                    conn.Close()
                }(conn)
            }
        }
    }()

    time.Sleep(time.Second)

    // Client
    if conn, err := net.Dial("tcp", addr); err == nil {
        for i := 0; i < 2; i++ {
            _, err := conn.Write([]byte("hello"))
            if err != nil {
                fmt.Println(err)
                conn.Close()
                break
            } else {
                fmt.Println("ok")
            }
            // sleep 10 seconds and re-send
            time.Sleep(10*time.Second)
        }
    } else {
        panic(err)
    }

}

输出:
> hello
ok
ok

客户端两次写入服务器。第一次读取后,服务器立即关闭连接,但是客户端 sleep 10秒钟,然后使用已关闭的相同的连接对象(conn)重新写入服务器。

为什么第二次写入成功(返回的错误为nil)?

有人可以帮忙吗?

PS:

为了检查系统的缓冲功能是否影响第二次写入的结果,我像这样编辑了Client,但是它仍然成功:
// Client
if conn, err := net.Dial("tcp", addr); err == nil {
    _, err := conn.Write([]byte("hello"))
    if err != nil {
        fmt.Println(err)
        conn.Close()
        return
    } else {
        fmt.Println("ok")
    }
    // sleep 10 seconds and re-send
    time.Sleep(10*time.Second)

    b := make([]byte, 400000)
    for i := range b {
        b[i] = 'x'
    }
    n, err := conn.Write(b)
    if err != nil {
        fmt.Println(err)
        conn.Close()
        return
    } else {
        fmt.Println("ok", n)
    }
    // sleep 10 seconds and re-send
    time.Sleep(10*time.Second)
} else {
    panic(err)
}

这是屏幕截图:
attachment

最佳答案

您的方法存在几个问题。

序言

第一个是您不必等待服务器goroutine
去完成。
在Go语言中,无论出于何种原因退出main()
所有其他仍在运行的goroutine(如果有的话)只是
强行拆除。

您正在尝试使用计时器“同步”事物,
但这仅适用于玩具情况,即使如此
只会不时这样做。

因此,让我们先修复您的代码:

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    addr := "127.0.0.1:8999"

    tcpaddr, err := net.ResolveTCPAddr("tcp4", addr)
    if err != nil {
        log.Fatal(err)
    }
    listener, err := net.ListenTCP("tcp", tcpaddr)
    if err != nil {
        log.Fatal(err)
    }

    // Server
    done := make(chan error)
    go func(listener net.Listener, done chan<- error) {
        for {
            conn, err := listener.Accept()
            if err != nil {
                done <- err
                return
            }
            go func(conn net.Conn) {
                var buffer [1024]byte
                n, err := conn.Read(buffer[:])
                if err != nil {
                    log.Println(err)
                } else {
                    log.Println(">", string(buffer[0:n]))
                }
                if err := conn.Close(); err != nil {
                    log.Println("error closing server conn:", err)
                }
            }(conn)
        }
    }(listener, done)

    // Client
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        log.Fatal(err)
    }
    for i := 0; i < 2; i++ {
        _, err := conn.Write([]byte("hello"))
        if err != nil {
            log.Println(err)
            err = conn.Close()
            if err != nil {
                log.Println("error closing client conn:", err)
            }
            break
        }
        fmt.Println("ok")
        time.Sleep(2 * time.Second)
    }

    // Shut the server down and wait for it to report back
    err = listener.Close()
    if err != nil {
        log.Fatal("error closing listener:", err)
    }
    err = <-done
    if err != nil {
        log.Println("server returned:", err)
    }
}

I've spilled a couple of minor fixes like using log.Fatal (which is log.Print + os.Exit(1)) instead of panicking, removed useless else clauses to adhere to the coding standard of keeping the main flow where it belongs, and lowered the client's timeout. I have also added checking for possible errors Close on sockets may return.



有趣的是,我们现在通过关闭监听器,然后等待服务器goroutine正确报告来正确关闭服务器(不幸的是,在这种情况下,Go不会从net.Listener.Accept返回自定义类型的错误,因此我们无法真正检查该Accept已退出,因为我们已经关闭了监听器)。
无论如何,我们的goroutine现在已正确同步,并且
没有不确定的行为,因此我们可以推断代码的工作方式。

仍然存在的问题

仍然存在一些问题。

更明显的是您错误地假设TCP保留了
消息边界-即,如果您向客户端写“hello”
套接字的末尾,服务器回读“hello”。
这是不正确的:TCP会考虑连接的两端
作为产生和消耗不透明的字节流。
这意味着,当客户端写“hello”时,客户端的
TCP堆栈可以自由传递“he”,并可以延迟发送“llo”,
并且服务器的堆栈可以自由地向read产生“hell”
在套接字上调用,仅返回“o”(可能还返回其他一些值)
数据)在后面的read中。

因此,要使代码“真实”,您需要以某种方式引入这些
消息边界进入TCP之上的协议(protocol)。
在这种情况下,最简单的方法是
使用由固定长度和双方同意的“消息”组成的
endianness前缀,指示以下内容的长度
数据,然后是字符串数据本身。
然后,服务器将使用类似
var msg [4100]byte
_, err := io.ReadFull(sock, msg[:4])
if err != nil { ... }
mlen := int(binary.BigEndian.Uint32(msg[:4]))
if mlen < 0 {
  // handle error
}
if mlen == 0 {
  // empty message; goto 1
}
_, err = io.ReadFull(sock, msg[5:5+mlen])
if err != nil { ... }
s := string(msg[5:5+mlen])

另一种方法是同意消息不包含
换行符,并以换行符终止每个消息
(ASCII LF,\n,0x0a)。
然后,服务器端将使用类似
通常的 bufio.Scanner loop可以得到
socket 上的整行。

您的方法剩下的问题是不处理
套接字上的Read返回什么:请注意io.Reader.Read(这是套接字实现的功能)
从中读取一些数据后返回错误
基础流。在您的玩具示例中,这可能是正确的
无关紧要,但是假设您正在编写类似wget的代码
能够恢复文件下载的工具:即使
从服务器读取返回一些数据和错误,您
必须先处理返回的块,然后再处理
处理错误。

回到眼前的问题

我相信问题中出现的问题仅是因为在您的设置中由于消息的长度太短而遇到了一些TCP缓冲问题。

在运行Linux 4.9/amd64的机器上,有两件事可靠地“修复”了
问题:
  • 正在发送长度为4000字节的消息:第二个调用
    Write立即“看到”问题。
  • 进行更多的Write调用。

  • 对于前者,请尝试类似
    msg := make([]byte, 4000)
    for i := range msg {
        msg[i] = 'x'
    }
    for {
        _, err := conn.Write(msg)
        ...
    

    对于后者-就像
    for {
        _, err := conn.Write([]byte("hello"))
        ...
        fmt.Println("ok")
        time.Sleep(time.Second / 2)
    }
    

    (降低在发送内容之间的停顿时间是明智的
    两种情况)。

    有趣的是,前面的例子write: connection reset by peer(POSIX中的ECONNRESET)
    错误,而第二个命中write: broken pipe(POSIX中的EPIPE)。

    这是因为当我们分批发送值(value)4k字节的数据块时,
    为流生成的一些数据包设法变成
    连接的服务器端进行管理之前的“运行中”
    将有关其关闭的信息传播给客户端,
    这些数据包撞到已经关闭的套接字并被拒绝
    设置了RST TCP标志。
    在第二个示例中,尝试发送另一个数据块
    看到客户端已经知道连接
    已被拆除,并且发送失败而没有“触碰”
    电线”。

    TL; DR,底线

    欢迎来到网络的美好世界。 ;-)

    我建议购买一份“TCP/IP Illustrated”的副本,
    阅读并进行实验。
    TCP(以及IP和IP之上的其他协议(protocol))
    有时工作不像人们期望的那样通过应用
    他们的“常识”。

    关于sockets - 在封闭的net.Conn上写入,但返回nil错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51317968/

    有关sockets - 在封闭的net.Conn上写入,但返回nil错误的更多相关文章

    1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

      作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

    2. ruby-openid:执行发现时未设置@socket - 2

      我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

    3. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

      大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

    4. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    5. ruby - 如何模拟 Net::HTTP::Post? - 2

      是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou

    6. Ruby 写入和读取对象到文件 - 2

      好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

    7. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

      如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

    8. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

      我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

    9. ruby-on-rails - Rails 中的 NoMethodError::MailersController#preview undefined method `activation_token=' for nil:NilClass - 2

      似乎无法为此找到有效的答案。我正在阅读Rails教程的第10章第10.1.2节,但似乎无法使邮件程序预览正常工作。我发现处理错误的所有答案都与教程的不同部分相关,我假设我犯的错误正盯着我的脸。我已经完成并将教程中的代码复制/粘贴到相关文件中,但到目前为止,我还看不出我输入的内容与教程中的内容有什么区别。到目前为止,建议是在函数定义中添加或删除参数user,但这并没有解决问题。触发错误的url是http://localhost:3000/rails/mailers/user_mailer/account_activation.http://localhost:3000/rails/mai

    10. ruby - 检查字符串是否包含散列中的任何键并返回它包含的键的值 - 2

      我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案

    随机推荐