关于go:golang中的socket编程

32次阅读

共计 5677 个字符,预计需要花费 15 分钟才能阅读完成。

0.1、索引

https://waterflow.link/articles/1664591292871

1、tcp 的 3 次握手(建设连贯)

  1. 客户端的协定栈向服务器端发送了 SYN 包,并通知服务器端以后发送序列号 j,客户端进入 SYNC_SENT 状态;
  2. 服务器端的协定栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,示意对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,通知客户端以后我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
  3. 客户端协定栈收到 ACK 之后,使得应用程序从 connect 调用返回,示意客户端到服务器端的单向连贯建设胜利,客户端的状态为 ESTABLISHED,同时客户端协定栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
  4. 应答包达到服务器端后,服务器端协定栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连贯也建设胜利,服务器端也进入 ESTABLISHED 状态。

2、tcp 的 4 次挥手(敞开连贯)

  1. 一方应用程序调用 close,咱们称该方为被动敞开方,该端的 TCP 发送一个 FIN 包,示意须要敞开连贯。之后被动敞开方进入 FIN_WAIT_1 状态。
  2. 接管到这个 FIN 包的对端执行被动敞开。这个 FIN 由 TCP 协定栈解决,咱们晓得,TCP 协定栈为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序能够通过 read 调用来感知这个 FIN 包。肯定要留神,这个 EOF 会被放在 已排队等待的其余已接管的数据之后,这就意味着接收端应用程序须要解决这种异常情况,因为 EOF 示意在该连贯上再无额定数据达到。此时,被动敞开方进入 CLOSE_WAIT 状态。
  3. 被动敞开方将读到这个 EOF,于是,应用程序也调用 close 敞开它的套接字,这导致它的 TCP 也发送一个 FIN 包。这样,被动敞开方将进入 LAST_ACK 状态。
  4. 被动敞开方接管到对方的 FIN 包,并确认这个 FIN 包。被动敞开方进入 TIME_WAIT 状态,而接管到 ACK 的被动敞开方则进入 CLOSED 状态。进过 2MSL 工夫之后,被动敞开方也进入 CLOSED 状态。

3、socket 中的连贯建设和敞开

我看先看下流程:

  1. 服务端调用 socket、bind 绑定 ip 端口、listen 开启服务端监听。
  2. accept 阻塞期待下次调用,并返回一个 tcp 连贯。
  3. 客户端调用 connect 连贯服务端。
  4. 此时服务端 accept 完结阻塞,代表客户端和服务端胜利建设连贯。
  5. 而后就是数据交互读写读写。
  6. 当客户端连贯敞开时,服务端的 read 办法会读取一个 io.EOF 的谬误,代表客户端敞开连贯。服务端收到敞开连贯的谬误后也调用 close 敞开连贯。

4、golang 中的连贯建设

咱们先看下服务端:

package main

import (
    "fmt"
    "net"
)

func main() {
    server := ":8330"
    tcpAddr, err := net.ResolveTCPAddr("tcp", server)
    if err != nil {fmt.Println("resolve err:", err)
        return
    }

  // 监听某个端口的 tcp 网络
    listen, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {fmt.Println("listen err:", err)
        return
    }
    defer listen.Close()

    for {
    // 期待下次申请过去并建设连贯
        conn, err := listen.Accept()
        if err != nil {fmt.Println("accept err:", err)
            continue
        }

    // 在这个连贯上做一些事件
        go handler(conn)

    }
}

func handler(conn net.Conn) {}
  1. 首先咱们定义好 ip 和端口,开启监听
  2. 而后调用 accept 期待下次申请过去,并建设 tcp 连贯

咱们运行下下面的代码:

go run server.go

而后在另一个 shell 中执行上面的命令:

watch -d 'netstat -nat |grep"8330"'Every 2.0s: netstat -nat |grep"8330"                                                 userdeMacBook-Pro.local: Thu Sep 29 16:38:42 2022

tcp46      0      0  *.8330                 *.*                    LISTEN

能够看到此时 8330 端口曾经开启监听

客户端:

package main

import (
    "fmt"
    "net"
)

func main() {
    serverAddr := ":8330"

    tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
    if err != nil {fmt.Println("resolve err:", err)
        return
    }

  // 发动一个 tcp 的网络拨号
    _, err = net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {fmt.Println("dial err:", err)
        return
    }


    closed := make(chan bool)


  // 客户端阻塞不间接敞开
    for  {
        select {
        case <-closed:
            fmt.Println("服务端敞开")
            return
        }
    }

}

其中外围的办法就是 net.DialTCP,第一个参数会返回一个建设胜利的连贯,第二个参数会返回没建设胜利的错误信息。

而后咱们命令行执行下:

go run client.go

接着看下 watch -d 'netstat -nat |grep"8330"' 的返回,这个命令是实时的,所以不须要反复执行

Every 2.0s: netstat -nat |grep "8330"                                                 userdeMacBook-Pro.local: Thu Sep 29 16:45:57 2022

tcp4       0      0  127.0.0.1.8330         127.0.0.1.59146        ESTABLISHED
tcp4       0      0  127.0.0.1.59146        127.0.0.1.8330         ESTABLISHED
tcp46      0      0  *.8330                 *.*                    LISTEN

能够看到客户端服务端,服务端和客户端都胜利建设了连贯(连贯是否建设胜利不是看是否有条线真连上了,连贯状态是保护在各个端的)

同时咱们也能够在 wireshark 中看到三次握手建设连贯的流程:

5、golang 中的读和写

咱们当初略微批改下服务端的代码:

package main

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

