Go从入门到精通——示例:Telnet 回音服务器——TCP服务器的基本结构

示例:Telnet 回音服务器——TCP服务器的基本结构

  本节使用 Go 语言中的 Socket、goroutine 和通道编写一个简单的 Telnet 协议的回音服务器。

  回音服务器的代码分成4个部分,分别是接受连接、会话处理、Telnet 命令处理和程序入口。

1、接受连接

  Go 语言中可以根据实际会话数量创建多个 goroutine,并自动的调度它们的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)
    }
}

2、会话处理

  每个连接的会话就是一个接收数据的循环。当没有数据时,调用 reader.ReadString 会发生阻塞,等待数据的到来。一旦数据到来,就可以进行各种逻辑处理。

  回音服务器的基本逻辑是“收到什么返回什么”,reader.ReadString 可以一直读取 Socket 连接中的数据直到碰到期望的结尾符。这种期望的结尾符也叫定界符,一般用于将 TCP 封包中的逻辑数据拆分开。

  下例中使用的定界符是回车换行符(“\r\n”),HTTP 协议也是使用同样的定界符。使用 reader.ReadString() 函数可以将封包简单地拆分开。

  Telnet 数据处理过程:

  回音服务器需要将收到的有效数据通过 Socket 发送回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
        }
    }
}

3、Telnet命令处理

  Telnet 是一种协议。在操作系统可以在命令行使用 Telnet 命令发起 TCP 连接。我们一般用 Telnet 来连接 TCP 服务器,键盘输入一行字符串回车后,即被发送到服务器上。

  • 输入 "@close" 退出当前连接会话。
  • 输入 "@shutdown" 终止服务器运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
 
import (
    "fmt"
    "strings"
)
 
//处理 Telnet 命令的函数入口,传入有效字符并退出通道
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
}

4、程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
 
import "os"
 
func main() {
 
    //创建一个程序结束码的通道
    exitChan := make(chan int)
 
    //将服务器并发运行
    go server("127.0.0.1:7001", exitChan) //服务器逻辑,传入地址和退出通道
 
    //通道阻塞,等待接收返回值
    code := <-exitChan
 
    os.Exit(code)
}

5、完整程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
 
posted @   左扬  阅读(308)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
levels of contents
点击右上角即可分享
微信分享提示