第十五章修改 (#834)

Co-authored-by: Joe Chen <jc@unknwon.io>
This commit is contained in:
Haigang Zhou
2022-05-17 16:33:20 +08:00
committed by GitHub
parent 60fe3dd076
commit 72f2eccbc5
12 changed files with 102 additions and 92 deletions

View File

@@ -1,6 +1,6 @@
# 15.1 tcp 服务器
这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个web服务器应用需要响应众多客户端的并发请求Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。
这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个 (web) 服务器应用需要响应众多客户端的并发请求Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。
服务器端代码是一个单独的文件:
@@ -46,7 +46,7 @@ func doServerStuff(conn net.Conn) {
}
```
`main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于 TCP 协议)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 for 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
`main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(基于 TCP 协议下,位于 IP 地址为 127.0.0.1端口为 50000 的 localhost)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 `for` 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len()` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
客户端代码写在另一个文件 client.go 中:
@@ -91,21 +91,23 @@ func main() {
}
}
```
客户端通过 `net.Dial` 创建了一个和服务器之间的连接。
客户端通过 `net.Dial()` 创建了一个和服务器之间的连接。
它通过无限循环从 `os.Stdin` 接收来自键盘的输入直到输入了“Q”。注意裁剪 `\r``\n` 字符(仅 Windows 平台需要)。裁剪后的输入被 `connection``Write` 方法发送到服务器。
它通过无限循环从 `os.Stdin` 接收来自键盘的输入直到输入了“Q”。注意裁剪 `\r``\n` 字符(仅 Windows 平台需要)。裁剪后的输入被 `connection``Write()` 方法发送到服务器。
当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:`对tcp 127.0.0.1:50000发起连接时产生错误由于目标计算机的积极拒绝而无法创建连接`
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:
打开命令提示符并转到服务器和客户端可执行程序所在的目录Windows 系统下输入 `server.exe`(或者只输入 `server` Linux 系统下输入 `./server`
```
dial tcp [::1]:xxxx: connectex: No connection could be made because the target machine actively refused it.
```
接下来控制台出现以下信息:`Starting the server ...`
打开命令提示符并转到服务器和客户端代码所在的目录,输入 `go run server.go`接下来控制台出现以下信息:`Starting the server ...`
在 Windows 系统中,我们可以通过 CTRL/C 停止程序。
在 Windows 系统中,我们可以通过 CTRL+C 停止程序。
然后开启 2 个或者 3 个独立的控制台窗口,分别输入 client 回车启动客户端程序
然后开启 2 个或者 3 个独立的控制台窗口,分别启动客户端程序
以下是服务器的输出:
@@ -115,12 +117,12 @@ Received data: IVO says: Hi Server, what's up ?
Received data: CHRIS says: Are you busy server ?
Received data: MARC says: Don't forget our appointment tomorrow !
```
当客户端输入 Q 并结束程序时,服务器会输出以下信息:
当客户端输入“Q”并结束程序时,服务器会输出以下信息:
```
Error reading WSARecv tcp 127.0.0.1:50000: The specified network name is no longer available.
```
在网络编程中 `net.Dial` 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 `Conn` 类型的接口,我们可以用它发送和接收数据。`Dial` 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6TCP 或者 UDP 都可以使用这个公用接口。
在网络编程中 `net.Dial()` 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 `Conn` 类型的接口,我们可以用它发送和接收数据。`Dial()` 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6TCP 或者 UDP 都可以使用这个公用接口。
以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址:
@@ -152,7 +154,7 @@ func checkConnection(conn net.Conn, err error) {
fmt.Printf("Connection is made with %v\n", conn)
}
```
下边也是一个使用 net 包从 socket 中打开,写入,读取数据的例子:
下边也是一个使用 `net` 包从 socket 中打开,写入,读取数据的例子:
示例 15.4 [socket.go](examples/chapter_15/socket.go)
@@ -191,11 +193,11 @@ func main() {
**练习 15.1**
编写新版本的客户端和服务器[client1.go](exercises/chapter_15/client1.go) / [server1.go](exercises/chapter_15/server1.go)
编写新版本的客户端和服务器 ([client1.go](exercises/chapter_15/client1.go) / [server1.go](exercises/chapter_15/server1.go))
* 增加一个检查错误的函数 `checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 [示例 15.14](examples/chapter_15/template_validation.go) 中它是如何被解决的
* 使客户端可以通过发送一条命令 SH 来关闭服务器
* 让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送 WHO 指令的时候,服务器将显示如下列表:
* 让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送 `WHO` 指令的时候,服务器将显示如下列表:
```
This is the client list: 1:active, 0=inactive
User IVO is 1
@@ -292,7 +294,7 @@ func checkError(error error, info string) {
}
}
```
**译者注:应该是由于 go 版本的更新,会提示 os.EAGAIN undefined ,修改后的代码:[simple_tcp_server_v1.go](examples/chapter_15/simple_tcp_server_v1.go)**
**译者注:应该是由于 Go 版本的更新,会提示 os.EAGAIN undefined修改后的代码:[simple_tcp_server_v1.go](examples/chapter_15/simple_tcp_server_v1.go)**
都有哪些改进?
@@ -303,13 +305,13 @@ if flag.NArg() != 2 {
panic("usage: host port")
}
```
传入的参数通过 `fmt.Sprintf` 函数格式化成字符串
传入的参数通过 `fmt.Sprintf()` 函数格式化成字符串
```go
hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
```
*`initServer` 函数中通过 `net.ResolveTCPAddr` 得到了服务器地址和端口,这个函数最终返回了一个 `*net.TCPListener`
* 每一个连接都会以协程的方式运行 `connectionHandler` 函数。函数首先通过 `conn.RemoteAddr()` 获取到客户端的地址并显示出来
* 它使用 `conn.Write` 发送 Go 推广消息给客户端
*`initServer()` 函数中通过 `net.ResolveTCPAddr()` 得到了服务器地址和端口,这个函数最终返回了一个 `*net.TCPListener`
* 每一个连接都会以协程的方式运行 `connectionHandler()` 函数。函数首先通过 `conn.RemoteAddr()` 获取到客户端的地址并显示出来
* 它使用 `conn.Write()` 发送 Go 推广消息给客户端
* 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 `switch` 语句 `default` 分支,退出无限循环并关闭连接。如果是操作系统的 `EAGAIN` 错误,它会重试。
* 所有的错误检查都被重构在独立的函数 `checkError` 中,当错误产生时,利用错误上下文来触发 panic。
@@ -323,7 +325,7 @@ Connection from: 127.0.0.1:49347
<25:Marc says: Do you remembe><25:r our first meeting serve><2:r?>
```
net.Error
**net.Error**
`net` 包返回的错误类型遵循惯例为 `error`,但有些错误实现包含额外的方法,他们被定义为 `net.Error` 接口:
```go
package net