tcp 服务器
这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个 (web) 服务器应用需要响应众多客户端的并发请求:Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。
服务器端代码是一个单独的文件:
示例 15.1 server.go
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("Starting the server ...")
// 创建 listener
listener, err := net.Listen("tcp", "localhost:50000")
if err != nil {
fmt.Println("Error listening", err.Error())
return //终止程序
}
// 监听并接受来自客户端的连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting", err.Error())
return // 终止程序
}
go doServerStuff(conn)
}
}
func doServerStuff(conn net.Conn) {
for {
buf := make([]byte, 512)
len, err := conn.Read(buf)
if err != nil {
fmt.Println("Error reading", err.Error())
return //终止程序
}
fmt.Printf("Received data: %v", string(buf[: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 中:
示例 15.2 client.go
客户端通过 net.Dial() 创建了一个和服务器之间的连接。
它通过无限循环从 os.Stdin 接收来自键盘的输入,直到输入了“Q”。注意裁剪 和 字符(仅 Windows 平台需要)。裁剪后的输入被 connection 的 Write() 方法发送到服务器。
当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:
打开命令提示符并转到服务器和客户端代码所在的目录,输入 go run server.go,接下来控制台出现以下信息:Starting the server ...
在 Windows 系统中,我们可以通过 CTRL+C 停止程序。
然后开启 2 个或者 3 个独立的控制台窗口,分别启动客户端程序
以下是服务器的输出:
当客户端输入“Q”并结束程序时,服务器会输出以下信息:
在网络编程中 net.Dial() 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 Conn 类型的接口,我们可以用它发送和接收数据。Dial() 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6,TCP 或者 UDP 都可以使用这个公用接口。
以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址:
示例 15.3 dial.go
下边也是一个使用 net 包从 socket 中打开,写入,读取数据的例子:
示例 15.4 socket.go
练习 15.1
编写新版本的客户端和服务器 (client1.go / server1.go):
增加一个检查错误的函数
checkError(error);讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 示例 15.14 中它是如何被解决的使客户端可以通过发送一条命令 SH 来关闭服务器
让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送
WHO指令的时候,服务器将显示如下列表:
注意:当服务器运行的时候,你无法编译/连接同一个目录下的源码来产生一个新的版本,因为 server.exe 正在被操作系统使用而无法被替换成新的版本。
下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!
示例 15.5 simple_tcp_server.go:
(译者注:应该是由于 Go 版本的更新,会提示 os.EAGAIN undefined,修改后的代码:simple_tcp_server_v1.go)
都有哪些改进?
服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过
flag包来读取这些参数。这里使用了flag.NArg()检查是否按照期望传入了 2 个参数:
传入的参数通过 fmt.Sprintf() 函数格式化成字符串
在
initServer()函数中通过net.ResolveTCPAddr()得到了服务器地址和端口,这个函数最终返回了一个*net.TCPListener每一个连接都会以协程的方式运行
connectionHandler()函数。函数首先通过conn.RemoteAddr()获取到客户端的地址并显示出来它使用
conn.Write()发送 Go 推广消息给客户端它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入
switch语句default分支,退出无限循环并关闭连接。如果是操作系统的EAGAIN错误,它会重试。所有的错误检查都被重构在独立的函数
checkError中,当错误产生时,利用错误上下文来触发 panic。
在命令行中输入 simple_tcp_server localhost 50000 来启动服务器程序,然后在独立的命令行窗口启动一些 client.go 的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址:
net.Error: net 包返回的错误类型遵循惯例为 error,但有些错误实现包含额外的方法,他们被定义为 net.Error 接口:
通过类型断言,客户端代码可以测试 net.Error,从而区分是临时发生的还是必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。
链接
上一节:网络、模版与网页应用
下一节:一个简单的网页服务器
Last updated
Was this helpful?