golang:TCP總結
在TCP/IP協議中,“IP地址+TCP或UDP埠號”唯一標識網路通訊中的一個程序。“IP地址+埠號”就對應一個socket。欲建立連線的兩個程序各自有一個socket來標識,那麼這兩個socket組成的socket pair就唯一標識一個連線。因此可以用Socket來描述網路連線的一對一關係。
常用的Socket型別有兩種:流式Socket(SOCK_STREAM)和資料報式Socket(SOCK_DGRAM)。流式是一種面向連線的Socket,針對於面向連線的TCP服務應用;資料報式Socket是一種無連線的Socket,對應於無連線的UDP服務應用。
套接字通訊原理示意
TCP的C/S架構
在整個通訊過程中,伺服器端有兩個socket參與進來,但用於通訊的只有conn這個socket。它是由 listener建立的。隸屬於伺服器端。客戶端有一個socket參與進來。
net.Listen()
建立一個用於連線監聽的套接字
listen.Accept()
// 阻塞監聽客戶端連線請求,成功用於連線,返回用於通訊的socket
net.Dial()
客戶端向服務端發起連線建立一個socket連線
併發的C/S模型通訊
Server
Accept()函式的作用是等待客戶端的連結,如果客戶端沒有連結,該方法會阻塞。如果有客戶端連結,那麼該方法返回一個Socket負責與客戶端進行通訊。所以,每來一個客戶端,該方法就應該返回一個Socket與其通訊,因此,可以使用一個死迴圈,將Accept()呼叫過程包裹起來。
需要注意,實現併發處理多個客戶端資料的伺服器,就需要針對每一個客戶端連線,單獨產生一個Socket,並建立一個單獨的goroutine與之完成通訊。
package main import ( "fmt" "net" "strings" ) func handleConnect(conn net.Conn){ var ( b []byte err error n int ) fmt.Println(conn.RemoteAddr(),"建立連線.") defer conn.Close() b = make([]byte,4096) // 客戶端可能持續不斷的傳送資料,因此接收資料的過程可以放在for迴圈中,服務端也持續不斷的向客戶端返回處理後的資料。 for { n,err = conn.Read(b) content := strings.Trim(string(b[:n]),"\r\n") // window中傳送的內容存在換行符,作為判斷時需要刪除 // 當客戶端退出,服務端從chan中讀取內容時是沒有的,因此的到0 或者客戶端主動退出輸入exit或者quit if n == 0 || content == "exit" || content == "quit" { fmt.Println("客戶端退出:",conn.RemoteAddr()) return } if err != nil { fmt.Println(err) return } if _,err = conn.Write([]byte(fmt.Sprintf("server reply:%s",b[:n])));err !=nil { fmt.Println(err) return } fmt.Println("client send: ",content) } } func main() { var ( listener net.Listener err error conn net.Conn ) // 建立一個用於連線監聽的套接字 if listener, err = net.Listen("tcp", "10.0.0.1:8088"); err != nil { fmt.Println(err) return } defer listener.Close() fmt.Println("waiting client connect.") // 阻塞監聽客戶端連線請求,成功用於連線,返回用於通訊的socket for { if conn, err = listener.Accept(); err != nil { fmt.Println(err) return } go handleConnect(conn) } }
使用nc作為客戶端向服務端傳送資訊
自定義客戶端
客戶端需要持續的向服務端傳送資料,同時也要接收從服務端返回的資料。因此可將傳送和接收放到不同的協程中。
- 主協程迴圈接收伺服器回發的資料(該資料應已轉換為大寫),並列印至螢幕;
- 子協程迴圈從鍵盤讀取使用者輸入資料。
- 讀取鍵盤輸入可使用
os.Stdin.Read()
。
注意事項:
- 服務端有對 exit返回的是
io.EOF
- 當服務端斷開時,chan讀取的資訊就為0了即服務端已經退出,如果客戶端不退出會一直報錯
package main
import (
"fmt"
"io"
"net"
"os"
"strings"
)
func main() {
var (
conn net.Conn
err error
n int
)
if conn, err = net.Dial("tcp", "10.0.0.1:8088"); err != nil {
fmt.Println(err, 111)
return
}
defer conn.Close()
go func() {
str := make([]byte, 1024)
for {
n, err := os.Stdin.Read(str)
content := strings.ToLower(strings.Trim(string(str[:n]), "\r\n"))
if n == 0 {
fmt.Println("與服務端斷開連線")
return
}
if err == io.EOF || content == "quit" {
return
}
if err != nil {
fmt.Println(1, err)
continue
}
_, err = conn.Write([]byte(content))
if err != nil {
fmt.Println(111, err)
return
}
}
}()
byt := make([]byte, 1024)
for {
if _, err = conn.Read(byt); err != nil {
if err == io.EOF {
return
}
fmt.Println(err)
continue
}
fmt.Println("server reply:", string(byt[:n]))
}
}