C語言中文網 目錄
首頁 > Go語言教程 > Go語言并發 閱讀:2,367

Go語言Telnet回音服務器——TCP服務器的基本結構

Telnet 協議是 TCP/IP 協議族中的一種。它允許用戶(Telnet客戶端)通過一個協商過程與一個遠程設備進行通信。本例將使用一部分 Telnet 協議與服務器進行通信。

服務器的網絡庫為了完整展示自己的代碼實現了完整的收發過程,一般比較傾向于使用發送任意封包返回原數據的邏輯。這個過程類似于對著大山高喊,大山把你的聲音原樣返回的過程。也就是回音(Echo)。本節使用 Go 語言中的 Socket、goroutine 和通道編寫一個簡單的 Telnet 協議的回音服務器。

回音服務器的代碼分為 4 個部分,分別是接受連接、會話處理、Telnet 命令處理和程序入口。

接受連接

回音服務器能同時服務于多個連接。要接受連接就需要先創建偵聽器,偵聽器需要一個偵聽地址和協議類型。就像你想賣東西,需要先確認賣什么東西,賣東西的類型就是協議類型,然后需要一個店面,店面位于街區的某個位置,這就是偵聽器的地址。一個服務器可以開啟多個偵聽器,就像一個街區可以有多個店面。街區上的編號對應的就是地址中的端口號,如下圖所示。


圖:IP和端口號
 
  • 主機 IP:一般為一個 IP 地址或者域名,127.0.0.1 表示本機地址。
  • 端口號:16 位無符號整型值,一共有 65536 個有效端口號。

通過地址和協議名創建偵聽器后,可以使用偵聽器響應客戶端連接。響應連接是一個不斷循環的過程,就像到銀行辦理業務時,一般是排隊處理,前一個人辦理完后,輪到下一個人辦理。

我們把每個客戶端連接處理業務的過程叫做會話。在會話中處理的操作和接受連接的業務并不沖突可以同時進行。就像銀行有 3 個窗口,喊號器會將用戶分配到不同的柜臺。這里的喊號器就是 Accept 操作,窗口的數量就是 CPU 的處理能力。因此,使用 goroutine 可以輕松實現會話處理和接受連接的并發執行。

如下圖清晰地展現了這一過程。


圖:Socket 處理過程

Go語言中可以根據實際會話數量創建多個goroutine,并自動的調度它們的處理。

telnet 服務器處理:
package main

import (
    "fmt"
    "net"
)

// 服務邏輯, 傳入地址和退出的通道
func server(address string, exitChan chan int) {

    // 根據給定地址進行偵聽
    l, err := net.Listen("tcp", address)

    // 如果偵聽發生錯誤, 打印錯誤并退出
    if err != nil {
        fmt.Println(err.Error())
        exitChan <- 1
    }

    // 打印偵聽地址, 表示偵聽成功
    fmt.Println("listen: " + address)

    // 延遲關閉偵聽器
    defer l.Close()

    // 偵聽循環
    for {

        // 新連接沒有到來時, Accept是阻塞的
        conn, err := l.Accept()

        // 發生任何的偵聽錯誤, 打印錯誤并退出服務器
        if err != nil {
            fmt.Println(err.Error())
            continue
        }

        // 根據連接開啟會話, 這個過程需要并行執行
        go handleSession(conn, exitChan)
    }
}
代碼說明如下:
  • 第 9 行,接受連接的入口,address 為傳入的地址,退出服務器使用 exitChan 的通道控制。往 exitChan 寫入一個整型值時,進程將以整型值作為程序返回值來結束服務器。
  • 第 12 行,使用 net 包的 Listen() 函數進行偵聽。這個函數需要提供兩個參數,第一個參數為協議類型,本例需要做的是 TCP 連接,因此填入“tcp”;address 為地址,格式為“主機:端口號”。
  • 第 15 行,如果偵聽發生錯誤,通過第 17 行,往 exitChan 中寫入非 0 值結束服務器,同時打印偵聽錯誤。
  • 第 24 行,使用 defer,將偵聽器的結束延遲調用。
  • 第 27 行,偵聽開始后,開始進行連接接受,每次接受連接后需要繼續接受新的連接,周而復始。
  • 第 30 行,服務器接受了一個連接。在沒有連接時,Accept() 函數調用后會一直阻塞。連接到來時,返回 conn 和錯誤變量,conn 的類型是 *tcp.Conn。
  • 第 33 行,某些情況下,連接接受會發生錯誤,不影響服務器邏輯,這時重新進行新連接接受。
  • 第 39 行,每個連接會生成一個會話。這個會話的處理與接受邏輯需要并行執行,彼此不干擾。

