自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go BIO/NIO探討:基于系統(tǒng)調(diào)用實(shí)現(xiàn)Tcp Echo Server

開發(fā) 前端
這篇文章中,我們利用面向過程編程的思路,只依賴Syscall庫而不是Net庫,實(shí)現(xiàn)一個(gè)簡單的 Echo Server,以更好地理解 Tcp Server 的工作原理。

Go net庫對 tcp server 的支持非常完善,其中最核心的部分依賴系統(tǒng)調(diào)用 socket/bind/listen/accept。這些系統(tǒng)調(diào)用被完好地封裝在syscall庫里, 而且這層封裝屏一定程度上蔽掉了底層操作系統(tǒng)的差異性。

如果你讀過前一篇文章,會發(fā)現(xiàn)net庫應(yīng)用了面向?qū)ο缶幊痰乃悸?,對系統(tǒng)調(diào)用做了很多層封裝。這篇文章中,我們利用面向過程編程的思路,只依賴syscall庫而不是net庫,實(shí)現(xiàn)一個(gè)簡單的 echo server,以更好地理解 tcp server 的工作原理。

服務(wù)流程

  1. 創(chuàng)建套接字: syscall.Socket()。
  2. 綁定套接字和ip:port: syscall.Bind()。
  3. 監(jiān)聽套接字: syscall.Listen()。
  4. for循環(huán):接收tcp connection: syscall.Accept()處理tcp connection: go echo(clientSocketFd)。

通用的一些變量有:

var (
// IPV4協(xié)議
family = syscall.AF_INET
// 基于TCP, 提供有序、可靠、雙向、基于連接的字節(jié)流,不限制消息長度,支持消息的優(yōu)先級傳輸
sotype = syscall.SOCK_STREAM
// protocol = tcp
_ = "tcp"
// ESTABLISHED狀態(tài)的tcp conn隊(duì)列的最大長度
listenBacklog = syscall.SOMAXCONN
// server ip:port
serverip = net.IPv4(0, 0, 0, 0)
serverport = 8080
)

為了方便代碼跳轉(zhuǎn)到linux的實(shí)現(xiàn),可以修改Goland上的GOOS選項(xiàng):

Goland配置

創(chuàng)建套接字

// 創(chuàng)建套接字
sockfd, err := syscall.Socket(family, sotype, 0)
if err != nil {
panic(fmt.Errorf("fails to create socket: %s", err))
}
syscall.CloseOnExec(sockfd)

net庫將套接字設(shè)置為 SOCK_NONBLOCK,非阻塞模式下 accept/read/write 有時(shí)候會返回 EWOULDBLOCK 或 EAGAIN 錯(cuò)誤,需要利用 wait 機(jī)制去實(shí)現(xiàn)goroutine的阻塞,增加了編程的復(fù)雜度。這里我們使用默認(rèn)的阻塞模式。如果想要實(shí)驗(yàn)非阻塞模式,可以參考下面這段代碼:

// Nonblock 處理起來太復(fù)雜了,先注釋掉這一段
if err := syscall.SetNonblock(sockfd, true); err != nil {
syscall.Close(sockfd)
log.Printf("setnonblock error=%v\n", err)
os.Exit(-1)
}

綁定套接字和ip:port

// ipToSockaddrInet4 是從 net/tcpsock_posix.go 抄的
addr, err := ipToSockaddrInet4(serverip, serverport)
if err != nil {
panic(fmt.Sprintf("fails to convert address %s:%d to socket addr, err=%s",
serverip, serverport,
err))
}

if err := syscall.Bind(sockfd, &addr); err != nil {
panic(fmt.Sprintf("fails to bind socket %d to address %s:%d, err=%s",
sockfd,
serverip, serverport,
err))
}

監(jiān)聽套接字

syscall.Listen函數(shù)修改sockfd的狀態(tài)為 LISTEN,內(nèi)核開始監(jiān)聽套接字。

if err := syscall.Listen(sockfd, listenBacklog); err != nil {
log.Printf("listen sockfd %d to addr error=%v\n", sockfd, err)
panic(fmt.Sprintf("fails to listen socket %d", sockfd))
} else {
log.Printf("Started listening on %s:%d", serverip, serverport)
}

for循環(huán) accept

這里 syscall.Accept 仍然采用了阻塞模式。如果要采用非阻塞模式,則需要改成 syscall.Accept4 并傳入 SOCK_NONBLOCK 和 SOCK_CLOEXEC flag。

