这部分我们将使用TCP协议和在14章讲到的协程范式编写一个简单的客户端-服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go会为每一个客户端产生一个协程用来处理请求。我们需要使用net包中网络通信的功能。它包含了用于TCP/IP以及UDP协议、域名解析等方法。
服务器代码,单独的一个文件:
示例 15.1 server.go
Copy 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
的变量,他是一个服务器的基本函数:用来监听和接收来自客户端的请求(来自localhost即IP地址为127.0.0.1端口为50000基于TCP协议)。这个Listen()
函数可以返回一个error
类型的错误变量。用一个无限for循环的listener.Accept()
来等待客户端的请求。客户端的请求将产生一个net.Conn
类型的连接变量。然后一个独立的协程使用这个连接执行doServerStuff()
,开始使用一个512字节的缓冲data
来读取客户端发送来的数据并且把它们打印到服务器的终端,len
获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
客户端代码写在另外一个文件client.go中:
示例 15.2 client.go
Copy package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main () {
//打开连接:
conn, err := net. Dial ( "tcp" , "localhost:50000" )
if err != nil {
//由于目标计算机积极拒绝而无法创建连接
fmt. Println ( "Error dialing" , err. Error ())
return // 终止程序
}
inputReader := bufio. NewReader (os.Stdin)
fmt. Println ( "First, what is your name?" )
clientName, _ := inputReader. ReadString ( '\n' )
// fmt.Printf("CLIENTNAME %s", clientName)
trimmedClient := strings. Trim (clientName, "\r\n" ) // Windows 平台下用 "\r\n",Linux平台下使用 "\n"
// 给服务器发送信息直到程序退出:
for {
fmt. Println ( "What to send to the server? Type Q to quit." )
input, _ := inputReader. ReadString ( '\n' )
trimmedInput := strings. Trim (input, "\r\n" )
// fmt.Printf("input:--%s--", input)
// fmt.Printf("trimmedInput:--%s--", trimmedInput)
if trimmedInput == "Q" {
return
}
_, err = conn. Write ([] byte (trimmedClient + " says: " + trimmedInput))
}
}
客户端通过net.Dial
创建了一个和服务器之间的连接
它通过无限循环中的os.Stdin接收来自键盘的输入直到输入了“Q”。注意使用\r
和\n
换行符分割字符串(在windows平台下使用\r\n
)。接下来分割后的输入通过connection
的Write
方法被发送到服务器。
当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接
。
打开控制台并转到服务器和客户端可执行程序所在的目录,Windows系统下输入server.exe(或者只输入server),Linux系统下输入./server。
接下来控制台出现以下信息:Starting the server ...
在Windows系统中,我们可以通过CTRL/C停止程序。
然后开启2个或者3个独立的控制台窗口,然后分别输入client回车启动客户端程序
以下是服务器的输出:
Copy Starting the Server ...
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 并结束程序时,服务器会输出以下信息:
Copy Error reading WSARecv tcp 127.0.0.1:50000: The specified network name is no longer available.
在网络编程中net.Dial
函数是非常重要的,一旦你连接到远程系统,就会返回一个Conn类型接口,我们可以用它发送和接收数据。Dial
函数巧妙的抽象了网络结构及传输。所以IPv4或者IPv6,TCP或者UDP都可以使用这个公用接口。
下边这个示例先使用TCP协议连接远程80端口,然后使用UDP协议连接,最后使用TCP协议连接IPv6类型的地址:
示例 15.3 dial.go
Copy // make a connection with www.example.org:
package main
import (
"fmt"
"net"
"os"
)
func main () {
conn, err := net. Dial ( "tcp" , "192.0.32.10:80" ) // tcp ipv4
checkConnection (conn, err)
conn, err = net. Dial ( "udp" , "192.0.32.10:80" ) // udp
checkConnection (conn, err)
conn, err = net. Dial ( "tcp" , "[2620:0:2d0:200::10]:80" ) // tcp ipv6
checkConnection (conn, err)
}
func checkConnection (conn net . Conn , err error ) {
if err != nil {
fmt. Printf ( "error %v connecting!" , err)
os. Exit ( 1 )
}
fmt. Printf ( "Connection is made with %v \n" , conn)
}
下边也是一个使用net包从socket中打开,写入,读取数据的例子:
示例 15.4 socket.go
Copy package main
import (
"fmt"
"io"
"net"
)
func main () {
var (
host = "www.apache.org"
port = "80"
remote = host + ":" + port
msg string = "GET / \n"
data = make ([] uint8 , 4096 )
read = true
count = 0
)
// 创建一个socket
con, err := net. Dial ( "tcp" , remote)
// 发送我们的消息,一个http GET请求
io. WriteString (con, msg)
// 读取服务器的响应
for read {
count, err = con. Read (data)
read = (err == nil )
fmt. Printf ( string (data[ 0 :count]))
}
con. Close ()
}
练习 15.1
编写新版本的客户端和服务器(client1.go / server1.go ):
增加一个检查错误的函数checkError(error)
;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在示例15.14 中它是如何被解决的
让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送WHO指令的时候,服务器将显示如下列表:
Copy This is the client list: 1:active, 0=inactive
User IVO is 1
User MARC is 1
User CHRIS is 1
注意:当服务器运行的时候,你无法编译/连接同一个目录下的源码来产生一个新的版本,因为server.exe
正在被操作系统使用而无法被替换成新的版本。
下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个tcp服务器的示例 server.go 并且拥有更好的结构,它只用了80行代码!
示例 15.5 simple_tcp_server.go :
Copy // Simple multi-thread/multi-core TCP server.
package main
import (
"flag"
"fmt"
"net"
"os"
)
const maxRead = 25
func main () {
flag. Parse ()
if flag. NArg () != 2 {
panic ( "usage: host port" )
}
hostAndPort := fmt. Sprintf ( " %s : %s " , flag. Arg ( 0 ), flag. Arg ( 1 ))
listener := initServer (hostAndPort)
for {
conn, err := listener. Accept ()
checkError (err, "Accept: " )
go connectionHandler (conn)
}
}
func initServer (hostAndPort string ) * net . TCPListener {
serverAddr, err := net. ResolveTCPAddr ( "tcp" , hostAndPort)
checkError (err, "Resolving address:port failed: '" + hostAndPort + "'" )
listener, err := net. Listen ( "tcp" , serverAddr)
checkError (err, "ListenTCP: " )
println ( "Listening to: " , listener. Addr (). String ())
return listener
}
func connectionHandler (conn net . Conn ) {
connFrom := conn. RemoteAddr (). String ()
println ( "Connection from: " , connFrom)
sayHello (conn)
for {
var ibuf [] byte = make ([] byte , maxRead + 1 )
length, err := conn. Read (ibuf[ 0 :maxRead])
ibuf[maxRead] = 0 // to prevent overflow
switch err {
case nil :
handleMsg (length, err, ibuf)
case os.EAGAIN: // try again
continue
default :
goto DISCONNECT
}
}
DISCONNECT:
err := conn. Close ()
println ( "Closed connection: " , connFrom)
checkError (err, "Close: " )
}
func sayHello (to net . Conn ) {
obuf := [] byte { 'L' , 'e' , 't' , '\'' , 's' , ' ' , 'G' , 'O' , '!' , '\n' }
wrote, err := to. Write (obuf)
checkError (err, "Write: wrote " + string (wrote) + " bytes." )
}
func handleMsg (length int , err error , msg [] byte ) {
if length > 0 {
print ( "<" , length, ":" )
for i := 0 ; ; i ++ {
if msg[i] == 0 {
break
}
fmt. Printf ( " %c " , msg[i])
}
print ( ">" )
}
}
func checkError ( error error , info string ) {
if error != nil {
panic ( "ERROR: " + info + " " + error . Error ()) // terminate program
}
}
(译者注:应该是由于go版本的更新,会提示os.EAGAIN undefined ,修改后的代码: simple_tcp_server_v1.go )
都有哪些改进?
服务器地址和端口不再是硬编码,而是通过命令行传入参数并通过flag
包来读取这些参数。这里使用了flag.NArg()
检查是否按照期望传入了2个参数:
Copy if flag. NArg () != 2 {
panic ( "usage: host port" )
}
传入的参数通过fmt.Sprintf
函数格式化成字符串
Copy hostAndPort := fmt. Sprintf ( " %s : %s " , flag. Arg ( 0 ), flag. Arg ( 1 ))
在initServer
函数中通过net.ResolveTCPAddr
指定了服务器地址和端口,这个函数最终返回了一个*net.TCPListener
每一个连接都会以协程的方式运行connectionHandler
函数。这些开始于当通过conn.RemoteAddr()
获取到客户端的地址
它使用conn.Write
发送改进的go-message给客户端
它使用一个25字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入switch
语句的default
分支关闭连接。如果是操作系统的EAGAIN
错误,它会重试。
所有的错误检查都被重构在独立的函数'checkError'中,用来分发出现的上下文错误。
在命令行中输入simple_tcp_server localhost 50000
来启动服务器程序,然后在独立的命令行窗口启动一些client.go的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址:
Copy E:\Go\GoBoek\code examples\chapter 14>simple_tcp_server localhost 50000
Listening to: 127.0.0.1:50000
Connection from: 127.0.0.1:49346
<25:Ivo says: Hi server, do y><12:ou hear me ?>
Connection from: 127.0.0.1:49347
<25:Marc says: Do you remembe><25:r our first meeting serve><2:r?>
net.Error: 这个net
包返回错误的错误类型,下边是约定的写法,不过net.Error
接口还定义了一些其他的错误实现,有些额外的方法。
Copy package net
type Error interface {
Timeout () bool // 错误是否超时
Temporary () bool // 是否是临时错误
}
通过类型断言,客户端代码可以用来测试net.Error
,从而区分哪些临时发生的错误或者必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。
Copy // in a loop - some function returns an error err
if nerr, ok := err.( net . Error ); ok && nerr. Temporary (){
time. Sleep ( 1 e 9 )
continue // try again
}
if err != nil {
log. Fatal (err)
}
链接