GO 入门指南
  • README
  • 开始
    • 前言
  • 第一部分:学习 Go 语言
    • 第1章:Go 语言的起源,发展与普及
      • 起源与发展
      • 语言的主要特性与发展的环境和影响因素
    • 第2章:安装与运行环境
      • 平台与架构
      • Go 环境变量
      • 在 Linux 上安装 Go
      • 在 Mac OS X 上安装 Go
      • 在 Windows 上安装 Go
      • 安装目录清单
      • Go 运行时(runtime)
      • Go 解释器
    • 第3章: 编辑器、集成开发环境与其它工具
      • Go 开发环境的基本要求
      • 编辑器和集成开发环境
      • 调试器
      • 构建并运行 Go 程序
      • 格式化代码
      • 生成代码文档
      • 其它工具
      • Go 性能说明
      • 与其它语言进行交互
  • 第二部分:语言的核心结构与技术
    • 第4章:基本结构和基本数据类型
      • 文件名、关键字与标识符
      • Go 程序的基本结构和要素
      • 常量
      • 变量
      • 基本类型和运算符
      • 字符串
      • strings 和 strconv 包
      • 时间和日期
      • 指针
    • 第5章:控制结构
      • if-else 结构
      • 测试多返回值函数的错误
      • switch 结构
      • for 结构
      • Break 与 continue
      • 标签与 goto
    • 第6章:函数(function)
      • 介绍
      • 函数参数与返回值
      • 传递变长参数
      • defer 和追踪
      • 内置函数
      • 递归函数
      • 将函数作为参数
      • 闭包
      • 应用闭包:将函数作为返回值
      • 使用闭包调试
      • 计算函数执行时间
      • 通过内存缓存来提升性能
    • 第7章:数组与切片
      • 声明和初始化
      • 切片
      • For-range 结构
      • 切片重组(reslice)
      • 切片的复制与追加
      • 字符串、数组和切片的应用
    • 第8章:Map
      • 声明、初始化和 make
      • 测试键值对是否存在及删除元素
      • for-range 的配套用法
      • map 类型的切片
      • map 的排序
      • 将 map 的键值对调
    • 第9章:包(package)
      • 标准库概述
      • regexp 包
      • 锁和 sync 包
      • 精密计算和 big 包
      • 自定义包和可见性
      • 为自定义包使用 godoc
      • 使用 go install 安装自定义包
      • 自定义包的目录结构、go install 和 go test
      • 通过 Git 打包和安装
      • Go 的外部包和项目
      • 在 Go 程序中使用外部库
    • 第10章:结构(struct)与方法(method)
      • 结构体定义
      • 使用工厂方法创建结构体实例
      • 使用自定义包中的结构体
      • 带标签的结构体
      • 匿名字段和内嵌结构体
      • 方法
      • 类型的 String() 方法和格式化描述符
      • 垃圾回收和 SetFinalizer
    • 第11章:接口(interface)与反射(reflection)
      • 接口是什么
      • 接口嵌套接口
      • 类型断言:如何检测和转换接口变量的类型
      • 类型判断:type-switch
      • 测试一个值是否实现了某个接口
      • 使用方法集与接口
      • 第一个例子:使用 Sorter 接口排序
      • 第二个例子:读和写
      • 空接口
      • 反射包
      • Printf 和反射
      • 接口与动态类型
      • 总结:Go 中的面向对象
      • 结构体、集合和高阶函数
  • 第三部分:Go 高级编程
    • 第12章:读写数据
      • 读取用户的输入
      • 文件读写
      • 文件拷贝
      • 从命令行读取参数
      • 用 buffer 读取文件
      • 用切片读写文件
      • 用 defer 关闭文件
      • 使用接口的实际例子:fmt.Fprintf
      • JSON 数据格式
      • XML 数据格式
      • 用 Gob 传输数据
      • Go 中的密码学
    • 第13章:错误处理与测试
      • 错误处理
      • 运行时异常和 panic
      • 从 panic 中恢复(Recover)
      • 自定义包中的错误处理和 panicking
      • 一种用闭包处理错误的模式
      • 启动外部命令和程序
      • Go 中的单元测试和基准测试
      • 测试的具体例子
      • 用(测试数据)表驱动测试
      • 性能调试:分析并优化 Go 程序
    • 第14章:协程(goroutine)与通道(channel)
      • 并发、并行和协程
      • 协程间的信道
      • 协程的同步:关闭通道-测试阻塞的通道
      • 使用 select 切换协程
      • 通道、超时和计时器(Ticker)
      • 协程和恢复(recover)
      • 新旧模型对比:任务和worker
      • 惰性生成器的实现
      • 实现 Futures 模式
      • 复用
      • 限制同时处理的请求数
      • 链式协程
      • 在多核心上并行计算
      • 并行化大量数据的计算
      • 漏桶算法
      • 对Go协程进行基准测试
      • 使用通道并发访问对象
    • 第15章:网络、模版与网页应用
      • tcp 服务器
      • 一个简单的 web 服务器
      • 访问并读取页面数据
      • 写一个简单的网页应用
      • 确保网页应用健壮
      • 用模板编写网页应用
      • 探索 template 包
      • 精巧的多功能网页服务器
      • 用 rpc 实现远程过程调用
      • 基于网络的通道 netchan
      • 与 websocket 通信
      • 用 smtp 发送邮件
  • 第四部分:实际应用
    • 第16章:常见的陷阱与错误
      • 误用短声明导致变量覆盖
      • 误用字符串
      • 发生错误时使用 defer 关闭一个文件
      • 何时使用new()和make()
      • 不需要将一个指向切片的指针传递给函数
      • 使用指针指向接口类型
      • 使用值类型时误用指针
      • 误用协程和通道
      • 闭包和协程的使用
      • 糟糕的错误处理
    • 第17章:模式
      • 逗号 ok 模式
      • defer 模式
      • 可见性模式
      • 运算符模式和接口
    • 第18章:出于性能考虑的实用代码片段
      • 字符串
      • 数组和切片
      • 映射
      • 结构体
      • 接口
      • 函数
      • 文件
      • 协程(goroutine)与通道(channel)
      • 网络和网页应用
      • 其他
      • 出于性能考虑的最佳实践和建议
    • 第19章:构建一个完整的应用程序
      • 简介
      • 短网址项目简介
      • 数据结构
      • 用户界面:web 服务端
      • 持久化存储:gob
      • 用协程优化性能
      • 以 json 格式存储
      • 多服务器处理架构
      • 使用代理缓存
      • 总结和增强
    • 第 20 章:Go 语言在 Google App Engine 的使用
      • 什么是 Google App Engine?
      • 云上的 Go
      • 安装 Go App Engine SDK:为 Go 部署的开发环境
      • 建造你自己的 Hello world 应用
      • 使用用户服务和探索其 API
      • 处理窗口
      • 使用数据存储
      • 上传到云端
    • 第 21 章:真实世界中 Go 的使用
      • Heroku:一个使用 Go 的高度可用一致数据存储
      • MROffice:一个使用 Go 的呼叫中心网络电话 (eBook/VOIP) 系统
      • Atlassian:一个虚拟机群管理系统
      • Camilistore:一个可寻址内容存储系统
      • Go 语言的其他应用
  • 附录
    • 其他
      • 关于本文16.10.2小结糟糕错误处理的一些见解
    • A 代码引用
    • B 有趣的 Go 引用
    • C 代码示例列表
      • 目录
      • 第2章示例
      • 第3章示例
      • 第4章示例
      • 第5章示例
      • 第6章示例
      • 第7章示例
      • 第8章示例
      • 第9章示例
      • 第10章示例
      • 第11章示例
      • 第12章示例
      • 第13章示例
      • 第14章示例
      • 第15章示例
      • 第16章示例
      • 第19章示例
      • 第20章示例
    • D 书中的包引用
    • E 书中的工具引用
    • F 常见问题解答
    • G 习题答案
      • 第4章答案
      • 第5章答案
      • 第6章答案
      • 第7章答案
      • 第8章答案
      • 第9章答案
      • 第10章答案
      • 第11章答案
      • 第12章答案
      • 第13章答案
      • 第14章答案
      • 第15章答案
      • 第16章答案
      • 第19章答案
      • 第20章答案
    • H 参考文献
