在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程。
因此可以用Socket來描述網絡連接的一對一關系。
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對于面向連接的TCP服務應用;數據報式Socket是一種無連接的Socket,對應于無連接的UDP服務應用。
1 TCP的C/S架構
1.1 單服務版本
服務端代碼:
package main import ( "fmt" "net") func main() { // 創建監控 listener, err := net.Listen("tcp", "localhost:8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close() // 主協程結束時,關閉listener fmt.Println("服務器等待客戶端建立連接...") // 等待客戶端連接請求 conn, err := listener.Accept() if err != nil { fmt.Println("accpet err:", err) return } defer conn.Close() // 使用結束,斷開與客戶端鏈接 fmt.Println("客戶端與服務器連接建立成功...") // 接收客戶端數據 buf := make([]byte, 1024) // 創建1024大小的緩沖區,用于read n, err := conn.Read(buf) // 讀取到n個大小的數據 if err != nil { fmt.Println("read err:", err) return } fmt.Println("服務器讀到:", string(buf[:n])) // 讀多少,打印多少
}
如圖,在整個通信過程中,服務器端有兩個socket參與進來,但用于通信的只有 conn 這個socket。它是由 listener創建的。隸屬于服務器端。
客戶端代碼:
package main import ( "fmt" "net") func main() { // 主動發送連接請求 conn, err := net.Dial("tcp", "localhost:8000") if err != nil { fmt.Println("Dial err", err) } defer conn.Close() // 結束時,關閉連接 // 發送數據 _, err = conn.Write([]byte("Are u ready?")) if err != nil { fmt.Println("Write err:", err) return }
}
1.2 并發服務
并發服務端:
Accept()函數的作用是等待客戶端的鏈接,如果客戶端沒有鏈接,該方法會阻塞。如果有客戶端鏈接,那么該方法返回一個Socket負責與客戶端進行通信。所以,每來一個客戶端,該方法就應該返回一個Socket與其通信,因此,可以使用一個死循環,將Accept()調用過程包裹起來。
需要注意的是,實現并發處理多個客戶端數據的服務器,就需要針對每一個客戶端連接,單獨產生一個Socket,并創建一個單獨的goroutine與之完成通信。
在判斷客戶端數據是否為“exit”字符串時,要注意,客戶端會自動的多發送2個字符:KaTeX parse error: Undefined control sequence: \n at position 4: “\r\?n?”(這在windows系統下代表回車、換行)
服務端代碼:
package main import ( "fmt" "net" "strings") func main() { // 創建監控 listener, err := net.Listen("tcp", "localhost:8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close() // 主協程結束時,關閉listener for { // 等待客戶端連接請求 conn, err := listener.Accept() if err != nil { fmt.Println("accpet err:", err) return } // 處理用戶請求,新建一個協程 go HandleConn(conn) }
} // 處理用戶請求
func HandleConn(conn net.Conn) { // 函數調用完畢,自動關閉conn defer conn.Close() // 獲取客戶端發過來的網址信息 addr := conn.RemoteAddr().String() fmt.Println(addr, "connect successful") buf := make([]byte, 2048) for { // 讀取用戶數據 n, err := conn.Read(buf) if err != nil { fmt.Println("err=", err) return } fmt.Printf("[%s]: %s\n", addr, string(buf[:n])) fmt.Println("len = ", len(string(buf[:n]))) //if string(buf[:n-1]) == "exit" // nc測試,發送時,只有/n if string(buf[:n-2]) == "exit" { fmt.Println(addr, "exit") return } // 將數據轉化為大寫,再給用戶發送 conn.Write([]byte(strings.ToUpper(string(buf[:n])))) }
}
并發客戶端:
客戶端不僅需要持續的向服務端發送數據,同時也要接收從服務端返回的數據。因此可將發送和接收放到不同的協程中。
主協程循環接收服務器回發的數據(該數據應已轉換為大寫),并打印至屏幕;子協程循環從鍵盤讀取用戶輸入數據,寫給服務器。讀取鍵盤輸入可使用 os.Stdin.Read(str)。定義切片str,將讀到的數據保存至str中。
這樣,客戶端也實現了多任務。
package main import ( "fmt" "net" "os") func main() { // 主動發送連接請求 conn, err := net.Dial("tcp", "localhost:8000") if err != nil { fmt.Println("Dial err", err) } defer conn.Close() // 客戶端終止時,關閉于服務器通訊的socket // 啟動子協程: 接受用戶鍵盤輸入發送給服務端 go func() { // 創建用于存儲用戶鍵盤輸入數據的切片緩沖區str := make([]byte, 1024) for { // 反復讀取 n, err := os.Stdin.Read(str) // 獲取用戶鍵盤輸入(阻塞) if err != nil { fmt.Println("os.Stdin.Read err:", err) return } // 從鍵盤讀到的數據,發送給服務端 _, err = conn.Write(str[:n]) if err != nil { fmt.Println("conn.Write err:", err) return } } }() // 主協程: 接受服務端數據,進行打印輸出 buf := make([]byte, 1024) // 定義用于存儲服務器回發數據的切片緩沖區 for { n, err := conn.Read(buf) // 從通信socket中讀數據,存入切片緩沖區(阻塞) if err != nil { fmt.Println("conn.Read err:", err) return } fmt.Printf("服務器回發: %s\n", string(buf[:n])) } }