func main() {
    server := ":8330"
    tcpAddr, err := net.ResolveTCPAddr("tcp", server)
    if err != nil {fmt.Println("resolve err:", err)
        return
    }

    listen, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {fmt.Println("listen err:", err)
        return
    }
    defer listen.Close()

    for {conn, err := listen.Accept()
        if err != nil {fmt.Println("accept err:", err)
            continue
        }

        go handler(conn)

    }
}

func handler(conn net.Conn) {go func() {
        for  {
      // 指定从 buffer 中读取数据的最大容量
            var buf = make([]byte, 1024)
      // 从 buffer 中读取数据并保留到 buf 中,n 代表理论返回的数据大小
            n, err := conn.Read(buf)
            if err != nil {
        // 客户端敞开会触发 EOF
                if err == io.EOF {conn.Close()
                    return
                }
                fmt.Println("read err:", err)
                return
            }

            fmt.Println("read data", n, ":", string(buf))
        }
    }()


    curTime := time.Now().String()
  // 数据写到缓冲区
    _, err := conn.Write([]byte(curTime))
    if err != nil {fmt.Println("write err:", err)
        return
    }
    fmt.Println("send data:", curTime)

}

首先要明确,操作系统内核会为每个连贯的客户端和服务端调配 发送缓冲区 接收缓冲区

  1. 当客户端须要发送数据到服务端,调用 conn.Write 从客户端缓冲区发送数据到操作系统内核的发送缓冲区。理论所做的事件是把数据 从应用程序缓冲区中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
  2. 数据通过 tcp 发送到服务端的接收缓冲区,而后服务端的程序从接收缓冲区读取数据。

非阻塞 I /O,当应用程序调用非阻塞 I/O 实现某个操作时,内核立刻返回,不会把 CPU 工夫切换给其余过程,应用程序在返回后,能够失去足够的 CPU 工夫持续实现其余事件。

读操作:如果套接字对应的接收缓冲区没有数据可读,在非阻塞状况下 read 调用会立刻返回,个别返回 EWOULDBLOCK 或 EAGAIN 出错信息。

写操作:在非阻塞 I/O 的状况下,如果套接字的发送缓冲区已达到了极限,不能包容更多的字节,那么操作系统内核会 尽最大可能 从应用程序拷贝数据到发送缓冲区中,并立刻从 write 等函数调用中返回。可想而知,在拷贝动作产生的霎时,有可能一个字符也没拷贝,有可能所有申请字符都被拷贝实现,那么这个时候就须要返回一个数值,通知应用程序到底有多少数据被胜利拷贝到了发送缓冲区中,应用程序须要再次调用 write 函数,以输入未实现拷贝的字节。

非阻塞 I/O 操作:拷贝→返回→再拷贝→再返回。

阻塞 I/O 操作:拷贝→直到所有数据拷贝至发送缓冲区实现→返回。

golang 中底层应用的还是非阻塞的 I /O,然而在代码层面做了一些解决,让用户感觉是以阻塞形式调用的。

...

for {n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
        if err != nil {
            n = 0
      // 非阻塞形式调用,如果遇到 syscall.EAGAIN 报错,代表没拿到数据,持续循环
            if err == syscall.EAGAIN && fd.pd.pollable() {if err = fd.pd.waitRead(fd.isFile); err == nil {continue}
            }
        }
        err = fd.eofError(n, err)
        return n, err
    }

...

6、golang 中的敞开

在 socket 中,当客户端调用 close()办法时,其实就是发送一个 FIN 标记位,意思就是我要被动敞开 TCP 连贯了。Close 办法会让对端的所有读写操作完结阻塞,并返回。

在 golang 中调用 Close 办法,会让对端的 Read 读取到 EOF 的谬误,此时就代表我想敞开连贯。对端接管到敞开的申请后也能够调用 Close 办法敞开连贯。

客户端:

package main

import (
    "fmt"
    "io"
    "net"
)

func main() {
    serverAddr := ":8330"

    tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
    if err != nil {fmt.Println("resolve err:", err)
        return
    }

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {fmt.Println("dial err:", err)
        return
    }


    closed := make(chan bool)
    go func() {
        for  {var buf = make([]byte, 1024)
            n, err := conn.Read(buf)
            if err != nil {
        // 读取到 EOF,服务端敞开连贯
                if err == io.EOF {conn.Close()
                    closed <- true
                    return
                }
                fmt.Println("read err:", err)
                return
            }

            fmt.Println("read data", n, ":", string(buf))
        }
    }()

    for  {
        select {
        case <-closed:
            fmt.Println("服务端敞开")
            return
        }
    }

}

服务端:

package main

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

func main() {
    server := ":8330"
    tcpAddr, err := net.ResolveTCPAddr("tcp", server)
    if err != nil {fmt.Println("resolve err:", err)
        return
    }

    listen, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {fmt.Println("listen err:", err)
        return
    }
    defer listen.Close()

    for {conn, err := listen.Accept()
        if err != nil {fmt.Println("accept err:", err)
            continue
        }

        go handler(conn)

    }
}

func handler(conn net.Conn) {go func() {
        for  {var buf = make([]byte, 1024)
            n, err := conn.Read(buf)
            if err != nil {
        // 读取到 EOF,客户端敞开连贯
                if err == io.EOF {conn.Close()
                    return
                }
                fmt.Println("read err:", err)
                return
            }

            fmt.Println("read data", n, ":", string(buf))
        }
    }()


    curTime := time.Now().String()
    _, err := conn.Write([]byte(curTime))
    if err != nil {fmt.Println("write err:", err)
        return
    }
    fmt.Println("send data:", curTime)

}

参考:

《极客工夫:网络编程实战》

正文完
 0