Powered by GitBook
On this page

Was this helpful?

  1. 第三部分:Go 高级编程
  2. 第15章:网络、模版与网页应用

tcp 服务器

Previous第15章:网络、模版与网页应用Next一个简单的 web 服务器

Last updated 2 years ago

Was this helpful?

这部分我们将使用 TCP 协议和在 14 章讲到的协程范式编写一个简单的客户端-服务器应用,一个 (web) 服务器应用需要响应众多客户端的并发请求:Go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了处理 TCP/IP 以及 UDP 协议、域名解析等方法。

服务器端代码是一个单独的文件:

示例 15.1

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 中:

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”。注意裁剪 和 字符(仅 Windows 平台需要)。裁剪后的输入被 connection 的 Write() 方法发送到服务器。

当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。

如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:

dial tcp [::1]:xxxx: connectex: No connection could be made because the target machine actively refused it.

打开命令提示符并转到服务器和客户端代码所在的目录,输入 go run server.go,接下来控制台出现以下信息:Starting the server ...

在 Windows 系统中,我们可以通过 CTRL+C 停止程序。

然后开启 2 个或者 3 个独立的控制台窗口,分别启动客户端程序

以下是服务器的输出:

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 地址:

// 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 中打开,写入,读取数据的例子:

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

  • 使客户端可以通过发送一条命令 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 行代码!