for {
clientSockfd, clientSockAddr, err := syscall.Accept(sockfd)
if err != nil {
log.Printf("accept sockfd %d error=%v\n", sockfd, err)
continue
}
clientSockAddrInet4 := clientSockAddr.(*syscall.SockaddrInet4)
log.Printf("Connected with new client, sock addr = %v:%d\n", clientSockAddrInet4.Addr, clientSockAddrInet4.Port)
go echo(clientSockfd)
}

一個(gè) ESTABLISHED 套接字代表一個(gè)client端的連接,我們將這個(gè)字段傳給echo函數(shù),實(shí)現(xiàn)復(fù)讀機(jī)功能。echo 會持續(xù)從套接字讀取數(shù)據(jù)到 byte buffer 結(jié)構(gòu)中,然后再寫回到套接字。如果client端關(guān)閉連接,Read/Write 就會失敗,導(dǎo)致函數(shù)退出。

func echo(sockfd int) {
defer func() {
if err := syscall.Close(sockfd); err != nil {
log.Printf("[echo] close sock %v fails, err=%v\n", sockfd, err)
}
}()
var buf [32 * 1024]byte
for {
nRead, err := syscall.Read(sockfd, buf[:])
if err != nil {
log.Printf("fails to read data from sockfd %d, err=%v\n", sockfd, err)
return
}

if _, err := syscall.Write(sockfd, buf[:nRead]); err != nil {
log.Printf("fails to write data %s into sockfd %d, err=%v\n", buf[:nRead], sockfd, err)
return
}
}
}

關(guān)閉套接字

作為一個(gè) Server,我們通常會要求 Graceful Shutdown (不過Gin框架沒有實(shí)現(xiàn)這一點(diǎn))。做法也比較簡單。

  1. 創(chuàng)建一個(gè)容量為0的channel。
  2. 注冊監(jiān)聽哪些操作系統(tǒng)信號。
  3. 在 goroutine 里從channel讀取信號,并做出相應(yīng)的反應(yīng)。
// 接收到Ctrl+C信號后,關(guān)閉socket
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Println("\r- Ctrl+C pressed in Terminal")

if err := syscall.Close(sockfd); err != nil {
log.Printf("Close sockfd %d fails, err=%v\n", sockfd, err)
} else {
log.Printf("Server stopped successfully!!!")
}
// 收到信號后需要處理, 否則程序會永久hang住, 需要kill -9 <pid>
// os.Exit 會導(dǎo)致所有g(shù)oroutine都會立即停止執(zhí)行
os.Exit(0)
}()

我們這里的處理比較簡單,沒有判斷具體是什么信號,只是關(guān)閉套接字,然后退出程序。

這段代碼放在 syscall.Socket 和 syscall.Bind 之間即可。

實(shí)現(xiàn) echo client

echo client的功能是:

  1. 通過 socket, connect 系統(tǒng)調(diào)用建立與tcp server的連接。
  2. 創(chuàng)建 bufio.Reader,從os.Stdin讀取輸入。
  3. for循環(huán): 從stdin讀取輸入,寫入套接字;遇到Ctrl+D退出。

代碼如下:

func main() {
var (
family = syscall.AF_INET
sotype = syscall.SOCK_STREAM
_ = "tcp"
serverip = net.IPv4(0, 0, 0, 0)
serverport = 8080
)

// 創(chuàng)建套接字
sockfd, err := syscall.Socket(family, sotype, 0)
if err != nil {
panic(fmt.Errorf("fails to create socket: %s", err))
}

defer syscall.Close(sockfd)

serverAddr, err := ipToSockaddrInet4(serverip, serverport)
if err != nil {
panic(fmt.Sprintf("fails to convert address %s:%d to socket addr, err=%v", serverip, serverport, err))
}

if err := syscall.Connect(sockfd, &serverAddr); err != nil {
panic(fmt.Errorf("fails to connect sockfd %d to server, err=%v\n", sockfd, err))
}

reader := bufio.NewReader(os.Stdin)
readBuf := make([]byte, 1024)

for {
dataBytes, err := reader.ReadBytes('\n')

if err == io.EOF { // keyboard signal: CTRL-D
log.Printf("Client exits gracefully!!!\n")
return
} else if err != nil {
log.Printf("read error %v, shall exit\n", err)
return
} else {
nWrite, err := syscall.Write(sockfd, dataBytes)
if err != nil {
log.Printf("write sockfd %d fails, error=%#v\n", sockfd, err)
return
} else {
log.Printf("write %d bytes\n", nWrite)
}

nRead, err := syscall.Read(sockfd, readBuf[:])
if err != nil {
log.Printf("read sockfd %d fails, error=%#v\n", sockfd, err)
return
} else {
log.Printf("read %d bytes, data=%s\n", nRead, readBuf[:nRead])
}
}
}
}

