Go语言Telnet回音服务器——TCP服务器的基本结构
发布时间:2022-12-30 15:49:17

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 (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {

	// 创建一个程序结束码的通道
	exitChan := make(chan int)

	// 将服务器并发运行
	go server("127.0.0.1:7001", exitChan)

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

	// 标记程序返回值,并退出
	os.Exit(code)
}

// 服务逻辑,传入地址和退出的通道
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)
	}
}

// 连接的会话逻辑
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
		}
	}

}
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

}