Go 语言最适合编写 web 应用, 因为它没有提供 GUI 框架, 所以只能将包含字符串或模板的 HTML, 作为应用的输出窗口.
15.1 tcp 服务器
在本节中, 将使用 TCP 协议和并发协程, 开发一个简单的客户端-服务器应用, 一个 (web) 服务器应用需响应多个客户端的并发请求, 在 Go 语言中, 每个客户端请求都将生成一个并发协程, 并对请求进行处理, 同时还需要 net 包, 其中实现了网络的通讯功能, 并包含了支持 TCP/IP 和 UDP 协议, 域名解析等功能的方法, 以下示例给出了所需的服务器代码,
例 15.1 server.go
在 main 函数中, 创建了一个 net.Listener 变量 listener, 它可实现服务器的基本功能, 监听来自于客户端的连接请求, 并接受该请求 (在本机状态下, 基于 ip 地址 127.0.0.1 及端口 50000, 可通过 TCP 协议模拟一个客户端连接请求), Listen() 函数可返回一个 err 变量 (error 类型), 之后将在一个死循环中, 使用 listener.Accept()等待客户端的连接请求, 并能接受该请求, 使用 net.Conn 类型变量 conn 可创建一个客户端连接请求, 建立客户端和服务器的连接后, 将启动一个并发协程 doServerStuff(), 其中可将读入的网络数据, 放入一个 512 字节的缓冲中, 并会在服务器终端上, 显示这些网络数据, 当来自于客户端的所有数据读取完毕, 该并发协程也将终止运行, 同时会为每个客户端, 生成一个独立的并发协程, 而服务器代码的运行必须早于客户端代码, 以下是客户端代码,
例 15.2 client.go
通过 net.Dial 可产生发送给服务器的客户端连接请求, 在上述示例中, 可在死循环中, 接收键盘命令 (os.Stdin),按下 Q 键, 可使客户端程序终止, 之后传输数据将使用网络连接的写入方法, 发送给服务器.
首先服务器程序必须首先启动, 如果服务器无法监听, 也就不能建立与客户端的网络连接, 如果客户端工作时,服务器还未处于监听状态, 客户端将终止运行, 并给出一个错误消息,
在控制台界面下, 首先可启动服务器程序 (Windows 为 server.exe,Linux 为./server), 这时将出现一条消息, Starting the server …, 之后可开启多个控制台界面, 可分别启动多个客户端, 并发送一些消息, 这时在服务器控制台中, 将出现以下的传输消息:
当一个客户端终止运行, 服务器控制台可输出以下消息:
net.Dial 是网络通讯中比较重要的一个函数, 当调用该函数, 与远端系统建立连接后, 可返回一个 conn 接口类型, 利用 conn 可实现数据的发送和接收, 同时该函数实现了必要的抽象, 因此 IPv4, IPv6, TCP, UDP 都可共享一个通用接口, 也就是 net.Dial 函数都支持上述协议, 以下示例将在 80 端口上, 分别使用 TCP, UDP, IPv6-TCP 协议, 向远端系统发送连接请求.
例 15.3 dial.go
以下示例将给出 net 包的一些功能, 即打开 soket, 基于 socket 实现的写入和读取.
例 15.4 socket.go
以下示例对 server.go 进行了一些的改进,
例 15.5 simple_tcp_server.go
上例给出的改进如下:
• 服务器的地址和端口不能直接写入程序中, 应基于 flag 包, 从命令行中读取, 如果未读取到服务器地址和端口,flag.NArg() 将产生一个告警消息. 同时这两个参数将在 fmt.Sprintf 函数中, 被放入一个格式化字符串中.
• 传入 initServer 函数的服务器地址和端口, 将被 net.ResolveTCPAddr 方法所解析, 同时 initServer 函数还可返回一个 *net.TCPListener
• 在每一个连接出现时, 都将启动一个并发协程, 并调用 connectionHandler 函数, conn.RemoteAddr() 将提供客户端的地址
• 通过 conn.Write 函数, 将发送一个消息给客户端
• 可从客户端中读取 25 个字节数据, 并打印出这些数据, 如果在死循环中, 读取出现错误, 则通过 default
子句, 退出死循环, 并关闭客户端的连接, 如果 OS 发出了一个 EAGAIN(重新操作) 错误, 则会重新读取
• 所有的错误检查将集中在一个 checkError 函数中, 当出现错误时, 该函数发布一个故障, 并给出与运行
环境有关的错误消息
在控制台中, 可使用以下命令, 启动一个服务器程序:
同时还可在其他的控制台中, 启动多个客户端, 在以下示例中, 服务器连接了两个客户端, 每个客户端都有自己的地址:
net 包可返回错误 (error 类型), 并能提供常见的错误定义, 而通过 net.Error 接口, 可自定义其他的错误类型(这些类型可包含自己的方法).
使用一个类型检查, 可测试 net.Error 包含的客户端数据, 之后可分析网络错误的一般原因, 比如 web 爬虫进入睡眠状态, 或是在遇到一个临时错误, 它可进行重新尝试, 又或者选择放弃.
15.2 简单的 web 服务器
http 是 tcp 的上层协议, 它描述了 web 服务器与浏览器客户端的通讯, 在 Go 语言中, 包含了 net/http 包, 以下将使用一些示例, 来讨论 http 包的用法, 首先是一个可发送 Hello world! 消息的 web 服务器.
首先需导入 http 包, 再使用 net.Listen(”tcp”,”localhost:50000”) 函数, 实现 web 服务器的监听功能, 之后再使用 http.ListenAndServe(”localhost:8080”, nil) 函数, 实现 http 端口 (8080) 的监听, 如果执行成功, 可返回nil, 如果省略服务器地址 (即 localhost), 将返回一个错误.
使用 http.URL 类型, 可描述一个 web 地址, 该类型的 Path 数据域, 可包含一个 url 字符串, 而使用 http.Request类型可描述一个客户端请求, 在该类型中, 也包含了一个 URL 数据域.
如果请求 req 是一个 html 格式的 POST 命令, 在 html 格式中, 将包含一个名为 var1 的 html 输入数据域, 因此在 Go 代码中, 可使用该名称, 即 req.FormValue(”var1”), 参见 15.4 节, 如果使用另一种编程方式, 可先调用 request.ParseForm(), 再使用 request.Form[”var1”] 可获取 var1 值, 如下:
如果获取到 var1 值,found 将返回 true, 否则 found 将返回 false.
Form 数据域是一个 map[string][]string 类型,web 服务器可发送一个 http.Response, 同时它将被放入http.ResponseWriter 对象, 该对象可视为 http 服务器的响应器, 对其进行写入, 可发送数据给 http 客户端.
web 服务器必须给出客户端请求的处理, 这类处理将放置在 http.HandleFunc 函数类型中, 在以下示例中, 如果请求 web 的根页面 (http://localhost:8080) 或是其他页面时, 将调用 HelloServer 函数, 该函数的类型为http.HandlerFunc, 在多数情况下, 这类函数将被命名为 Prefhandler(其中加入了前缀 Pref). http.HandleFunc可注册一个处理器函数 (本例为 HelloServer), 它用于处理 web 根页面的请求.
除了 web 根页面之外, 还可请求其他页面, 比如/create,/edit 等, 对于不同的 URL, 可定义不同的处理器函数,而处理器函数中可包含两个形参,ResponseWriter 和请求 req, 而 ResponseWriter 可写入一个字符串 (其中将包含 Hello 和 r.URL),req 中给出的 Path[1:], 可获得一个从第一个字符元素开始, 到数组末尾的路径 slice, 这等同于删除路径名之前的 /字符, 获得的路径