// 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
	}
}

都有哪些改进?

  • 服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过 flag 包来读取这些参数。这里使用了 flag.NArg() 检查是否按照期望传入了 2 个参数:

if flag.NArg() != 2 {
	panic("usage: host port")
}

传入的参数通过 fmt.Sprintf() 函数格式化成字符串

hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
  • 在 initServer() 函数中通过 net.ResolveTCPAddr() 得到了服务器地址和端口,这个函数最终返回了一个 *net.TCPListener

  • 每一个连接都会以协程的方式运行 connectionHandler() 函数。函数首先通过 conn.RemoteAddr() 获取到客户端的地址并显示出来

  • 它使用 conn.Write() 发送 Go 推广消息给客户端

  • 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 switch 语句 default 分支,退出无限循环并关闭连接。如果是操作系统的 EAGAIN 错误,它会重试。

  • 所有的错误检查都被重构在独立的函数 checkError 中,当错误产生时,利用错误上下文来触发 panic。

在命令行中输入 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 包返回的错误类型遵循惯例为 error,但有些错误实现包含额外的方法,他们被定义为 net.Error 接口:

package net

type Error interface {
	Timeout() bool // 错误是否超时
	Temporary() bool // 是否是临时错误
}

通过类型断言,客户端代码可以测试 net.Error,从而区分是临时发生的还是必然会出现的错误。举例来说,一个网络爬虫程序在遇到临时发生的错误时可能会休眠或者重试,如果是一个必然发生的错误,则他会放弃继续执行。

// 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)
}

链接

示例 15.2

示例 15.3

示例 15.4

编写新版本的客户端和服务器 ( / ):

增加一个检查错误的函数 checkError(error);讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 中它是如何被解决的

示例 15.5 :

(译者注:应该是由于 Go 版本的更新,会提示 os.EAGAIN undefined,修改后的代码:)

上一节:

下一节:

server.go
client.go
dial.go
socket.go
client1.go
server1.go
示例 15.14
simple_tcp_server.go
simple_tcp_server_v1.go
目录
网络、模版与网页应用
一个简单的网页服务器