測試

為了能夠在Linux下運(yùn)行代碼,可以在機(jī)器上安裝docker,在容器里跑。docker官方提供了 golang:1.19 鏡像,GOPATH 是 /go,我們直接用這個(gè),并把本機(jī)的目錄映射進(jìn)去:

# 刪除之前的容器,如果有
docker rm -f go_app

# 啟動(dòng)容器
docker run -d \
--mount type=bind,source=$HOME/go/src,target=/go/src \
--workdir /go/src/github.com/ \
--name go_app \
--restart always \
golang:1.19 \
sleep infinity

# 進(jìn)入容器的命令行
docker exec -it go_app bash

# cd echo_server directory
go run main.go

Golang鏡像不提供 netstat vim 等命令,需要手動(dòng)在容器里安裝 net-tools 和 vim:

# 查看linux發(fā)行版
cat /etc/issue

# 替換成阿里云 debian 11 的源
# https://developer.aliyun.com/mirror/debian/
cat > /etc/apt/sources.list << EOF \
deb https://mirrors.aliyun.com/debian/ bullseye main non-free contrib \
deb-src https://mirrors.aliyun.com/debian/ bullseye main non-free contrib \
deb https://mirrors.aliyun.com/debian-security/ bullseye-security main \
deb-src https://mirrors.aliyun.com/debian-security/ bullseye-security main \
deb https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib \
deb-src https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib \
deb https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib \
deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib \
EOF

apt update
apt install -y net-tools vim man

一個(gè)觀察

關(guān)于四次揮手的一些觀察:有client連接時(shí),server關(guān)閉后,需要等待一段時(shí)間才能釋放端口。這里挖個(gè)坑,后續(xù)可能不會填了。

下面是一個(gè)server和一個(gè)client的情況:

# netstat -anlop |grep 8080
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1784/main off (0.00/0/0)
tcp 0 0 127.0.0.1:34132 127.0.0.1:8080 ESTABLISHED 1856/main off (0.00/0/0)
tcp 0 0 127.0.0.1:8080 127.0.0.1:34132 ESTABLISHED 1784/main off (0.00/0/0)

Ctrl+C 關(guān)掉server,netstat 返回這樣的結(jié)果:

# netstat -anlop |grep 8080
tcp 1 0 127.0.0.1:34132 127.0.0.1:8080 CLOSE_WAIT 1856/main off (0.00/0/0)
tcp 0 0 127.0.0.1:8080 127.0.0.1:34132 FIN_WAIT2 - timewait (33.07/0/0)

短時(shí)間內(nèi)再次啟動(dòng)server, bind時(shí)會報(bào)錯(cuò) address already in use。再等一段時(shí)間,client端自動(dòng)斷開,server才能啟動(dòng):

# netstat -anlop |grep 8080
tcp 1 0 127.0.0.1:34132 127.0.0.1:8080 CLOSE_WAIT 1856/main off (0.00/0/0)
責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2023-03-07 08:00:12

netpollGo

2023-03-06 08:37:58

JavaNIO

2023-03-09 08:22:57

Go net庫Socket

2020-04-16 15:20:43

PHP前端BIO

2011-12-15 10:56:55

JavaNIO

2011-03-31 10:41:49

BIONIOIO

2022-04-16 16:52:24

Netty網(wǎng)絡(luò)服務(wù)器客戶端程序

2011-12-15 09:40:06

Javanio

2018-09-19 14:53:02

NIOBIO運(yùn)行

2020-10-10 19:37:27

BIO 、NIO 、A

2023-07-11 08:40:02

IO模型后臺

2019-10-18 08:22:43

BIONIOAIO

2021-06-21 11:25:54

GoTLS語言

2021-08-12 18:48:31

響應(yīng)式編程Bio

2021-11-02 12:19:18

Go函數(shù)結(jié)構(gòu)

2011-07-22 17:48:29

IOS Socket TCP

2010-01-19 14:42:43

VB.NET調(diào)用過程重

2024-06-11 00:05:00

CasaOS云存儲管理

2024-05-08 16:44:40

TCPRST網(wǎng)絡(luò)協(xié)議

2022-04-13 07:59:23

IOBIONIO
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號