Go語言實現TCP通訊

minch發表於2023-03-28

TCP協議為傳輸控制協議,TCP協議有以下幾個特點:
1. TCP是面向連線的傳輸層協議;
2. 每條TCP連線只能有兩個端點,每條TCP連線是點到點的通訊;
3. TCP提供可靠的交付服務,保證傳送的資料無差錯,不丟失,不重要且有序;
4. TCP提供全雙工通訊,允許雙方在任何時候都能傳送資料,為此TCP連線的兩端都設有傳送快取和接收快取,用來臨時存放雙向通訊的資料;
5. TCP是面向位元組流的;

傳送快取用來暫存以下資料:
① 傳送應用程式傳送給傳送方TCP準備傳送的資料;
② TCP已傳送但尚未收到確認的資料;

接收快取用來暫存以下資料:
① 按序到達但尚未被接收應用程式讀取的資料;
② 不按序到達的資料;

因為是面向連線的協議,資料像水流一樣傳輸,會存在黏包問題。

TCP連線
一個TCP服務端可以同時連線很多個客戶端,Go語言可以使用go關鍵字開啟goroutine,每建立一個連線就建立一個goroutine,這樣可以併發執行每一個建立的連線,tcp服務端主要的處理流程有:1. 監聽埠;2. 接收客戶端請求建立tcp連線;3. 使用go關鍵字開啟goroutine處理每一個建立的連線,收發資料;4. 關閉連線;tcp客戶端主要的處理流程有:1. 建立與服務端的連線;2. 收發資料;3. 關閉連線;關於tcp通訊我們一般會使用到bufio,net,strings,os包,其中bufio包主要用來做輸入輸出資料的快取,bufio.NewReader()函式可以傳遞os.Stdin型別(任何實現了io.Reader介面中的read()方法都可以作為引數傳遞進來,通常是傳遞一個實現了io.Reader介面中的read()方法的結構體或者是結構體指標),os.Stdin為標準輸入,我們可以接受讀取從控制檯輸入的資料,os.NewReader()函式返回的是一個新的帶有4096 byte大小緩衝區的Reader結構體指標型別,透過Reader結構體指標型別我們就可以呼叫很多的方法,比如可以呼叫ReadString(delim byte) (string,error)方法,ReadString方法可以一直從標準輸入中讀取資料,直到遇到指定的終止符號delim,並且讀取的內容會包含當前的delim,所以bufio包主要用來對輸入和輸出的資料進行緩衝;net包主要是連線的監聽、建立,以及連線資料的讀取和寫入(例如tcp,udp等網路程式設計)等相關工作,tcp服務端中可以使用一個process()函式傳遞一個net.Conn型別也即*net.TCPConn型別的變數conn(TCPConn結構體實現了Conn介面),*TCPConn型別那麼就可以呼叫很多的方法,因為我們需要從tcp連線中讀取客戶端或者是服務端傳送的資料,所以可以呼叫*TCPConn型別的read()方法讀取tcp連線的資料,然後輸出資料即可,服務端可以使用net.Listen()函式監聽連線,使用Accept()方法建立tcp連線,客戶端則可以使用net.Dial()連線建立的tcp連線,使用conn介面的實現*TCPConn(Dial()方法的返回值就是*TCPConn型別)呼叫conn的read()方法將讀取tcp連線的內容,write()方法將資料寫入到tcp連線中;strings包主要是對讀取到的資料的字串形式進行處理,比如去除掉字串的一些符號等等。

下面的程式碼需要先執行服務端的程式碼,再執行客戶端的程式碼:

TCP服務端

package main
 
import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)
 
// TCP 服務端
func process(conn net.Conn) {
	// 函式執行完之後關閉連線
	defer conn.Close()
	// 輸出主函式傳遞的conn可以發現屬於*TCPConn型別, *TCPConn型別那麼就可以呼叫*TCPConn相關型別的方法, 其中可以呼叫read()方法讀取tcp連線中的資料
	fmt.Printf("服務端: %T\n", conn)
	for {
		var buf [128]byte
		// 將tcp連線讀取到的資料讀取到byte陣列中, 返回讀取到的byte的數目
		n, err := conn.Read(buf[:])
		if err != nil {
			// 從客戶端讀取資料的過程中發生錯誤
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("服務端收到客戶端發來的資料:", recvStr)
		// 由於是tcp連線所以雙方都可以傳送資料, 下面接收服務端傳送的資料這樣客戶端也可以收到對應的資料
		inputReader := bufio.NewReader(os.Stdin)
		s, _ := inputReader.ReadString('\n')
		t := strings.Trim(s, "\r\n")
		// 向當前建立的tcp連線傳送資料, 客戶端就可以收到服務端傳送的資料
		conn.Write([]byte(t))
	}
}
 
func main() {
	// 監聽當前的tcp連線
	listen, err := net.Listen("tcp", "127.0.0.1:20000")
	fmt.Printf("服務端: %T=====\n", listen)
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立連線
		fmt.Println("當前建立了tcp連線")
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		// 對於每一個建立的tcp連線使用go關鍵字開啟一個goroutine處理
		go process(conn) 
	}
}

  

TCP客戶端

package main
 
import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)
 
func main() {
    // 連線到服務端建立的tcp連線
    conn, err := net.Dial("tcp", "127.0.0.1:20000")
    // 輸出當前建Dial函式的返回值型別, 屬於*net.TCPConn型別
    fmt.Printf("客戶端: %T\n", conn)
    if err != nil {
        // 連線的時候出現錯誤
        fmt.Println("err :", err)
        return
    }
    // 當函式返回的時候關閉連線
    defer conn.Close() 
    // 獲取一個標準輸入的*Reader結構體指標型別的變數
    inputReader := bufio.NewReader(os.Stdin)
    for {
        // 呼叫*Reader結構體指標型別的讀取方法
        input, _ := inputReader.ReadString('\n') // 讀取使用者輸入
        // 去除掉\r \n符號
        inputInfo := strings.Trim(input, "\r\n")
        // 判斷輸入的是否是Q, 如果是Q則退出
        if strings.ToUpper(inputInfo) == "Q" { // 如果輸入q就退出
            return
        }
        _, err = conn.Write([]byte(inputInfo)) // 傳送資料
        if err != nil {
            return
        }
        buf := [512]byte{}
        // 讀取服務端傳送的資料
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Println("recv failed, err:", err)
            return
        }
        fmt.Println("客戶端接收服務端傳送的資料: ", string(buf[:n]))
    }
}

 

相關文章