會話處理

每個連接的會話就是一個接收數據的循環。當沒有數據時,調用 reader.ReadString 會發生阻塞,等待數據的到來。一旦數據到來,就可以進行各種邏輯處理。

回音服務器的基本邏輯是“收到什么返回什么”,reader.ReadString 可以一直讀取 Socket 連接中的數據直到碰到期望的結尾符。這種期望的結尾符也叫定界符,一般用于將 TCP 封包中的邏輯數據拆分開。下例中使用的定界符是回車換行符(“\r\n”),HTTP 協議也是使用同樣的定界符。使用 reader.ReadString() 函數可以將封包簡單地拆分開。

如下圖所示為 Telnet 數據處理過程。


圖:Telnet 數據處理過程

回音服務器需要將收到的有效數據通過 Socket 發送回去。

Telnet會話處理:
package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

// 連接的會話邏輯
func handleSession(conn net.Conn, exitChan chan int) {

    fmt.Println("Session started:")

    // 創建一個網絡連接數據的讀取器
    reader := bufio.NewReader(conn)

    // 接收數據的循環
    for {

        // 讀取字符串, 直到碰到回車返回
        str, err := reader.ReadString('\n')

        // 數據讀取正確
        if err == nil {

            // 去掉字符串尾部的回車
            str = strings.TrimSpace(str)

            // 處理Telnet指令
            if !processTelnetCommand(str, exitChan) {
                conn.Close()
                break
            }

            // Echo邏輯, 發什么數據, 原樣返回
            conn.Write([]byte(str + "\r\n"))

        } else {
            // 發生錯誤
            fmt.Println("Session closed")
            conn.Close()
            break
        }
    }

}
代碼說明如下:
  • 第 11 行是會話入口,傳入連接和退出用的通道。handle Session() 函數被并發執行。
  • 第 16 行,使用 bufio 包的 NewReader() 方法,創建一個網絡數據讀取器,這個 Reader 將輸入數據的讀取過程進行封裝,方便我們迅速獲取到需要的數據。
  • 第 19 行,會話處理開始時,從 Socket 連接,通過 reader 讀取器讀取封包,處理封包后需要繼續讀取從網絡發送過來的下一個封包,因此需要一個會話處理循環。
  • 第 22 行,使用 reader.ReadString() 方法進行封包讀取。內部會自動處理粘包過程,直到下一個回車符到達后返回數據。這里認為封包來自 Telnet,每個指令以回車換行符(“\r\n”)結尾。
  • 第 25 行,數據讀取正常時,返回 err 為 nil。如果發生連接斷開、接收錯誤等網絡錯誤時,err 就不是 nil 了。
  • 第 28 行,reader.ReadString 讀取返回的字符串尾部帶有回車符,使用 strings.TrimSpace() 函數將尾部帶的回車和空白符去掉。
  • 第 31 行,將 str 字符串傳入 Telnet 指令處理函數 processTelnetCommand() 中,同時傳入退出控制通道 exitChan。當這個函數返回 false 時,表示需要關閉當前連接。
  • 第 32 行和第 33 行,關閉當前連接并退出會話接受循環。
  • 第 37 行,將有效數據通過 conn 的 Write() 方法寫入,同時在字符串尾部添加回車換行符(“\r\n”),數據將被 Socket 發送給連接方。
  • 第 41~43 行,處理當 reader.ReadString() 函數返回錯誤時,打印錯誤信息并關閉連接,退出會話并接收循環。

