glight2000 пре 10 година
родитељ
комит
3d5fade00d
1 измењених фајлова са 333 додато и 0 уклоњено
  1. 333 0
      eBook/15.1.md

+ 333 - 0
eBook/15.1.md

@@ -0,0 +1,333 @@
+# 15.1 基本类型和运算符
+
+这部分我们将使用TCP协议和在14章讲到的携程范式编写一个简单的客户端-服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go会为每一个客户端产生一个携程用来处理请求。我们需要使用net包中网络通信的功能。它包含了用于TCP/IP以及UDP协议、域名解析等方法。
+
+服务器代码,单独的一个文件:
+
+示例 15.1 [server.go](examples/chapter_15/server.go)
+```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)
+		_, err := conn.Read(buf)
+		if err != nil {
+			fmt.Println("Error reading", err.Error())
+			return //终止程序
+		}
+		fmt.Printf("Received data: %v", string(buf))
+	}
+}
+
+```
+
+我们在`main()`创建了一个`net.Listener`的变量,他是一个服务器的基本函数:用来监听和接收来自客户端的请求(来自localhost即IP地址为127.0.0.1端口为50000基于TCP协议)。这个`Listen()`函数可以返回一个`error`类型的错误变量。用一个无限for循环的`listener.Accept()`来等待客户端的请求。客户端的请求将产生一个`net.Conn`类型的连接变量。然后一个独立的携程使用这个连接执行`doServerStuff()`,开始使用一个512字节的缓冲`data`来读取客户端发送来的数据并且把它们打印到服务器的终端;当客户端发送的所有数据都被读取完成时,携程就结束了。这段程序会为每一个客户端连接创建一个独立的携程。必须先运行服务器代码,再运行客户端代码。
+
+客户端代码写在另外一个文件client.go中:
+
+示例 15.2 [client.go](examples/chapter_15/client.go)
+```go
+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回车启动客户端程序
+
+以下是服务器的输出(在移除掉512字节的字符串中内容为空的区域后):
+```
+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 并结束程序时,服务器会输出以下信息:
+```
+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](examples/chapter_15/dial.go)
+```go
+// 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!")
+		os.Exit(1)
+	}
+	fmt.Println("Connection is made with %v", conn)
+}
+```
+下边也是一个使用net包从socket中打开,写入,读取数据的例子:
+
+示例 15.4 [socket.go](examples/chapter_15/socket.go)
+```go
+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.to / server1.go):
+*	增加一个检查错误的函数`checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在示例15.14中它是如何被解决的
+*	使客户端可以通过发送一条命令SH来关闭服务器
+*	让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送WHO指令的时候,服务器将显示如下列表:
+
+```
+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](examples/chapter_15/simple_tcp_server.go):
+```go
+// 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.ListenTCP("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](examples/chapter_15/simple_tcp_server_v1.go)**)
+
+都有哪些改进?
+(1)服务器地址和端口不再是硬编码,而是通过命令行传入参数并通过`flag`包来读取这些参数。这里使用了`flag.NArg()`检查是否按照期望传入了2个参数:
+```go
+	if flag.NArg() != 2{
+		panic("usage: host port")
+	}
+```
+传入的参数通过`fmt.Sprintf`函数格式化成字符串
+```go
+	hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
+```
+(2)在`initServer`函数中通过`net.ResolveTCPAddr`指定了服务器地址和端口,这个函数最终返回了一个`*net.TCPListener`
+(3)每一个连接都会以携程的方式运行`connectionHandler`函数。这些开始于当通过`conn.RemoteAddr()`获取到客户端的地址
+(4)它使用`conn.Write`发送改进的go-message给客户端
+(5)它使用一个25字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入`switch`语句的`default`分支关闭连接。如果是操作系统的`EAGAIN`错误,它会重试。
+(6)所有的错误检查都被重构在独立的函数'checkError'中,用来分发出现的上下文错误。
+
+在命令行中输入`simple_tcp_server localhost 50000`来启动服务器程序,然后在独立的命令行窗口启动一些client.go的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址:
+```
+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`接口还定义了一些其他的错误实现,有些额外的方法。
+```go
+package net
+
+type Error interface{
+	Timeout() bool // 错误是否超时
+	Temporary() bool // 是否是临时错误
+```
+通过类型断言,客户端代码可以用来测试`net.Error`,从而区分哪些临时发生的错误或者必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。
+```go
+// in a loop - some function returns an error err
+if nerr, ok := err.(net.Error); ok && nerr.Temporary(){
+	time.Sleep(1e9)
+	continue // try again
+}
+if err != nil{
+	log.Fatal(err)
+```
+
+## 链接
+
+- [目录](directory.md)
+- 上一节:[网络、模版与网页应用](15.0.md)
+- 下一节:[一个简单的web服务器](15.2.md)