共计 5677 个字符,预计需要花费 15 分钟才能阅读完成。
0.1、索引
https://waterflow.link/articles/1664591292871
1、tcp 的 3 次握手(建设连贯)
- 客户端的协定栈向服务器端发送了 SYN 包,并通知服务器端以后发送序列号 j,客户端进入 SYNC_SENT 状态;
- 服务器端的协定栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,示意对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,通知客户端以后我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
- 客户端协定栈收到 ACK 之后,使得应用程序从 connect 调用返回,示意客户端到服务器端的单向连贯建设胜利,客户端的状态为 ESTABLISHED,同时客户端协定栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
- 应答包达到服务器端后,服务器端协定栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连贯也建设胜利,服务器端也进入 ESTABLISHED 状态。
2、tcp 的 4 次挥手(敞开连贯)
- 一方应用程序调用 close,咱们称该方为被动敞开方,该端的 TCP 发送一个 FIN 包,示意须要敞开连贯。之后被动敞开方进入 FIN_WAIT_1 状态。
- 接管到这个 FIN 包的对端执行被动敞开。这个 FIN 由 TCP 协定栈解决,咱们晓得,TCP 协定栈为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序能够通过 read 调用来感知这个 FIN 包。肯定要留神,这个 EOF 会被放在 已排队等待的其余已接管的数据之后,这就意味着接收端应用程序须要解决这种异常情况,因为 EOF 示意在该连贯上再无额定数据达到。此时,被动敞开方进入 CLOSE_WAIT 状态。
- 被动敞开方将读到这个 EOF,于是,应用程序也调用 close 敞开它的套接字,这导致它的 TCP 也发送一个 FIN 包。这样,被动敞开方将进入 LAST_ACK 状态。
- 被动敞开方接管到对方的 FIN 包,并确认这个 FIN 包。被动敞开方进入 TIME_WAIT 状态,而接管到 ACK 的被动敞开方则进入 CLOSED 状态。进过 2MSL 工夫之后,被动敞开方也进入 CLOSED 状态。
3、socket 中的连贯建设和敞开
我看先看下流程:
- 服务端调用 socket、bind 绑定 ip 端口、listen 开启服务端监听。
- accept 阻塞期待下次调用,并返回一个 tcp 连贯。
- 客户端调用 connect 连贯服务端。
- 此时服务端 accept 完结阻塞,代表客户端和服务端胜利建设连贯。
- 而后就是数据交互读写读写。
- 当客户端连贯敞开时,服务端的 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) {}
- 首先咱们定义好 ip 和端口,开启监听
- 而后调用 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)
}
首先要明确,操作系统内核会为每个连贯的客户端和服务端调配 发送缓冲区 和接收缓冲区。
- 当客户端须要发送数据到服务端,调用 conn.Write 从客户端缓冲区发送数据到操作系统内核的发送缓冲区。理论所做的事件是把数据 从应用程序缓冲区中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
- 数据通过 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)
}
参考:
《极客工夫:网络编程实战》