Telnet命令處理

Telnet 是一種協議。在操作系統中可以在命令行使用 Telnet 命令發起 TCP 連接。我們一般用 Telnet 來連接 TCP 服務器,鍵盤輸入一行字符回車后,即被發送到服務器上。

在下例中,定義了以下兩個特殊控制指令,用以實現一些功能:
  • 輸入“@close”退出當前連接會話。
  • 輸入“@shutdown”終止服務器運行。

Telnet命令處理:
package main

import (
    "fmt"
    "strings"
)

func processTelnetCommand(str string, exitChan chan int) bool {

    // @close指令表示終止本次會話
    if strings.HasPrefix(str, "@close") {

        fmt.Println("Session closed")

        // 告訴外部需要斷開連接
        return false

        // @shutdown指令表示終止服務進程
    } else if strings.HasPrefix(str, "@shutdown") {

        fmt.Println("Server shutdown")

        // 往通道中寫入0, 阻塞等待接收方處理
        exitChan <- 0

        // 告訴外部需要斷開連接
        return false
    }

    // 打印輸入的字符串
    fmt.Println(str)

    return true

}
代碼說明如下:
  • 第 8 行,處理 Telnet 命令的函數入口,傳入有效字符并退出通道。
  • 第 11~16 行,當輸入字符串中包含“@close”前綴時,在第 16 行返回 false,表示需要關閉當前會話。
  • 第 19~27 行,當輸入字符串中包含“@shutdown”前綴時,第 24 行將 0 寫入 exitChan,表示結束服務器。
  • 第 31 行,沒有特殊的控制字符時,打印輸入的字符串。

程序入口

Telnet 回音處理主流程:
package main

import (
    "os"
)

func main() {

    // 創建一個程序結束碼的通道
    exitChan := make(chan int)

    // 將服務器并發運行
    go server("127.0.0.1:7001", exitChan)

    // 通道阻塞, 等待接收返回值
    code := <-exitChan

    // 標記程序返回值并退出
    os.Exit(code)
}
代碼說明如下:
  • 第 10 行,創建一個整型的無緩沖通道作為退出信號。
  • 第 13 行,接受連接的過程可以并發操作,使用 go 將 server() 函數開啟 goroutine。
  • 第 16 行,從 exitChan 中取出返回值。如果取不到數據就一直阻塞。
  • 第 19 行,將程序返回值傳入 os.Exit() 函數中并終止程序。

編譯所有代碼并運行,命令行提示如下:

listen: 127.0.0.1:7001

此時,Socket 偵聽成功。在操作系統中的命令行中輸入:

telnet 127.0.0.1 7001

嘗試連接本地的 7001 端口。接下來進入測試服務器的流程。

測試輸入字符串

在 Telnet 連接后,輸入字符串 hello,Telnet 命令行顯示如下:

$ telnet 127.0.0.1 7001
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
hello
hello

服務器顯示如下:

listen: 127.0.0.1:7001
Session started:
hello

客戶端輸入的字符串會在服務器中顯示,同時客戶端也會收到自己發給服務器的內容,這就是一次回音。

測試關閉會話

當輸入 @close 時,Telnet 命令行顯示如下:

@close
Connection closed by foreign host

服務器顯示如下:

Session closed

此時,客戶端 Telnet 與服務器斷開連接。

測試關閉服務器

當輸入 @shutdown 時,Telnet 命令行顯示如下:

@shutdown
Connection closed by foreign host

服務器顯示如下:

Server shutdown

此時服務器會自動關閉。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo