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]))
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM