diff --git a/eBook/15.0.md b/eBook/15.0.md index 02377dc..ab3eff9 100644 --- a/eBook/15.0.md +++ b/eBook/15.0.md @@ -1,9 +1,9 @@ # 15.0 网络,模板和网页应用 -go在编写web应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的html界面是目前go编写应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的GUI库例如:go ui。**) +Go 在编写 web 应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的 html 页面是目前 Go 编写界面应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的GUI库例如:go ui。**) ## 链接 - [目录](directory.md) -- 上一节:[使用通道(channel)并发修改对象数据](14.17.md) -- 下一节:[一个Tcp服务器](15.1.md) +- 上一节:[使用通道并发访问对象](14.17.md) +- 下一节:[tcp服务器](15.1.md) diff --git a/eBook/15.1.md b/eBook/15.1.md index f5938c9..0b16427 100644 --- a/eBook/15.1.md +++ b/eBook/15.1.md @@ -1,8 +1,8 @@ # 15.1 tcp服务器 -这部分我们将使用TCP协议和在14章讲到的协程范式编写一个简单的客户端-服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go会为每一个客户端产生一个协程用来处理请求。我们需要使用net包中网络通信的功能。它包含了用于TCP/IP以及UDP协议、域名解析等方法。 +这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。 -服务器代码,单独的一个文件: +服务器端代码是一个单独的文件: 示例 15.1 [server.go](examples/chapter_15/server.go) @@ -44,12 +44,11 @@ func doServerStuff(conn net.Conn) { 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`获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。 +在 `main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于TCP协议)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 for 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。 -客户端代码写在另外一个文件client.go中: +客户端代码写在另一个文件 client.go 中: 示例 15.2 [client.go](examples/chapter_15/client.go) @@ -92,21 +91,21 @@ func main() { } } ``` -客户端通过`net.Dial`创建了一个和服务器之间的连接 +客户端通过 `net.Dial` 创建了一个和服务器之间的连接。 -它通过无限循环中的os.Stdin接收来自键盘的输入直到输入了“Q”。注意使用`\r`和`\n`换行符分割字符串(在windows平台下使用`\r\n`)。接下来分割后的输入通过`connection`的`Write`方法被发送到服务器。 +它通过无限循环从 `os.Stdin` 接收来自键盘的输入,直到输入了“Q”。注意裁剪 `\r` 和 `\n` 字符(仅 Windows 平台需要)。裁剪后的输入被 `connection` 的 `Write` 方法发送到服务器。 当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。 如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:`对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接`。 -打开控制台并转到服务器和客户端可执行程序所在的目录,Windows系统下输入server.exe(或者只输入server),Linux系统下输入./server。 +打开命令提示符并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入server.exe(或者只输入server),Linux系统下输入./server。 接下来控制台出现以下信息:`Starting the server ...` -在Windows系统中,我们可以通过CTRL/C停止程序。 +在 Windows 系统中,我们可以通过 CTRL/C 停止程序。 -然后开启2个或者3个独立的控制台窗口,然后分别输入client回车启动客户端程序 +然后开启 2 个或者 3 个独立的控制台窗口,分别输入 client 回车启动客户端程序 以下是服务器的输出: @@ -121,9 +120,9 @@ Received data: MARC says: Don't forget our appointment tomorrow ! ``` 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都可以使用这个公用接口。 +在网络编程中 `net.Dial` 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 `Conn` 类型的接口,我们可以用它发送和接收数据。`Dial` 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6,TCP 或者 UDP 都可以使用这个公用接口。 -下边这个示例先使用TCP协议连接远程80端口,然后使用UDP协议连接,最后使用TCP协议连接IPv6类型的地址: +以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址: 示例 15.3 [dial.go](examples/chapter_15/dial.go) @@ -153,7 +152,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) @@ -189,23 +188,23 @@ func main() { con.Close() } ``` + **练习 15.1** 编写新版本的客户端和服务器([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指令的时候,服务器将显示如下列表: - +* 增加一个检查错误的函数 `checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 [示例15.14](examples/chapter_15/template_validation.go) 中它是如何被解决的 +* 使客户端可以通过发送一条命令 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`正在被操作系统使用而无法被替换成新的版本。 +注意:当服务器运行的时候,你无法编译/连接同一个目录下的源码来产生一个新的版本,因为 `server.exe` 正在被操作系统使用而无法被替换成新的版本。 -下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个tcp服务器的示例 server.go 并且拥有更好的结构,它只用了80行代码! +下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个tcp服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码! 示例 15.5 [simple_tcp_server.go](examples/chapter_15/simple_tcp_server.go): @@ -293,28 +292,28 @@ 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)**) 都有哪些改进? -* 服务器地址和端口不再是硬编码,而是通过命令行传入参数并通过`flag`包来读取这些参数。这里使用了`flag.NArg()`检查是否按照期望传入了2个参数: +* 服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过 `flag` 包来读取这些参数。这里使用了 `flag.NArg()` 检查是否按照期望传入了2个参数: ```go 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-message给客户端 -* 它使用一个25字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入`switch`语句的`default`分支关闭连接。如果是操作系统的`EAGAIN`错误,它会重试。 -* 所有的错误检查都被重构在独立的函数'checkError'中,用来分发出现的上下文错误。 +* 在 `initServer` 函数中通过 `net.ResolveTCPAddr` 得到了服务器地址和端口,这个函数最终返回了一个 `*net.TCPListener` +* 每一个连接都会以协程的方式运行 `connectionHandler` 函数。函数首先通过 `conn.RemoteAddr()` 获取到客户端的地址并显示出来 +* 它使用 `conn.Write` 发送 Go 推广消息给客户端 +* 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 `switch` 语句 `default` 分支,退出无限循环并关闭连接。如果是操作系统的 `EAGAIN` 错误,它会重试。 +* 所有的错误检查都被重构在独立的函数 `checkError` 中,当错误产生时,利用错误上下文来触发 panic。 -在命令行中输入`simple_tcp_server localhost 50000`来启动服务器程序,然后在独立的命令行窗口启动一些client.go的客户端。当有两个客户端连接的情况下服务器的典型输出如下,这里我们可以看到每个客户端都有自己的地址: +在命令行中输入 `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 @@ -323,17 +322,18 @@ Connection from: 127.0.0.1:49346 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`接口还定义了一些其他的错误实现,有些额外的方法。 +`net` 包返回的错误类型遵循惯例为 `error`,但有些错误实现包含额外的方法,他们被定义为 `net.Error` 接口: ```go package net -type Error interface{ +type Error interface { Timeout() bool // 错误是否超时 Temporary() bool // 是否是临时错误 } ``` -通过类型断言,客户端代码可以用来测试`net.Error`,从而区分哪些临时发生的错误或者必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。 +通过类型断言,客户端代码可以测试 `net.Error`,从而区分是临时发生的还是必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。 ```go // in a loop - some function returns an error err if nerr, ok := err.(net.Error); ok && nerr.Temporary(){ diff --git a/eBook/15.2.md b/eBook/15.2.md index dbbe227..fac356a 100644 --- a/eBook/15.2.md +++ b/eBook/15.2.md @@ -1,28 +1,26 @@ # 15.2 一个简单的网页服务器 -Http是一个比tcp更高级的协议,它描述了客户端浏览器如何与网页服务器进行通信。Go有自己的`net/http`包,我们来看看它。我们从一些简单的示例开始, -首先编写一个“Hello world!”:[查看示例15.6](examples/chapter_15/hello_world_webserver.go) +http 是比 tcp 更高层的协议,它描述了网页服务器如何与客户端浏览器进行通信。Go有自己的 `net/http` 包,我们来看看它。我们从一些简单的示例开始,首先编写一个“Hello world!”网页服务器:[查看示例15.6](examples/chapter_15/hello_world_webserver.go) -我们引入了`http`包并启动了网页服务器,和15.1的`net.Listen("tcp", "localhost:50000")`函数的tcp服务器是类似的,使用`http.ListenAndServe("localhost:8080", nil)`函数,如果成功会返回空,否则会返回一个错误(可以指定localhost为其他地址,8080是指定的端口号) +我们引入了 `http` 包并启动了网页服务器,和 [15.1节](15.1.md) 的 `net.Listen("tcp", "localhost:50000")` 函数的 tcp 服务器是类似的,使用 `http.ListenAndServe("localhost:8080", nil)` 函数,如果成功会返回空,否则会返回一个错误(地址 localhost 部分可以省略,8080 是指定的端口号)。 -`http.URL`描述了web服务器的地址,内含存放了url字符串的`Path`属性;`http.Request`描述了客户端请求,内含一个`URL`属性 - -如果`req`请求是一个POST类型的html表单,“var1”就是html表单中一个输入属性的名称,然后用户输入的值就可以通过GO代码:`req.FormValue("var1")`获取到(请看[章节15.4](15.4.md))。还有一种方法就是先执行`request.ParseForm()`然后再获取`request.Form["var1"]`的第一个返回参数,就像这样: +`http.URL` 用于表示网页地址,其中包含字符串属性 `Path` 保存 url 路径;`http.Request` 描述了客户端请求,内含一个 `URL` 字段。 +如果 `req` 是来自 html 表单的 POST 类型请求,“var1” 是该表单中一个输入域的名称,那么用户输入的值就可以通过 Go 代码 `req.FormValue("var1")` 获取到(请看 [15.4节](15.4.md))。还有一种方法是先执行 `request.ParseForm()`,然后再获取 `request.Form["var1"]` 的第一个返回参数,就像这样: ```go - var1, found := request.Form["var1"] +var1, found := request.Form["var1"] ``` -第二个参数`found`就是`true`,如果`var1`并未出现在表单中,`found`就是`false` +第二个参数 `found` 为 `true`。如果 `var1` 并未出现在表单中,`found` 就是 `false`。 -表单属性实际上是一个`map[string][]string`类型。网页服务器返回了一个`http.Response`,它是通过`http.ResponseWriter`对象输出的,这个对象整合了HTTP服务器的返回结果;通过对它写入内容,我们就将数据发送给了HTTP客户端。 +表单属性实际上是一个 `map[string][]string` 类型。网页服务器返回了一个 `http.Response`,它是通过 `http.ResponseWriter` 对象输出的,它会组装 HTTP 服务器响应;通过对它写入内容,我们就将数据发送给了 HTTP 客户端。 -现在我们还需要编写网页服务器必须执行的程序,它是如何处理请求的呢。这是在`http.HandleFunc`函数中完成的,就是在这个例子中当根路径“/”(url地址是http://localhost:8080 )被请求的时候(或者这个服务器上的其他地址),`HelloServer`函数就被执行了。这个函数是`http.HandlerFunc`类型的,它们通常用使用Prefhandler来命名,在前边加了一个Pref前缀。 +现在我们仍然要编写程序,以实现服务器必须做的事,即如何处理请求。这是通过 `http.HandleFunc` 函数完成的。在这个例子中,当根路径“/”(url地址是 `http://localhost:8080`)被请求的时候(或者这个服务器上的其他任意地址),`HelloServer` 函数就被执行了。这个函数是 `http.HandlerFunc` 类型的,它们通常被命名为 Prefhandler,和某个路径前缀 Pref 匹配。 -`http.HandleFunc`注册了一个处理函数(这里是`HelloServer`)来处理对应`/`的请求。 +`http.HandleFunc` 注册了一个处理函数(这里是 `HelloServer`)来处理对应 `/` 的请求。 -`/`可以被替换为其他特定的url比如`/create`,`/edit`等等;你可以为每一个特定的url定义一个单独的处理函数。这个函数需要两个参数:第一个是`ReponseWriter`类型的`w`;第二个是请求`req`。程序向`w`写入了`Hello`和`r.URL.Path[1:]`组成的字符串后边的`[1:]`表示“创建一个从第一个字符到结尾的子切片”,用来丢弃掉路径开头的“/”,`fmt.Fprintf()`函数完成了本次写入(请看[章节12.8](12.8.md));另外一种写法是`io.WriteString(w, "hello, world!\n")` +`/` 可以被替换为其他更特定的 url,比如 `/create`,`/edit` 等等;你可以为每一个特定的 url 定义一个单独的处理函数。这个函数需要两个参数:第一个是 `ReponseWriter` 类型的 `w`;第二个是请求 `req`。程序向 `w` 写入了 `Hello` 和 `r.URL.Path[1:]` 组成的字符串:末尾的 `[1:]` 表示“创建一个从索引为 1 的字符到结尾的子切片”,用来丢弃路径开头的“/”,`fmt.Fprintf()` 函数完成了本次写入(请看 [12.8节](12.8.md));另一种可行的写法是 `io.WriteString(w, "hello, world!\n")`。 -总结:第一个参数是请求的路径,第二个参数是处理这个路径请求的函数的引用。 +总结:第一个参数是请求的路径,第二个参数是当路径被请求时,需要调用的处理函数的引用。 示例 15.6 [hello_world_webserver.go](examples/chapter_15/hello_world_webserver.go): @@ -53,9 +51,10 @@ func main() { Starting Process E:/Go/GoBoek/code_examples/chapter_14/hello_world_webserver.exe ... ``` -然后打开你的浏览器并输入url地址:`http://localhost:8080/world`,浏览器就会出现文字:`Hello, world`,网页服务器会响应你在`:8080/`后边输入的内容 -使用`fmt.Println`在控制台打印状态,在每个handler被请求的时候,在他们内部打印日志会很有帮助 +然后打开浏览器并输入 url 地址:`http://localhost:8080/world`,浏览器就会出现文字:`Hello, world`,网页服务器会响应你在 `:8080/` 后边输入的内容。 + +`fmt.Println` 在服务器端控制台打印状态;在每个处理函数被调用时,把请求记录下来也许更为有用。 注: 1)前两行(没有错误处理代码)可以替换成以下写法: @@ -63,18 +62,18 @@ Starting Process E:/Go/GoBoek/code_examples/chapter_14/hello_world_webserver.exe http.ListenAndServe(":8080", http.HandlerFunc(HelloServer)) ``` -2)`fmt.Fprint`和`fmt.Fprintf`都是用来写入`http.ResponseWriter`的不错的函数(他们实现了`io.Writer`)。 +2)`fmt.Fprint` 和 `fmt.Fprintf` 都是可以用来写入 `http.ResponseWriter` 的函数(他们实现了 `io.Writer`)。 比如我们可以使用 ```go fmt.Fprintf(w, "