Go 网络编程(三)
六、管理字符集和编码
从前有 EBCDIC 和 ASCII。事实上,它从来没有那么简单,只是随着时间的推移变得更加复杂。地平线上有光,但一些人估计,我们可能需要 50 年才能在这里生活在日光下!
早期的计算机是在说英语的美国、英国和澳大利亚发展起来的。因此,人们对正在使用的语言和字符集做出了假设。基本上,使用拉丁字母,加上数字,标点符号,和一些其他的。然后使用 ASCII 或 EBCDIC 将这些编码成字节。
字符处理机制基于此:文本文件和 I/O 由一系列字节组成,每个字节代表一个字符。字符串比较可以通过匹配相应的字节来完成;从大写到小写的转换可以通过映射单个字节来完成,等等。
世界上大约有 6500 种口语(其中 850 种在巴布亚新几内亚!).少数语言使用“英语”字符,但大多数不使用。像法语这样的罗马语言在不同的字符上有装饰,所以你可以用两个不同重音的元音写“j'ai arrêté”。同样,日耳曼语也有额外的字符,如“σ”。甚至英国英语也有不在标准 ASCII 码集中的字符:英镑符号“”和最近的欧元“€”。
但是这个世界并不局限于拉丁字母的变体。泰国有自己的字母表,单词看起来像这样:“ภาษาไทย".还有很多其他的字母,日本甚至有两个,平假名和片假名。
也有象形文字语言,比如中文,你可以在上面写字百度一下,你就知道".
从技术角度来看,如果全世界都使用 ASCII 就好了。然而,趋势是相反的,越来越多的用户要求软件使用他们熟悉的语言。如果你开发一个可以在不同国家运行的应用程序,那么用户会要求它使用他们自己的语言。在分布式系统中,期望不同语言和字符的用户可以使用系统的不同组件。
国际化(i18n)是您如何编写应用程序,以便它们可以处理各种语言和文化。本地化(l10n)是针对特定文化群体定制您的国际化应用程序的过程。
i18n 和 l10n 本身就是大话题。例如,它们涵盖了颜色等问题:虽然白色在西方文化中意味着“纯洁”,但它对中国人来说意味着“死亡”,对埃及人来说意味着“快乐”。在这一章中,我们只看字符处理的问题。
定义
很重要的一点是要注意你所说的是文本处理系统的哪一部分。这里有一组被证明有用的定义。
性格;角色;字母
字符是“大致对应于自然语言的字形(书面符号)的信息单元,如字母、数字或标点符号”(维基百科)。字符是“书面语言中具有语义价值的最小组成部分”(Unicode)。这包括字母,如“a”和“à”(或任何其他语言的字母),数字,如“2”,标点符号,如“,”和各种符号,如英国英镑货币符号“”。
一个字符是任何实际符号的某种抽象:字符“a”对于任何书写的“a”就像柏拉图的圆圈对于任何实际的圆圈一样。字符的概念还包括控制字符,它不对应于自然语言符号,而是对应于用于处理语言文本的其他信息。
一个角色没有任何特定的外貌,尽管我们用外貌来帮助识别角色。然而,即使是外观也可能必须在一个上下文中理解:在数学中,如果你看到符号π (pi ),它是圆周与半径之比的字符,而如果你正在阅读希腊文本,它是字母表的第 16 个字母:“ρρoσ”是希腊单词“with ”,与 3.14159 无关。
字符集/字符集
字符集是一组不同的字符,如拉丁字母。不假定特定的顺序。在英语中,虽然我们说“a”在字母表中比“z”早,但我们不会说“a”比“z”少。“电话簿”的排序将“麦克菲”放在“麦克雷”之前,这表明“字母排序”对角色来说并不重要。
剧目指定了角色的名字,通常还有角色的样片。例如,字母“a”可能看起来像“a”、“a”或“a”。但这并不强迫它们看起来像那样——它们只是样品。剧目可能会作出区分,如大写和小写,以便“A”和“A”是不同的。但它可能认为它们是相同的,只是样品外观不同。(就像一些编程语言将大写和小写视为不同一样——Go——但一些不这样——Basic。).另一方面,一个汇编可能包含具有相同样本外观的不同字符:一个希腊数学家的汇编可能有两个具有π外观的不同字符。这也称为非编码字符集。
字符二进制码
字符代码是从字符到整数的映射。字符集的映射也称为编码字符集或代码集。这种映射中每个字符的值通常称为代码点。ASCII 是一种代码集。“A”的码位是 97,而“A”的码位是 65(十进制)。
字符代码仍然是一个抽象概念。它还不是我们将在文本文件或 TCP 包中看到的。然而,它越来越接近了,因为它提供了从面向人的概念到数字概念的映射。
字符编码
为了交流或存储一个字符,你需要以某种方式对它进行编码。要传输一个字符串,需要对字符串中的所有字符进行编码。任何代码集都有许多可能的编码。
例如,7 位 ASCII 码位可以编码为 8 位字节(一个八位字节)。因此,ASCII“A”(代码点为 65)被编码为 8 位二进制八位数 01000001。然而,另一种不同的编码方式是将高位用于奇偶校验。例如,对于奇数奇偶校验,ASCII“A”将是八位字节 11000001。一些协议如 Sun 的 XDR 使用 32 位字长编码。ASCII“A”将被编码为 0000000000000000000000000001000001。
字符编码是我们在编程层面的功能。我们的程序处理编码字符。显然,我们是处理带或不带奇偶校验的 8 位字符,还是处理 32 位字符是有区别的。
编码扩展到字符串。“ABC”的字长偶校验编码可能是 10000000(高位字节中的奇偶校验位)0100000011(C)01000010(B)01000001(低位字节中的 A)。关于编码重要性的评论同样适用于字符串,只是规则可能有所不同。
传输编码
字符编码足以在单个应用程序中处理字符。然而,一旦你开始在应用程序之间发送文本,那么就有一个更进一步的问题,如何将字节、缩写或单词放到网络上。编码可以基于节省空间和带宽的技术,如压缩文本。或者可以简化为 7 位格式,以允许奇偶校验位,例如 base64。
如果我们知道字符和传输编码,那么管理字符和字符串就是编程的问题了。如果我们不知道字符或传输编码,那么如何处理任何特定的字符串就只能靠猜测了。文件没有约定来表示字符编码。
然而,在通过互联网传输的文本中有一个信令编码的惯例。很简单:文本消息的报头包含编码信息。例如,HTTP 头可以包含如下行:
Content-Type: text/html; charset=ISO-8859-4
Content-Encoding: gzip
这表明字符集是 ISO 8859-4(对应于欧洲的某些国家)的默认编码,但是后来被压缩了。第二部分—内容编码—我们称之为“传输编码”(IETF RFC 2130)。
但是你如何阅读这些信息呢?不是被编码了吗?我们不是有一个先有鸡还是先有蛋的局面吗?不,约定是这样的信息以 ASCII(准确地说是 US ASCII)给出,这样程序可以读取文件头,然后为文档的其余部分调整它的编码。
美国信息交换标准代码
ASCII 包含英文字符、数字、标点符号和一些控制字符。这个熟悉的表格给出了 ASCII 的代码点:
Oct Dec Hex Char Oct Dec Hex Char
------------------------------------------------------------
000 0 00 NUL '¥0' 100 64 40 @
001 1 01 SOH 101 65 41 A
002 2 02 STX 102 66 42 B
003 3 03 ETX 103 67 43 C
004 4 04 EOT 104 68 44 D
005 5 05 ENQ 105 69 45 E
006 6 06 ACK 106 70 46 F
007 7 07 BEL '\a' 107 71 47 G
010 8 08 BS '\b' 110 72 48 H
011 9 09 HT '\t' 111 73 49 I
012 10 0A LF '\n' 112 74 4A J
013 11 0B VT '\v' 113 75 4B K
014 12 0C FF '\f' 114 76 4C L
015 13 0D CR '\r' 115 77 4D M
016 14 0E SO 116 78 4E N
017 15 0F SI 117 79 4F O
020 16 10 DLE 120 80 50 P
021 17 11 DC1 121 81 51 Q
022 18 12 DC2 122 82 52 R
023 19 13 DC3 123 83 53 S
024 20 14 DC4 124 84 54 T
025 21 15 NAK 125 85 55 U
026 22 16 SYN 126 86 56 V
027 23 17 ETB 127 87 57 W
030 24 18 CAN 130 88 58 X
031 25 19 EM 131 89 59 Y
032 26 1A SUB 132 90 5A Z
033 27 1B ESC 133 91 5B [
034 28 1C FS 134 92 5C \
035 29 1D GS 135 93 5D ]
036 30 1E RS 136 94 5E ^
037 31 1F US 137 95 5F _
040 32 20 SPACE 140 96 60 `
041 33 21 ! 141 97 61 a
042 34 22 " 142 98 62 b
043 35 23 # 143 99 63 c
044 36 24 $ 144 100 64 d
045 37 25 % 145 101 65 e
046 38 26 & 146 102 66 f
047 39 27 ' 147 103 67 g
050 40 28 ( 150 104 68 h
051 41 29 ) 151 105 69 i
052 42 2A * 152 106 6A j
053 43 2B + 153 107 6B k
054 44 2C , 154 108 6C l
055 45 2D - 155 109 6D m
056 46 2E . 156 110 6E n
057 47 2F / 157 111 6F o
060 48 30 0 160 112 70 p
061 49 31 1 161 113 71 q
062 50 32 2 162 114 72 r
063 51 33 3 163 115 73 s
064 52 34 4 164 116 74 t
065 53 35 5 165 117 75 u
066 54 36 6 166 118 76 v
067 55 37 7 167 119 77 w
070 56 38 8 170 120 78 x
071 57 39 9 171 121 79 y
072 58 3A : 172 122 7A z
073 59 3B ; 173 123 7B {
074 60 3C < 174 124 7C |
075 61 3D = 175 125 7D }
076 62 3E > 176 126 7E ∼
077 63 3F ? 177 127 7F DEL
(一个有趣的四列版本在罗比的垃圾,四列 ASCII 在 https://garbagecollected.org/2017/01/31/four-column-ascii/ 。)
ASCII 最常见的编码使用代码点作为 7 位字节,因此例如“A”的编码是 65。
这套其实是美国 ASCII。由于欧洲人对重音字符的需求,一些标点符号被省略以形成最小集合,ISO 646,而有适合欧洲字符的“国家变体”。朱卡·科尔佩拉的网站 http://www.cs.tut.fi/~jkorpela/chars.html 为感兴趣的人提供了更多信息。但是,对于本书中的工作,您不需要这些变体。
ISO 8859
八位字节现在是字节的标准大小。这为 ASCII 扩展提供了 128 个额外的代码点。许多不同的代码集,以捕捉欧洲各种语言子集的剧目是 ISO 8859 系列。ISO 8859-1 也被称为拉丁语-1,涵盖西欧的许多语言,而本系列中的其他语言涵盖欧洲其他地区,甚至希伯来语、阿拉伯语和泰语。例如,ISO 8859-5 包括俄国等国家的西里尔字符,而 ISO 8859-8 包括希伯来字母。
这些字符集的标准编码是使用它们的码位作为 8 位值。例如,ISO 8859-1 的字符“Á”的代码点为 193,编码为 193。所有 ISO 8859 序列的底部 128 个值都与 ASCII 相同,因此所有这些集合中的 ASCII 字符都是相同的。
用来推荐 ISO 8859-1 字符集的 HTML 规范。HTML 3.2 是最后一个这样做的,之后 HTML 4.0 推荐了 Unicode。2008 年,谷歌估计它看到的网页中,大约 20%仍然是 ISO 8859 格式,20%仍然是 ASCII 格式(见http://googleblog.blogspot.com/2010/01/unicode-nearing-50-of-web.html“Unicode 接近 50%的网页”)。更多背景信息参见 http://pinyin.info/news/2015/utf-8-unicode-vs-other-encodings-over-time/ 和 https://w3techs.com/technologies/history_overview/character_encoding 。
统一码
ASCII 和 ISO 8859 都没有涵盖基于象形文字的语言。据估计,中文大约有 20,000 个独立的汉字,其中大约有 5,000 个是通用的。这些需要不止一个字节,通常使用两个字节。这种双字节字符集有很多:中文的 Big5、EUC-TW、GB2312 和 GBK/GBX,日文的 JIS X 0208 等等。这些编码通常不相互兼容。
Unicode 是一种兼容的标准字符集,旨在涵盖所有正在使用的主要字符集。它包括欧洲、亚洲、印度等等。现在已经到了 9.0 版本,有 128,172 个字符。代码点的数量现在超过了 65,536。这比 2¹⁶.还多这对字符编码有影响。
前 256 个码位对应于 ISO 8859-1,前 128 个是美国 ASCII 码。这样就有了与这些主要字符集的向后兼容性,因为 ISO 8859-1 和 ASCII 的码位在 Unicode 中完全相同。对于其他字符集来说,情况就不一样了:例如,虽然大部分 Big5 字符也是 Unicode 的,但是代码点却不一样。网站 http://moztw.org/docs/big5/table/unicode1.1-obsolete.txt 包含一个从 Big5 到 Unicode 的(大)表映射的例子。
要在计算机系统中表示 Unicode 字符,必须使用编码。编码 UCS 是使用 Unicode 字符的码位值的双字节编码。但是,由于现在 Unicode 中的字符太多,无法全部放入 2 个字节,这种编码已经过时,不再使用。相反,有:
- UTF-32 是一种 4 字节编码,但并不常用,HTML 5 明确警告不要使用它。
- UTF-16 将最常见的字符编码成 2 个字节,另外 2 个字节用于“溢出”,ASCII 和 ISO 8859-1 具有通常的值。
- UTF-8 使用每个字符 1 到 4 个字节,ASCII 具有通常的值(但不是 ISO 8859-1)。
- UTF-7 有时会使用,但并不常见。
UTF 8 号,走,还有符文
UTF 8 是最常用的编码。谷歌估计,在 2008 年,它看到的 50%的网页是用 UTF-8 编码的,而且这个比例还在增加。ASCII 集在 UTF-8 中具有相同的编码值,因此 UTF-8 阅读器可以阅读仅由 ASCII 字符组成的文本以及完整 Unicode 集中的文本。
Go 在其字符串中使用 UTF-8 编码的字符。每个字符的类型都是rune。这是 int32 的别名。在 UTF-8 编码中,一个 Unicode 字符最多可以有 4 个字节,因此需要 4 个字节来表示所有字符。就字符而言,字符串是一个符文数组,每个符文使用 1、2 或 4 个字节。
字符串也是一个字节数组,但是你必须小心:只有对于 ASCII 子集,一个字节等于一个字符。所有其他字符占用 2、3 或 4 个字节。这意味着字符(符文)中字符串的长度通常与其字节数组的长度不同。只有当字符串仅由 ASCII 字符组成时,它们才相等。
下面的程序片段说明了这一点。如果您获取一个 UTF-8 字符串并测试它的长度,您将获得底层字节数组的长度。但是如果你将字符串转换成一个符文数组[]rune,那么你会得到一个 Unicode 码位数组,它通常是字符数:
str := "百度一下, 你就知道"
println("String length", len([]rune(str)))
println("Byte length", len(str))
prints
String length 9
Byte length 27
Go 博客(见 https://blog.golang.org/strings )给出了关于琴弦和符文更详细的解释。
UTF-8 客户端和服务器
可能令人惊讶的是,您不需要做任何特殊的事情来处理客户端或服务器中的 UTF-8 文本。Go 中 UTF-8 字符串的底层数据类型是一个字节数组,正如我们刚刚看到的,Go 会根据需要将字符串编码成 1、2、3 或 4 个字节。字符串的长度就是字节数组的长度,所以你可以通过写字节数组来写任何 UTF-8 字符串。
类似地,要读取一个字符串,只需读入一个字节数组,然后使用string([]byte)将该数组转换为一个字符串。如果 Go 不能正确地将字节解码成 Unicode 字符,那么它给出 Unicode 替换字符\uFFFD。结果字节数组的长度是字符串合法部分的长度。
因此,前几章给出的客户机和服务器可以很好地处理 UTF-8 编码的文本。
ASCII 客户端和服务器
ASCII 字符在 ASCII 和 UTF-8 中具有相同的编码。所以普通的 UTF-8 字符处理对 ASCII 字符来说很好。不需要进行特殊处理。
UTF-16 和 Go
UTF-16 处理短 16 位无符号整数数组。utf16 软件包就是为管理这样的数组而设计的。要将一个普通的 Go 字符串(即 UTF-8 字符串)转换成 UTF-16,首先通过将它强制转换成一个[]rune来提取代码点,然后使用utf16.Encode来产生一个 uint16 类型的数组。
类似地,要将一个无符号的短 UTF-16 值数组解码成一个 Go 字符串,可以使用utf16.Decode将其转换成类型为[]rune的码位,然后转换成一个字符串。以下代码片段说明了这一点:
str := "百度一下, 你就知道"
runes := utf16.Encode([]rune(str))
ints := utf16.Decode(runes)
str = string(ints)
这些类型转换需要由客户机或服务器适当地应用,以读取和写入 16 位短整数,如下所示。
小端和大端
可惜 UTF-16 背后潜伏着一个小恶魔。它基本上是将字符编码成 16 位短整数。大问题是:对于每个 short,如何写成两个字节?先顶一个,还是先顶一个第二?只要接收方使用与发送方相同的约定,任何一种方式都可以。
Unicode 通过一种称为 BOM(字节顺序标记)的特殊字符解决了这个问题。这是一个零宽度的非打印字符,所以你永远不会在文本中看到它。但是它的值0xfffe是这样选择的,这样您就可以知道字节顺序:
- 在大端系统中,它是 FF FE
- 在小端系统中,它是 FE FF
文本有时会将 BOM 作为文本中的第一个字符。然后,读取器可以检查这两个字节,以确定使用了什么字节序。
UTF-16 客户端和服务器
使用 BOM 约定,您可以编写一个服务器,预先计划一个 BOM,并以 UTF-16 格式编写一个字符串作为UTF16Server.go:
/* UTF16 Server
*/
package main
import (
"fmt"
"net"
"os"
"unicode/utf16"
)
const BOM = '\ufffe'
func main() {
service := "0.0.0.0:1210"
tcpAddr, err := net.ResolveTCPAddr("tcp", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
str := "j'ai arrêté"
shorts := utf16.Encode([]rune(str))
writeShorts(conn, shorts)
conn.Close() // we're finished
}
}
func writeShorts(conn net.Conn, shorts []uint16) {
var bytes [2]byte
// send the BOM as first two bytes
bytes[0] = BOM >> 8
bytes[1] = BOM & 255
_, err := conn.Write(bytes[0:])
if err != nil {
return
}
for _, v := range shorts {
bytes[0] = byte(v >> 8)
bytes[1] = byte(v & 255)
_, err = conn.Write(bytes[0:])
if err != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
而读取字节流、提取并检查 BOM,然后解码流的其余部分的客户端是UTF16Client.go:
/* UTF16 Client
*/
package main
import (
"fmt"
"net"
"os"
"unicode/utf16"
)
const BOM = '\ufffe'
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
conn, err := net.Dial("tcp", service)
checkError(err)
shorts := readShorts(conn)
ints := utf16.Decode(shorts)
str := string(ints)
fmt.Println(str)
os.Exit(0)
}
func readShorts(conn net.Conn) []uint16 {
var buf [512]byte
// read everything into the buffer
n, err := conn.Read(buf[0:2])
for true {
m, err := conn.Read(buf[n:])
if m == 0 || err != nil {
break
}
n += m
}
checkError(err)
var shorts []uint16
shorts = make([]uint16, n/2)
if buf[0] == 0xff && buf[1] == 0xfe {
// big endian
for i := 2; i < n; i += 2 {
shorts[i/2] = uint16(buf[i])<<8 + uint16(buf[i+1])
}
} else if buf[1] == 0xff && buf[0] == 0xfe {
// little endian
for i := 2; i < n; i += 2 {
shorts[i/2] = uint16(buf[i+1])<<8 + uint16(buf[i])
}
} else {
// unknown byte order
fmt.Println("Unknown order")
}
return shorts
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
客户端打印服务器发送的"j'ai arrêté"。
Unicode 哥特式
这本书不是关于 i18n 问题的。特别是,我们不想深究 Unicode 的神秘领域。但是你应该知道 Unicode 不是一个简单的编码,有很多复杂的地方。例如,一些早期的字符集使用非空格字符,尤其是重音字符。这是 Unicode 中引入的,因此您可以用两种方式生成重音字符:作为单个 Unicode 字符,或者作为一对非空格重音加非重音字符。例如 U+04D6,“西里尔文大写字母 ie 带短音符”是单个字符,。相当于 U+0415,“西里尔文大写字母 ie”结合短音符 U+0306“结合短音符”。这使得字符串比较有时很困难。这可能是一些非常难以理解的错误的原因。
Go 实验树中有一个叫golang.org/x/text/unicode/norm的包,可以规格化 Unicode 字符串。它可以安装到您的 Go 软件包树中:
go get golang.org/x/text/unicode/norm
请注意,它是“子库”Go 项目树中的一个包,可能不稳定。
实际上有四种标准的 Unicode 形式。最常见的是 NFC。一个字符串可以通过norm.NFC.String(str)转换成 NFC 形式。下面这个名为norm.go的程序以两种方式形成的字符串,一种是单个字符,另一种是组合字符,并打印字符串、它们的字节,然后是规范化形式及其字节。
package main
import (
"fmt"
"golang.org/x/text/unicode/norm"
)
func main() {
str1 := "\u04d6"
str2 := "\u0415\u0306"
norm_str2 := norm.NFC.String(str2)
bytes1 := []byte(str1)
bytes2 := []byte(str2)
norm_bytes2 := []byte(norm_str2)
fmt.Println("Single char ", str1, " bytes ", bytes1)
fmt.Println("Composed char ", str2, " bytes ", bytes2)
fmt.Println("Normalized char", norm_str2, " bytes ", norm_bytes2)
}
以下是输出:
Single charbytes
Composed charbytes
Normalized charbytes
ISO 8859 和 Go
ISO 8859 系列是 8 位字符集,适用于欧洲的不同地区和其他一些地区。它们在底部都有相同的 ASCII 集,但在顶部有所不同。据谷歌称,ISO 8859 代码约占其所见网页的 20%,但现在这一比例已经下降。
第一个代码是 ISO 8859-1 或 Latin-1,其前 256 个字符与 Unicode 相同。Latin-1 字符的编码值在 UTF-16 和默认的 ISO 8859-1 编码中是相同的。但这实际上没有多大帮助,因为 UTF-16 是 16 位编码,而 ISO 8859-1 是 8 位编码。UTF-8 是一种 8 位编码,但它使用最高位来表示额外的字节,因此 UTF-8 和 ISO 8859-1 只有 ASCII 子集重叠。所以 UTF-8 也帮不上什么忙。
但是 ISO 8859 系列没有任何复杂的问题。每个字符集中的每个字符对应一个唯一的 Unicode 字符。例如,在 ISO 8859-2,字符“带 ogonek 的拉丁文大写字母 I”具有 ISO 8859-2 码位 0xc7(十六进制)和相应的 Unicode 码位 U+012E。ISO 8859 集和相应的 Unicode 字符之间的转换本质上只是一个查找表的过程。
从 ISO 8859 码点到 Unicode 码点的表可以作为 256 个整数的数组来完成。但是其中许多将具有与索引相同的值。所以我们只是使用一个不同的映射,那些不在映射中的就取索引值。
ISO 8859-2 地图的一部分如下:
var unicodeToISOMap = map[int] uint8 {
0x12e: 0xc7,
0x10c: 0xc8,
0x118: 0xca,
// plus more
}
将 UTF-8 字符串转换为 ISO 8859-2 字节数组的函数如下:
/* Turn a UTF-8 string into an ISO 8859 encoded byte array
*/
func unicodeStrToISO(str string) []byte {
// get the unicode code points
codePoints := []int(str)
// create a byte array of the same length
bytes := make([]byte, len(codePoints))
for n, v := range(codePoints) {
// see if the point is in the exception map
iso, ok := unicodeToISOMap[v]
if !ok {
// just use the value
iso = uint8(v)
}
bytes[n] = iso
}
return bytes
}
以类似的方式,您可以将 ISO 8859-2 字节数组更改为 UTF-8 字符串:
var isoToUnicodeMap = map[uint8] int {
0xc7: 0x12e,
0xc8: 0x10c,
0xca: 0x118,
// and more
}
func isoBytesToUnicode(bytes []byte) string {
codePoints := make([]int, len(bytes))
for n, v := range(bytes) {
unicode, ok :=isoToUnicodeMap[v]
if !ok {
unicode = int(v)
}
codePoints[n] = unicode
}
return string(codePoints)
}
这些函数可以用来读写 ISO 8859-2 字节形式的 UTF-8 字符串。通过改变映射表,可以覆盖其他 ISO 8859 码。Latin-1,或 ISO 8859-1,是一个特例—异常映射为空,因为 Latin-1 的代码点在 Unicode 中是相同的。您也可以对基于表映射的其他字符集使用相同的技术,比如 Windows 1252。
其他字符集和 Go
有非常多的字符集编码。根据谷歌的说法,这些通常只在网络文档中有很小的用途,随着时间的推移,有望进一步减少。但是如果你的软件想要占领所有的市场,那么你可能需要处理它们。
在最简单的情况下,查找表就足够了。但这并不总是奏效。字符编码 ISO 2022 通过使用有限状态机交换代码页来最小化字符集的大小。这是借用了一些日本编码,使事情变得非常复杂。
Go 目前只对“子库”包树中的其他字符集提供包支持。例如,包golang.org/x/text/encoding/japanese处理 EUC-JP 和 Shift JIS。
结论
这一章没有太多代码。相反,出现了一些非常复杂领域的概念。这取决于你:如果你想假设每个人都说美国英语,那么这个世界很简单。但是,如果您希望您的应用程序可供世界其他地方使用,您需要注意这些复杂性。
七、安全
尽管互联网最初被设计为一个抵御敌对代理攻击的系统,但它发展成为一个相对可信的实体的合作环境。唉,那些日子早就过去了。垃圾邮件、拒绝服务(DoS)攻击、网络钓鱼企图等等都表明,任何人使用互联网都要自担风险。
应用程序必须构建为在敌对环境中正常工作。“正确地”不再仅仅意味着程序功能方面的正确,还意味着确保传输数据的隐私性和完整性,只允许合法用户访问,以及其他安全问题。
这当然会使你的程序更加复杂。在使应用程序安全的过程中,涉及到一些困难而微妙的计算问题。尝试自己去做(比如自己编加密库)通常注定会失败。相反,您需要使用安全专业人员设计的库。
如果这让事情变得更困难,你为什么要烦恼呢?几乎每天都有关于泄露信用卡信息的报道,关于政府官员运行的私人服务器被黑客攻击的报道,以及关于系统因拒绝服务攻击而瘫痪的报道。这些攻击中有许多可能是由于面向网络的应用程序中的编码错误造成的,如缓冲区溢出、跨站点脚本和 SQL 注入。但是大量的错误可以追溯到糟糕的网络处理:密码以纯文本形式传递,安全凭证被请求但没有被检查,以及仅仅信任你所处的环境。例如,一位同事最近购买了一台家用物联网设备。他使用 wireshark 查看它在自己的网络上做了什么,发现它正在用认证令牌admin.admin发送 RTMP 消息。一个简单的攻击媒介,甚至不需要破解密码!一家知名公司制造的无人机使用了存在已知缺陷的加密技术,可以被其他无人机“窃取”。一种越来越常见的窃取数据的方法是充当“流氓”无线接入点,假装是当地咖啡店的合法接入点,但监控通过的一切,包括您的银行账户详细信息。这些都是“低垂的果实”。数据泄露的范围由 http://www.informationisbeautiful.net/visualizations/worlds-biggest-data-breaches-hacks/ 的“全球最大数据泄露”显示。
本章介绍了 Go 提供的基本加密工具,您可以将这些工具构建到您的应用程序中。如果你不这样做,你的公司损失了 100 万美元——或者更糟,你的客户损失了 100 万美元——那么责任就会回到你身上。
ISO 安全架构
分布式系统的 ISO OSI(开放系统互连)七层模型是众所周知的,并在图 7-1 中重复。
图 7-1。
The OSI seven-layer model of distributed systems
不太为人所知的是,ISO 在这个架构上建立了一系列的文档。对于我们这里的目的,最重要的是 ISO 安全架构模型,ISO 7498-2。这需要购买,但是 ITU 已经制定了一个技术上与之一致的文件,X.800,可以从 ITU 的 https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.800-199103-I!!PDF-E&type=items 获得。
功能和级别
安全系统所需的主要功能如下:
- 认证:身份证明
- 数据完整性:数据没有被篡改
- 机密性:数据不会暴露给其他人
- 公证/签名
- 访问控制
- 保证/可用性
这些是 OSI 堆栈的以下级别所必需的:
- 对等实体认证(3,4,7)
- 数据源认证(3,4,7)
- 访问控制服务(3,4,7)
- 连接机密性(1,2,3,4,6,7)
- 无连接机密性(1,2,3,4,6,7)
- 选择性字段机密性(6,7)
- 流量机密性(1,3,7)
- 恢复连接完整性(4,7)
- 无恢复的连接完整性(4,7)
- 连接完整性选择字段(7)
- 无连接完整性选择字段(7)
- 起源时的不可否认性(7)
- 收据的不可否认性(7)
机制
实现这种安全级别的机制如下:
- 对等实体认证
- 加密
- 数字签名
- 认证交换
- 数据源认证
- 加密
- 数字签名
- 访问控制服务
- 访问控制列表
- 密码
- 功能列表
- 标签
- 连接保密性
- 加密
- 路由控制
- 无连接保密性
- 加密
- 路由控制
- 选择性字段保密
- 加密
- 交通流量保密性
- 加密
- 交通填充
- 路由控制
- 恢复连接完整性
- 加密
- 数据完整性
- 无恢复的连接完整性
- 加密
- 数据完整性
- 连接完整性选择字段
- 加密
- 数据完整性
- 无连接完整性
- 加密
- 数字签名
- 数据完整性
- 无连接完整性选择字段
- 加密
- 数字签名
- 数据完整性
- 原始不可否认性
- 数字签名
- 数据完整性
- 公证
- 收据的不可否认性
- 数字签名
- 数据完整性
- 公证
数据完整性
确保数据完整性意味着提供一种测试数据未被篡改的方法。通常,这是通过将数据中的字节组成一个简单的数字来实现的。这个过程称为散列,得到的数字称为散列或散列值。
一个简单的哈希算法只是将数据中的所有字节相加。但是,这仍然允许几乎任何数量的数据更改,并且仍然保留哈希值。例如,攻击者可以交换两个字节。这保留了哈希值,但最终您可能欠某人 65,536 美元,而不是 256 美元。
用于安全目的的散列算法必须是“强有力的”,这样攻击者就很难找到具有相同散列值的不同字节序列。这使得很难根据攻击者的目的修改数据。安全研究人员一直在测试哈希算法,看看他们是否能破解它们——也就是说,找到一种简单的方法,用字节序列来匹配哈希值。他们设计了一系列被认为是强大的加密哈希算法。
Go 支持多种哈希算法,包括 MD4、MD5、RIPEMD-160、SHA1、SHA224、SHA256、SHA384 和 SHA512。就 Go 程序员而言,它们都遵循相同的模式:适当包中的函数New(或类似函数)从hash包中返回一个Hash对象。
一个hash有一个io.Writer,你把要哈希的数据写到这个 writer。可以通过Size查询哈希值中的字节数,通过Sum查询哈希值。
一个典型的例子是 MD5 散列法。这使用了md5包。哈希值是一个 16 字节的数组。这通常以 ASCII 形式打印为四个十六进制数,每个由四个字节组成。一个简单的程序是MD5Hash.go:
/* MD5Hash
*/
package main
import (
"crypto/md5"
"fmt"
)
func main() {
hash := md5.New()
bytes := []byte("hello\n")
hash.Write(bytes)
hashValue := hash.Sum(nil)
hashSize := hash.Size()
for n := 0; n < hashSize; n += 4 {
var val uint32
val = uint32(hashValue[n])<<24 +
uint32(hashValue[n+1])<<16 +
uint32(hashValue[n+2])<<8 +
uint32(hashValue[n+3])
fmt.Printf("%x ", val)
}
fmt.Println()
}
这个程序打印"b1946ac9 2492d234 7c6235b4 d2611184"。
对此的一种变体是 HMAC(键控散列消息验证码),它向散列算法添加了一个密钥。用这个变化不大。要将 MD5 散列与密钥一起使用,请将对hash := md5.New()的调用替换为:
hash := hmac.New(md5.New, []byte("secret"))
对称密钥加密
有两种主要的数据加密机制。对称密钥加密使用加密和解密都相同的单个密钥。加密和解密代理都需要知道这个密钥。没有讨论这个密钥如何在代理之间传输。
与哈希算法一样,加密算法也有很多种。现在已知许多算法都有弱点,一般来说,随着时间的推移,随着计算机速度的提高,算法会变得越来越弱。Go 支持多种对称密钥算法,如 AES 和 DES。
这些算法是块算法。也就是说,他们处理数据块。如果您的数据与块大小不一致,您必须在末尾用额外的空白填充它。
每个算法由一个Cipher对象表示。这是由NewCipher在适当的包中创建的,并以对称密钥作为参数。
一旦有了密码,就可以用它来加密和解密数据块。我们使用 AES-128,其密钥大小为 128 位(16 字节),块大小为 128 位。密钥的大小决定了使用哪个版本的 AES。说明这一点的一个程序是Aes.go:
/* Aes
*/
package main
import (
"bytes"
"crypto/aes"
"fmt"
)
func main() {
key := []byte("my key, len 16 b")
cipher, err := aes.NewCipher(key)
if err != nil {
fmt.Println(err.Error())
}
src := []byte("hello 16 b block")
var enc [16]byte
cipher.Encrypt(enc[0:], src)
var decrypt [16]byte
cipher.Decrypt(decrypt[0:], enc[0:])
result := bytes.NewBuffer(nil)
result.Write(decrypt[0:])
fmt.Println(string(result.Bytes()))
}
这使用共享的 16 字节密钥"my key, len 16 b"对 16 字节块"hello 16 b block"进行加密和解密。
公钥加密
另一种主要的加密类型是公钥加密。公钥加密和解密需要两个密钥:一个用于加密,另一个用于解密。加密密钥通常以某种方式公开,这样任何人都可以加密发送给你的消息。解密密钥必须保密;否则,每个人都可以解密这些消息!公钥系统是非对称的,不同的密钥用于不同的用途。
Go 支持的公钥加密系统有很多。一个典型的例子是 RSA 方案。
从随机数生成 RSA 私钥和公钥的程序是GenRSAKeys.go:
/* GenRSAKeys
*/
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/gob"
"encoding/pem"
"fmt"
"os"
)
func main() {
reader := rand.Reader
bitSize := 512
key, err := rsa.GenerateKey(reader, bitSize)
checkError(err)
fmt.Println("Private key primes", key.Primes[0].String(), key.Primes[1].String())
fmt.Println("Private key exponent", key.D.String())
publicKey := key.PublicKey
fmt.Println("Public key modulus", publicKey.N.String())
fmt.Println("Public key exponent", publicKey.E)
saveGobKey("private.key", key)
saveGobKey("public.key", publicKey)
savePEMKey("private.pem", key)
}
func saveGobKey(fileName string, key interface{}) {
outFile, err := os.Create(fileName)
checkError(err)
encoder := gob.NewEncoder(outFile)
err = encoder.Encode(key)
checkError(err)
outFile.Close()
}
func savePEMKey(fileName string, key *rsa.PrivateKey) {
outFile, err := os.Create(fileName)
checkError(err)
var privateKey = &pem.Block{Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key)}
pem.Encode(outFile, privateKey)
outFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
该程序还使用gob序列化来保存证书。它们可以被LoadRSAKeys.go程序读回:
/* LoadRSAKeys
*/
package main
import (
"crypto/rsa"
"encoding/gob"
"fmt"
"os"
)
func main() {
var key rsa.PrivateKey
loadKey("private.key", &key)
fmt.Println("Private key primes", key.Primes[0].String(), key.Primes[1].String())
fmt.Println("Private key exponent", key.D.String())
var publicKey rsa.PublicKey
loadKey("public.key", &publicKey)
fmt.Println("Public key modulus", publicKey.N.String())
fmt.Println("Public key exponent", publicKey.E)
}
func loadKey(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := gob.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
X.509 证书
公钥基础设施(PKI)是一个框架,用于收集公钥,以及其他信息,如所有者姓名和位置,以及它们之间的链接,从而提供某种批准机制。
目前使用的主要 PKI 基于 X.509 证书。例如,web 浏览器使用它们来验证网站的身份。
为我的网站生成自签名 X.509 证书并将其存储在一个.cer文件中的示例程序是GenX509Cert.go:
/* GenX509Cert
*/
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/gob"
"encoding/pem"
"fmt"
"math/big"
"os"
"time"
)
func main() {
random := rand.Reader
var key rsa.PrivateKey
loadKey("private.key", &key)
now := time.Now()
then := now.Add(60 * 60 * 24 * 365 * 1000 * 1000 * 1000) // one year
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "jan.newmarch.name",
Organization: []string{"Jan Newmarch"},
},
NotBefore: now,
NotAfter: then,
SubjectKeyId: []byte{1, 2, 3, 4},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
DNSNames: []string{"jan.newmarch.name", "localhost"},
}
derBytes, err := x509.CreateCertificate(random, &template,
&template, &key.PublicKey, &key)
checkError(err)
certCerFile, err := os.Create("jan.newmarch.name.cer")
checkError(err)
certCerFile.Write(derBytes)
certCerFile.Close()
certPEMFile, err := os.Create("jan.newmarch.name.pem")
checkError(err)
pem.Encode(certPEMFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certPEMFile.Close()
keyPEMFile, err := os.Create("private.pem")
checkError(err)
pem.Encode(keyPEMFile, &pem.Block{Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(&key)})
keyPEMFile.Close()
}
func
loadKey(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := gob.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
这可以由LoadX509Cert.go回读:
/* LoadX509Cert
*/
package main
import (
"crypto/x509"
"fmt"
"os"
)
func main() {
certCerFile, err := os.Open("jan.newmarch.name.cer")
checkError(err)
derBytes := make([]byte, 1000) // bigger than the file
count, err := certCerFile.Read(derBytes)
checkError(err)
certCerFile.Close()
// trim the bytes to actual length in call
cert, err := x509.ParseCertificate(derBytes[0:count])
checkError(err)
fmt.Printf("Name %s\n", cert.Subject.CommonName)
fmt.Printf("Not before %s\n", cert.NotBefore.String())
fmt.Printf("Not after %s\n", cert.NotAfter.String())
}
func checkError(err error
) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
坦克激光瞄准镜(Tank Laser-Sight 的缩写)
如果你必须自己做所有的重活,加密/解密方案的用处是有限的。目前,互联网上支持加密消息传递的最流行的机制是 TLS(传输层安全性),其前身是 SSL(安全套接字层)。
在 TLS 中,客户端和服务器使用 X.509 证书协商身份。一旦完成,它们之间就发明了一个密钥,所有的加密/解密都是用这个密钥完成的。协商相对较慢,但一旦完成,就会使用更快的密钥机制。服务器需要有证书;如果需要,客户可以有一个。
基本客户
我们首先说明连接到一个服务器,该服务器具有由“众所周知的”认证机构(CA)如 RSA 签署的证书。从 web 服务器获取报头信息的程序可以适用于从 TLS web 服务器获取报头信息。节目是TLSGetHead.go。(我们在这里举例说明TLS.Dial,并将在后面的章节中讨论 HTTPS。)
/* TLSGetHead
*/
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
conn, err := tls.Dial("tcp", service, nil)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
conn.Close()
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
当针对适当的站点运行时,例如 www.google.com:443 :
go run TLSGetHead.go www.google.com:443
它会产生如下输出:
HTTP/1.0 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Location: https://www.google.com.au/?gfe_rd=cr&ei=L3lvWKSXMdPr8AfvhqKIBg
Content-Length: 263
Date: Fri, 06 Jan 2017 11:02:07 GMT
Alt-Svc: quic=":443"; ma=2592000; v="35,34"
其他站点可能会产生其他响应,但是这个客户端仍然很高兴已经与正确的身份验证服务器建立了 TLS 会话。
有趣的是运行这个网站 www.gooogle.com (注意多余的 o!):
go run TLSGetHead.go www.gooogle.com:443
这个网站实际上属于谷歌,因为他们可能是为了降低欺诈风险而购买的。该程序抛出一个致命错误,因为站点证书不适用于带有三个操作系统的 gooogle:
Fatal error x509: certificate is valid for google.com, *.2mdn.net, *.android.com, *.appengine.google.com, *.au.doubleclick.net, *.cc-dt.com, *.cloud.google.com, ...
指向同一个三 o 网站的浏览器如 Firefox 也会发出安全警报。
使用自签名证书的服务器
如果服务器使用自签名证书,可能在组织内部使用或者在试验时使用,那么 Go package when 将生成一个错误:"x509: certificate signed by unknown authority"。证书必须安装到客户端的操作系统中(这将依赖于操作系统),或者客户端必须将证书安装为根 CA。我们将展示第二种方式。
使用 TLS 和任何证书的 echo 服务器是TLSEchoServer.go:
/* TLSEchoServer
*/
package main
import (
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"os"
"time"
)
func main() {
cert, err := tls.LoadX509KeyPair("jan.newmarch.name.pem", "private.pem")
checkError(err)
config := tls.Config{Certificates: []tls.Certificate{cert}}
now := time.Now()
config.Time = func() time.Time { return now }
config.Rand = rand.Reader
service := "0.0.0.0:1200"
listener, err := tls.Listen("tcp", service, &config)
checkError(err)
fmt.Println("Listening")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err.Error())
continue
}
fmt.Println("Accepted")
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for {
fmt.Println("Trying to read")
n, err := conn.Read(buf[0:])
if err != nil {
fmt.Println(err)
return
}
_, err = conn.Write(buf[0:n])
if err != nil {
return
}
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
如果证书是自签名的,简单的 TLS 客户端将无法与此服务器一起工作,这里就是这样。我们需要将一个配置作为第三个参数设置为TLS. Dial,它将我们的证书安装为根证书。感谢 Josh Bleecher Snyder 在“获取 x509:由未知权威签署的证书”( https://groups.google.com/forum/#!topic/golang-nuts/v5ShM8R7Tdc )中展示了如何做到这一点。然后,服务器与TLSEchoClient.go客户机一起工作。
/* TLSEchoClient
*/
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
// Load the PEM self-signed certificate
certPemFile, err := os.Open("jan.newmarch.name.pem")
checkError(err)
pemBytes := make([]byte, 1000) // bigger than the file
_, err = certPemFile.Read(pemBytes)
checkError(err)
certPemFile.Close()
// Create a new certificate pool
certPool := x509.NewCertPool()
// and add our certificate
ok := certPool.AppendCertsFromPEM(pemBytes)
if !ok {
fmt.Println("PEM read failed")
} else {
fmt.Println("PEM read ok")
}
// Dial, using a config
with root cert set to ours
conn, err := tls.Dial("tcp", service, &tls.Config{RootCAs: certPool})
checkError(err)
// Now write and read
lots
for n := 0; n < 10; n++ {
fmt.Println("Writing...")
conn.Write([]byte("Hello " + string(n+48)))
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
}
conn.Close()
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
结论
安全性本身是一个很大的领域,本章几乎没有涉及到它。然而,主要的概念已经涵盖。没有强调的是在设计阶段需要构建多少安全性:事后才想到安全性几乎总是失败的。
八、HTTP
万维网是一个主要的分布式系统,拥有数百万用户。通过运行 HTTP 服务器,站点可以成为 web 主机。虽然 web 客户端通常是拥有浏览器的用户,但是还有许多其他的“用户代理”,例如 web 蜘蛛、web 应用程序客户端等等。
Web 建立在 HTTP(超文本传输协议)之上,而 HTTP 又是 TCP 之上的一层。HTTP 已经经历了四个公开的版本。版本 1.1(第三个版本)是最常用的,但是预计将会快速过渡到 HTTP/2,并且这现在占当前流量的 10%以上。
本章是 HTTP 的概述,接着是管理 HTTP 连接的 Go APIs。
URL 和资源
URL 指定资源的位置。资源通常是静态文件,如 HTML 文档、图像或声音文件。但越来越多的是,它可能是一个动态生成的对象,可能基于存储在数据库中的信息。
当用户代理请求资源时,返回的不是资源本身,而是该资源的某种表示。例如,如果资源是一个静态文件,那么发送给用户代理的就是该文件的副本。
多个 URL 可能指向同一个资源,HTTP 服务器将为每个 URL 返回适当的资源表示。例如,一家公司可能使用同一产品的不同 URL 在内部和外部提供产品信息。产品的内部表示可能包括产品的内部联系人等信息,而外部表示可能包括销售产品的商店的位置。
这种资源视图意味着 HTTP 协议可以相当简单直接,而 HTTP 服务器可以任意复杂。HTTP 必须将来自用户代理的请求传递给服务器并返回一个字节流,而服务器可能必须对请求进行大量的处理。
I18n
互联网的日益国际化带来了一些复杂的问题。主机名可能以国际化的形式给出,称为 IDN(国际化域名)。为了保持与不理解 Unicode 的遗留实现(如旧的电子邮件服务器)的兼容性,非 ASCII 域名被映射到称为 punycode 的 ASCII 表示中。例如,域名日本語。jp 具有 punycode 值xn—wgv71a119e.jp。从非 ASCII 域到 punycode 值的转换不是由 Go net 库自动执行的(从 Go 1.7 开始),但是有一个名为golang.org/x/net/idna的扩展包可以在 Unicode 和它的 punycode 值之间进行转换。在“弄清 IDNA 一派胡言的故事”( https://github.com/golang/go/issues/13835 )上正在进行一场关于这个话题的讨论。
国际化域名开启了所谓的同形异义词攻击的可能性。许多 Unicode 字符具有相似的外观,例如俄语 o (U+043E)、希腊语 o (U+03BF)和英语 o (U+006F)。使用同形异义词的域名,如google.com(带有两个俄语 o,可能会引起混乱。已知有多种防御措施,比如总是显示 punycode(这里是xn—ggle-55da.com,使用 Punycode 转换器)。
URI/URL 中的路径处理起来更复杂,因为它指的是相对于可能在特定本地化环境中运行的 HTTP 服务器的路径,编码可能不是 UTF-8,甚至不是 Unicode。IRI(国际化资源标识符)通过首先将任何本地化字符串转换为 UTF-8,然后对任何非 ASCII 字节进行百分比转义来管理这一点。名为“多语言网址介绍”( https://www.w3.org/International/articles/idn-and-iri/ ))的 W3C 页面有更多信息。从其他编码到 UTF-8 的转换在第六章中有介绍,而 Go 在Queryescape/Queryunescape的net /url 和PathEscape/PathUnescape的 Go 1.8 中有函数来做百分比转换。
HTTP 特征
HTTP 是一种无状态、无连接、可靠的协议。在最简单的形式中,来自用户代理的每个请求都被可靠地处理,然后连接被中断。
在 HTTP 的最早版本中,每个请求都涉及一个单独的 TCP 连接,所以如果需要很多资源(比如嵌入在 HTML 页面中的图像),那么就必须在很短的时间内建立和拆除很多 TCP 连接。
HTTP 1.1 在 HTTP 中加入了很多优化,在简单的结构上增加了复杂性,但却创造了更高效可靠的协议。为了进一步提高效率,HTTP/2 采用了二进制形式。
版本
HTTP 有四个版本:
- 0.9 版(1991 年):完全过时
- 版本 1.0 (1996):几乎过时
- 版本 1.1 (1999):目前最流行的版本
- 版本 2 (2015):最新版本
每个版本都必须理解早期版本的请求和响应。
HTTP 0.9
请求格式:
Request = Simple-Request
Simple-Request = "GET" SP Request-URI CRLF
响应格式
响应的形式如下:
Response = Simple-Response
Simple-Response = [Entity-Body]
HTTP 1.0
这个版本为请求和响应添加了更多的信息。而不是“增长”0.9 格式,它只是留在新版本旁边。
请求格式
从客户端到服务器的请求格式是:
Request = Simple-Request | Full-Request
Simple-Request = "GET" SP Request-URI CRLF
Full-Request = Request-Line
*(General-Header
| Request-Header
| Entity-Header)
CRLF
[Entity-Body]
一个Simple-Request是一个 HTTP/0.9 请求,必须由一个Simple-Response回复。
一个Request-Line有这样的格式:
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
在哪里
Method = "GET" | "HEAD" | POST |
extension-method
这里有一个例子:
GET http://jan.newmarch.name/index.html HTTP/1.0
响应格式
响应的形式如下:
Response = Simple-Response | Full-Response
Simple-Response = [Entity-Body]
Full-Response = Status-Line
*(General-Header
| Response-Header
| Entity-Header)
CRLF
[Entity-Body]
Status-Line给出关于请求命运的信息:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
这里有一个例子:
HTTP/1.0 200 OK
状态行中的状态代码如下:
Status-Code = "200" ; OK
| "201" ; Created
| "202" ; Accepted
| "204" ; No Content
| "301" ; Moved permanently
| "302" ; Moved temporarily
| "304" ; Not modified
| "400" ; Bad request
| "401" ; Unauthorized
| "403" ; Forbidden
| "404" ; Not found
| "500" ; Internal server error
| "501" ; Not implemented
| "502" ; Bad gateway
| "503" | Service unavailable
| extension-code
General- Header通常是日期,而Response-Header是位置、服务器或认证字段。
Entity- Header包含了关于Entity-Body的有用信息,可以跟随:
Entity-Header = Allow
| Content-Encoding
| Content-Length
| Content-Type
| Expires
| Last-Modified
| extension-header
例如,(其中字段类型在//之后给出):
HTTP/1.1 200 OK // status line
Date: Fri, 29 Aug 2003 00:59:56 GMT // general header
Server: Apache/2.0.40 (Unix) // response header
Content-Length: 1595 // entity header
Content-Type: text/html; charset=ISO-8859-1 // entity header
HTTP 1.1
HTTP 1.1 修复了 HTTP 1.0 的许多问题,但也因此变得更加复杂。这个版本是通过扩展或细化 HTTP 1.0 的可用选项来实现的。例如:
-
还有更多命令如
TRACE和CONNECT -
HTTP 1.1 收紧了请求 URL 的规则,以允许代理处理。如果请求是通过代理定向的,URL 应该是绝对 URL,如:
GET http://www.w3.org/index.html HTTP/1.1否则应该使用绝对路径,并且应该包括一个
Host头字段,如:GET /index.html HTTP/1.1 Host: www.w3.org -
有更多的属性,如
If-Modified-Since,也供代理使用
这些变化包括
- 主机名标识(允许虚拟主机)
- 内容协商(多种语言)
- 持久连接(减少 TCP 开销;这个很复杂)
- 分块传输
- 字节范围(文档的请求部分)
- 代理支持
HTTP/2
所有早期版本的 HTTP 都是基于文本的。HTTP/2 最大的不同在于它是一种二进制格式。为了确保向后兼容,这不能通过向旧服务器发送二进制消息来查看它做了什么来管理。取而代之的是发送一个带有额外属性的 HTTP 1.1 消息,本质上是询问服务器是否想切换到 HTTP/2。如果它不理解额外的字段,它会用一个普通的 HTTP 1.1 响应进行回复,会话继续使用 HTTP 1.1。
否则,服务器可以响应它愿意改变,并且会话可以使用 HTTP/2 继续。
0.9 协议用了一页。在大约 20 页中描述了 1.0 协议,并且包括 0.9 协议。1.1 协议需要 120 页,是对 1.0 的实质性扩展,而 HTTP/2 需要大约 96 页。HTTP/2 规范只是对 HTTP 1.1 规范的补充。
简单用户代理
浏览器等用户代理发出请求并得到响应。这涉及到 Go 类型和相关的方法调用。
响应类型
响应类型如下:
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
Header map[string][]string
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Trailer map[string][]string
Request *Request // the original request
TLS *tls.ConnectionState // info about the TLS connection or nil
}
头部方法
我们通过例子来检验这种数据结构。每个 HTTP 请求类型在net/http包中都有自己的 Go 函数。最简单的请求来自一个名为HEAD的用户代理,它请求关于资源及其 HTTP 服务器的信息。该功能可用于进行查询:
func Head(url string) (r *Response, err error)
响应的状态在响应字段Status中,而字段Header是 HTTP 响应中报头字段的映射。一个名为Head.go的程序发出这个请求并显示结果如下:
/* Head
*/
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
url := os.Args[1]
response, err := http.Head(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
fmt.Println(response.Status)
for k, v := range response.Header {
fmt.Println(k+":", v)
}
os.Exit(0)
}
针对资源运行时,如下所示:
go run Head.go http://www.golang.com/
它会打印出这样的内容:
200 OK
Date: [Fri, 06 Jan 2017 11:20:37 GMT]
Server: [Google Frontend]
Content-Length: [7902]
Alt-Svc: [quic=":443"; ma=2592000; v="35,34"]
Strict-Transport-Security: [max-age=31536000; preload]
Content-Type: [text/html; charset=utf-8]
X-Cloud-Trace-Context: [6e28ebc86bb1026ae7b784c891d0117c]
响应来自我们控制之外的服务器,它可能会在途中经过其他服务器。显示的字段可能会有所不同,当然字段的值也会有所不同。
GET 方法
通常,我们想要检索一个资源的表示,而不仅仅是获取关于它的信息。GET请求将做到这一点,并且可以使用以下方法来完成:
func Get(url string) (r *Response, finalURL string, err error)
响应的内容在类型为io.ReadCloser的响应字段Body中。我们可以用程序Get.go将内容打印到屏幕上:
/* Get
*/
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"os"
"strings
"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
url := os.Args[1]
response, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
if response.Status != "200 OK" {
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("The response header is")
b, _ := httputil.DumpResponse(response, false)
fmt.Print(string(b))
contentTypes := response.Header["Content-Type"]
if !acceptableCharset(contentTypes) {Arial
fmt.Println("Cannot handle", contentTypes)
os.Exit(4)
}
fmt.Println("The response body is")
var buf [512]byte
reader := response.Body
for {
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func acceptableCharset(contentTypes []string) bool {
// each type is like [text/html; charset=utf-8]
// we want the UTF-8 only
for _, cType := range contentTypes {
if strings.Index(cType, "utf-8") != -1 {
return true
}
}
return false
}
http://www.golang.com 运行时为:
go run Get.go http://www.golang.com
响应标头是:
HTTP/2.0 200 OK
Content-Length: 7902
Alt-Svc: quic=":443"; ma=2592000; v="35,34"
Content-Type: text/html; charset=utf-8
Date: Fri, 06 Jan 2017 11:29:12 GMT
Server: Google Frontend
Strict-Transport-Security: max-age=31536000; preload
X-Cloud-Trace-Context: ea9b41b4796f379af487388b1474ed4e
响应正文是:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#375EAB">
<title>The Go Programming Language</title>
...
(注意,这是通过 HTTP/2 发送的。Go 库已经为你进行了版本协商。)
请注意,有一些重要的字符集问题,就像上一章讨论的那样。服务器将使用某种字符集编码,可能还有某种传输编码来传递内容。通常这是用户代理和服务器之间的协商问题,但是我们使用的简单的GET命令不包括协商的用户代理部分。因此服务器可以发送它想要的任何字符编码。
第一次写作的时候,我在中国(可以访问谷歌)。当我在 www.google.com 上试用这个程序时,谷歌的服务器试图通过猜测我的位置并向我发送中文字符集 Big5 的文本来帮助我。如何告诉服务器什么样的字符编码对我来说是可以的将在后面讨论。
配置 HTTP 请求
Go 还为用户代理提供了一个与 HTTP 服务器通信的底层接口。如您所料,它不仅让您对客户机请求有更多的控制权,而且还要求您在构建请求时花费更多的精力。然而,复杂度只有很小的增加。
用于构建请求的数据类型是类型Request。这是一个复杂的类型,我们现在只显示主要字段。省略了几个字段和完整的 Go 文档。
type Request struct {
Method string // GET, POST, PUT, etc.
URL *url.URL // Parsed URL.
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// A header maps request lines to their values.
Header Header // map[string][]string
// The message body.
Body io.ReadCloser
// ContentLength records the length of the associated content.
// The value -1 indicates that the length is unknown.
// Values >= 0 indicate that the given number of bytes may be read from Body.
ContentLength int64
// TransferEncoding lists the transfer encodings from outermost to innermost.
// An empty list denotes the "identity" encoding.
TransferEncoding []string
// The host on which the URL is sought.
// Per RFC 2616, this is either the value of the Host: header
// or the host name given in the URL itself.
Host string
}
请求中可以存储许多信息。您不需要填写所有字段,只需填写感兴趣的字段。创建具有默认值的请求的最简单方法是使用,例如:
request, err := http.NewRequest("GET", url.String(), nil)
创建请求后,您可以修改字段。例如,要指定您只想接收 UTF-8,向请求添加一个Accept-Charset字段,如下所示:
request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0")
(请注意,除非列表中明确提到,否则默认设置 ISO-8859-1 的值始终为 1。HTTP 1.1 规范可以追溯到 1999 年!)
客户端设置一个charset请求很简单。但是对于服务器的字符集返回值会发生什么情况,还有些困惑。返回的资源应该有一个指定内容媒体类型的Content-Type,比如text/html。如果合适,媒体类型应该说明字符集,例如text/html; charset=UTF-8。如果没有字符集规范,那么根据 HTTP 规范,它应该被视为默认的 ISO8859-1 字符集。但是 HTML4 规范声明,由于很多服务器不符合这一点,所以你不能做任何假设。
如果在服务器的Content-Type中有指定的字符集,那么假设它是正确的。如果没有指定,因为超过 50%的页面是 UTF-8,有些是 ASCII,它是安全的假设 UTF-8。少于 10%的页面可能是错误的:-(。
客户端对象
要向服务器发送请求并获得回复,便利对象Client是最简单的方法。这个对象可以管理多个请求,并将处理诸如服务器是否保持 TCP 连接活动等问题。
这在下面的程序ClientGet.go中有说明。
该程序显示了如何添加 HTTP 头,因为我们添加头Accept-Charset只接受 UTF-8。这里有一个小问题,是由 Go 中的一个 bug 引起的,这个 bug 只在 Go 1.8 中得到修复。如果得到 301、302、303 或 307 响应,Client.Do函数将自动进行重定向。在 Go 1.8 之前,它不会在这个重定向中跨 HTTP 头进行复制。
如果你尝试访问一个像 http://www.google.com 这样的站点,那么它会重定向到一个像 http://www.google.com.au 这样的站点,但是会丢失Accept-Charset头并返回 ISO8859-1(根据 1999 HTTP 1.1 规范它应该这样做!).附带条件是,该程序在 Go 1.8 之前的版本中可能不会给出正确的结果,该程序如下所示:
/* ClientGet
*/
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
)
func
main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "http://host:port/page")
os.Exit(1)
}
url, err := url.Parse(os.Args[1])
checkError(err)
client := &http.Client{}
request, err := http.NewRequest("HEAD", url.String(), nil)
// only accept UTF-8
request.Header.Add("Accept-Charset", "utf-8;q=1, ISO-8859-1;q=0")
checkError(err)
response, err := client.Do(request)
checkError(err)
if response.Status != "200 OK" {
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("The response header is")
b, _ := httputil.DumpResponse(response, false)
fmt.Print(string(b))
chSet := getCharset(response)
if chSet != "utf-8" {
fmt.Println("Cannot handle", chSet)
os.Exit(4)
}
var buf [512]byte
reader := response.Body
fmt.Println("got body")
for {
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func getCharset(response *http.Response) string {
contentType := response.Header.Get("Content-Type")
if contentType == "" {
// guess
return "utf-8"
}
idx := strings.Index(contentType, "charset=")
if idx == -1 {
// guess
return "utf-8"
}
chSet := strings.Trim(contentType[idx+8:], " ")
return strings.ToLower(chSet)
}
func
checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
程序运行如下,例如:
go run ClientGet.go http://www.golang.com
代理处理
现在 HTTP 请求通过特定的 HTTP 代理是很常见的。这是构成 TCP 连接并在应用层起作用的服务器的补充。公司使用代理来限制他们自己的员工可以看到的内容,而许多组织使用 Cloudflare 等代理服务来充当缓存,从而减少组织自己的服务器上的负载。通过代理访问网站需要客户端进行额外的处理。
简单代理
HTTP 1.1 阐述了 HTTP 应该如何通过代理工作。应该向代理发出一个GET请求。但是,请求的 URL 应该是目的地的完整 URL。此外,HTTP 头应该包含一个设置为代理的Host字段。只要代理被配置为传递这样的请求,那么这就是所有需要做的事情。
Go 认为这是 HTTP 传输层的一部分。为了管理这个,它有一个类Transport。这包含一个可以设置为返回代理 URL 的函数的字段。如果我们有一个 URL 作为代理的字符串,则会创建适当的传输对象,然后将其提供给一个客户端对象,如下所示:
proxyURL, err := url.Parse(proxyString)
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
然后,客户端可以像以前一样继续。
以下程序ProxyGet.go说明了这一点
/* ProxyGet
*/
package main
import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
os.Exit(1)
}
proxyString := os.Args[1]
proxyURL, err := url.Parse(proxyString)
checkError(err)
rawURL := os.Args[2]
url,err := url.Parse(rawURL)
checkError(err)
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
request, err := http.NewRequest("GET", url.String(), nil)
urlp, _ := transport.Proxy(request)
fmt.Println("Proxy ", urlp)
dump, _ := httputil.DumpRequest(request, false)
fmt.Println(string(dump))
response, err := client.Do(request)
checkError(err)
fmt.Println("Read ok")
if response.Status != "200 OK" {
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("Response ok")
var buf [512]byte
reader := response.Body
for {
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func checkError(err error) {
if err != nil {
if err == io.EOF {
return
}
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
如果您在端口 8080 上有一个代理,比如说XYZ.com,您可以按如下方式进行测试:
go run ProxyGet.go http://XYZ.com:8080/ http://www.google.com
如果你没有合适的代理来测试这个,那么下载并安装 Squid 代理( http://www.squid-cache.org/ )到你自己的电脑上。
这个程序使用了一个已知的代理作为程序的参数。有许多方法可以让应用程序知道代理。大多数浏览器都有一个配置菜单,您可以在其中输入代理信息:这样的信息对于 Go 应用程序是不可用的。一些应用程序可能使用 Web 代理自动发现协议( https://en.wikipedia.org/wiki/Web_Proxy_Autodiscovery_Protocol )从网络中某个通常称为autoproxy.pac的文件中获取代理信息。Go 还不知道如何解析这些 JavaScript 文件,所以不能使用它们。特定的操作系统可能具有指定代理的系统特定的方式。Go 无法访问这些。但是如果在操作系统环境变量如HTTP_PROXY或http_proxy中设置了代理信息,它可以使用该函数找到代理信息:
func ProxyFromEnvironment(req *Request) (*url.URL, error)
如果您的程序运行在这样的环境中,您可以使用这个函数,而不必显式地知道代理参数。
认证代理
一些代理需要通过用户名和密码进行身份验证,以传递请求。一种常见的方案是“基本认证”,其中用户名和密码被连接成一个字符串"user:password",然后进行 Base64 编码。然后由 HTTP 请求头“Proxy-Authorization”将它提供给代理,并标记它是基本认证
下面的程序ProxyAuthGet.go说明了这一点,它将Proxy-Authentication头添加到前面的代理程序中:
/* ProxyAuthGet
*/
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
const auth = "jannewmarch:mypassword"
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: ", os.Args[0], "http://proxy-host:port http://host:port/page")
os.Exit(1)
}
proxy := os.Args[1]
proxyURL, err := url.Parse(proxy)
checkError(err)
rawURL := os.Args[2]
url, err := url.Parse(rawURL)
checkError(err)
// encode the auth
basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client := &http.Client{Transport: transport}
request, err := http.NewRequest("GET", url.String(), nil)
request.Header.Add("Proxy-Authorization", basic)
dump, _ := httputil.DumpRequest(request, false)
fmt.Println(string(dump))
// send the request
response, err := client.Do(request)
checkError(err)
fmt.Println("Read ok")
if response.Status != "200 OK" {
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("Response ok")
var buf [512]byte
reader := response.Body
for {
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func checkError(err error) {
if err != nil {
if err == io.EOF {
return
}
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
这个项目似乎没有公开的测试场地。我在使用认证代理的工作中测试了它。设置这样的代理超出了本书的范围。有一个关于如何做到这一点的讨论叫做“如何建立一个具有基本用户名和密码认证的 Squid 代理”(见 http://stackoverflow.com/questions/3297196/how-to-set-up-a-squid-proxy-with-basic-username-and-password-authentication )。
客户的 HTTPS 连接
对于安全、加密的连接,HTTP 使用 TLS,这在第七章中有所描述。HTTP+TLS 的协议被称为 HTTPS,它使用https://URL 而不是http://URL。
在客户端接受来自服务器的数据之前,服务器需要返回有效的 X.509 证书。如果证书是有效的,那么 Go 将处理幕后的一切,并且之前给定的客户端可以正常运行 https URLs。也就是说,像前面的ClientGet.go这样的程序不变地运行——你只需给它们一个 HTTPS URL。
许多网站都有无效的证书。它们可能已经过期,它们可能是自签名的,而不是由公认的证书颁发机构签名的,或者它们可能只是有错误(如服务器名不正确)。像 Firefox 这样的浏览器放了一个大大的警告通知,上面写着“让我离开这里!”按钮,但你可以继续冒险,许多人都这样做。
Go 在遇到证书错误时会立即退出。但是,您可以将客户端配置为忽略证书错误。当然,这是不可取的——证书配置错误的站点可能会有其他问题。
在第七章中,我们生成了自签名的 X.509 证书。在本章的后面,我们将给出一个使用 X.509 证书的 HTTPS 服务器,如果使用了自签名证书,那么ClientGet.go将生成这个错误:
x509: certificate signed by unknown authority
客户端通过打开传输配置标志InsecureSkipVerify来移除这些错误并继续。不安全程序是TLSUnsafeClientGet.go:
/* TLSUnsafeClientGet
*/
package main
import (
"fmt"
"net/http"
"net/url"
"os"
"strings"
"crypto/tls"
)
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "https://host:port/page")
os.Exit(1)
}
url, err := url.Parse(os.Args[1])
checkError(err)
if url.Scheme != "https" {
fmt.Println("Not https scheme ", url.Scheme)
os.Exit(1)
}
transport := &http.Transport{}
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: transport}
request, err := http.NewRequest("GET", url.String(), nil)
// only accept UTF-8
checkError(err)
response, err := client.Do(request)
checkError(err)
if response.Status != "200 OK" {
fmt.Println(response.Status)
os.Exit(2)
}
fmt.Println("get a response")
chSet := getCharset(response)
fmt.Printf("got charset %s\n", chSet)
if chSet != "UTF-8" {
fmt.Println("Cannot handle", chSet)
os.Exit(4)
}
var buf [512]byte
reader := response.Body
fmt.Println("got body")
for {
n, err := reader.Read(buf[0:])
if err != nil {
os.Exit(0)
}
fmt.Print(string(buf[0:n]))
}
os.Exit(0)
}
func getCharset(response *http.Response) string {
contentType := response.Header.Get("Content-Type")
if contentType == "" {
// guess
return "UTF-8"
}
idx := strings.Index(contentType, "charset:")
if idx == -1 {
// guess
return "UTF-8"
}
return strings
.Trim(contentType[idx:], " ")
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
服务器
构建客户机的另一面是处理 HTTP 请求的 web 服务器。最简单也是最早的服务器只是返回文件的副本。然而,在当前的服务器中,任何 URL 现在都可以触发任意计算。
文件服务器
我们从一个基本的文件服务器开始。Go 提供了一个多路复用器,即一个可以读取和解释请求的对象。它向在自己线程中运行的handlers,发出请求。因此,读取 HTTP 请求、对它们进行解码,以及在它们自己的线程中分支到合适的函数的大部分工作已经为我们完成了。
对于文件服务器,Go 也给出了一个FileServer对象,它知道如何从本地文件系统传递文件。它需要一个“根”目录,即本地系统中文件树的顶部,以及一个匹配 URL 的模式。最简单的模式是/,它是任何 URL 的顶部。这将匹配所有 URL。
考虑到这些对象,从本地文件系统传送文件的 HTTP 服务器几乎是令人尴尬的琐碎。是FileServer.go:
/* File Server
*/
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// deliver files from the directory /var/www
fileServer := http.FileServer(http.Dir("/var/www"))
// register the handler and deliver requests to it
err := http.ListenAndServe(":8000", fileServer)
checkError(err)
// That's it!
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
服务器按如下方式运行:
go run FileServer.go
这个服务器甚至发送" 404 not found "消息来请求不存在的文件资源!如果请求的文件是一个目录,它返回一个包含在<pre> ... </pre>标签中的列表,没有其他 HTML 头或标记。如果使用 Wireshark 或简单的 telnet 客户端,目录以text/html的形式发送,HTML 文件以text/html的形式发送,Perl 文件以text/x-perl的形式发送,Java 文件以text/x-java的形式发送,等等。FileServer采用了一些类型识别,并将其包含在 HTTP 请求中,但是它不像 Apache 这样的服务器那样提供对标记的控制。
处理函数
在最后一个程序中,处理程序在第二个参数中给定给了ListenAndServe。可以通过调用Handle或HandleFunc首先注册任意数量的处理程序,签名如下:
func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
ListenAndServe的第二个参数可以是nil,然后调用被分派给所有注册的处理程序。每个处理程序应该有不同的 URL 模式。例如,文件处理器可能有 URL 模式/,而函数处理器可能有 URL 模式/cgi-bin。更具体的模式优先于更一般的模式。
常见的 CGI 程序有test-cgi(写在 shell 中)和printenv(写在 Perl 中),它们打印环境变量的值。可以编写一个处理程序,以类似于PrintEnv.go的方式工作。
/* Print Env
*/
package main
import (
"fmt"
"net/http"
"os"
)
Arial
func main() {
// file handler for most files
fileServer := http.FileServer(http.Dir("/var/www"))
http.Handle("/", fileServer)
// function handler for /cgi-bin/printenv
http.HandleFunc("/cgi-bin/printenv", printEnv)
// deliver requests to the handlers
err := http.ListenAndServe(":8000", nil)
checkError(err)
// That's it!
}
func printEnv(writer http.ResponseWriter, req *http.Request) {
env := os.Environ()
writer.Write([]byte("<h1>Environment</h1>\n<pre>"))
for _, v := range env {
writer.Write([]byte(v + "\n"))
}
writer.Write([]byte("</pre>"))
}
func
checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
Note
为了简单起见,这个程序不提供格式良好的 HTML。它缺少 html、head 和 body 标签。在本地主机上运行程序并将浏览器指向http://localhost/cgi-bin/printenv会在我的电脑上产生如下输出:
Environment
XDG_VTNR=7
XDG_SESSION_ID=c2
CLUTTER_IM_MODULE=xim
XDG_GREETER_DATA_DIR=/var/lib/lightdm-data/newmarch
SESSION=gnome-flashback-compiz
GPG_AGENT_INFO=/home/newmarch/.gnupg/S.gpg-agent:0:1
TERM=xterm-256color
SHELL=/bin/bash
...
在这个程序中使用cgi-bin目录有点厚脸皮:它不像 CGI 脚本那样调用外部程序。它只是调用 Go 函数printEnv。Go 确实有能力使用os.ForkExec调用外部程序,但是还不支持像 Apache 的mod_perl这样的动态链接模块。
旁路默认多路复用器
Go 服务器收到的 HTTP 请求通常由多路复用器处理,多路复用器检查 HTTP 请求中的路径并调用适当的文件处理程序等。您可以定义自己的处理程序。这些可以通过调用http.HandleFunc注册到默认的多路复用器中,它采用一个模式和一个函数。然后,像ListenAndServe这样的函数接受一个nil处理函数。这在上一个例子中已经完成了。
然而,如果您想接管多路复用器的角色,那么您可以给一个非nil函数作为ListenAndServe的处理函数。这个函数将负责管理请求和响应。
下面的例子很简单,但是说明了它的用法。多路复用器功能简单地为所有对ServerHandler.go的请求返回一个"204 No content":
/* ServerHandler
*/
package main
import (
"net/http"
)
func main() {
myHandler := http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
// Just return no content - arbitrary headers can be set, arbitrary body
rw.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", myHandler)
}
可以通过对服务器运行telnet来测试服务器,给出如下输出:
$telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0
HTTP/1.0 204 No Content
Date: Tue, 10 Jan 2017 05:32:53 GMT
或者通过使用这个:
curl -v localhost:8080
要给出这个输出:
* Rebuilt URL to: localhost:8080/
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Wed, 08 Mar 2017 08:46:35 GMT
<
* Connection #0 to host localhost left intact
相反,可以构建任意复杂的行为。
安全超文本传输协议
对于安全、加密的连接,HTTP 使用 TLS,这在第七章中有所描述。HTTP+TLS 的协议被称为 HTTPS,它使用https://URL 而不是http://URL。
对于使用 HTTPS 的服务器,它需要一个 X.509 证书和该证书的私钥文件。Go 目前要求这些是在第七章中使用的 PEM 编码。然后用 HTTPS (HTTP+TLS)函数ListenAndServeTLS替换 HTTP 函数ListenAndServe。
前面给出的文件服务器程序可以写成一个 HTTPS 服务器为HTTPSFileServer.go:
/* HTTPSFileServer
*/
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// deliver files from the directory /var/www
fileServer := http.FileServer(http.Dir("/var/www"))
// register the handler and deliver requests to it
err := http.ListenAndServeTLS(":8000", "jan.newmarch.name.pem",
"private.pem", fileServer)
checkError(err)
// That's it!
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
例如,该服务器由https://localhost:8000/index.html访问。如果证书是自签名证书,则需要不安全的客户端来访问服务器内容。例如:
curl -kv https://localhost:8000
如果您想要一个同时支持 HTTP 和 HTTPs 的服务器,那么在它自己的go例程中运行每个监听器。
结论
Go 对 HTTP 有广泛的支持。这并不奇怪,因为 Go 的发明部分是为了满足谷歌对自己服务器的需求。本章讨论了 Go 对 HTTP 和 HTTPS 的各种级别的支持。