Golang 源码分析 - net/url 包分析

栏目: 编程工具 · 发布时间: 7年前

内容简介:的熟悉和对规范在实际应用中处于何种地位有至关重要的作用。

摘要

URL(Uniform Resource Locator) 统一资源定位符如何定义有一套完整的规范。 URL 的使用需要注意什么, jsencodeURI 又做了哪些转义?学习 Golang 对于此规范的实现,有助于 Golang

的熟悉和对规范在实际应用中处于何种地位有至关重要的作用。

背景

URL 是连接用户与服务的媒介,通过 URL 你可以获取到你需要的资源。在编程的过程中,经常会使用到。通常使用起来也非常简单,基本没有什么问题。可是当涉及到转义及空格时,经常会引起莫名的问题。以 http 开头的为 URL 的字集。

URL 的设计有一整套的规范,可以参见: 《Uniform Resource Identifier (URI): Generic Syntax》

本文将以 golang 1.11.1 版本标准库中的 net/url 进行分析,进而了解 URL 规范在实际应用中的实现。 golang 基本遵循 RFC3986 进行设计,因为一些兼容问题会作此许的修改。

学习编程语言对规范的实现,一来可以提高自己对于此语言的认识,二来可以对规范有更深刻的理解,三来可以学习下编程的思想。这些都很重要,所以有时间还想再去研究一下 Rust 中对 URL 的实现。

开始

Golang 对于实现 URL 规范让我想到了傅里叶变换,单拿整个 URL 其实挺复杂的,而 Golang 使用分而治之的方式会让代码更加清晰,实现起来也更加简单。

GolangURL 分为如下结构:

[scheme:][//[userinfo@]host][/]path[?query][#fragment]

我们可以称每个 [] 为一个组件,下文提到组件均为些概念。

规范中对于每一个 [] 中都有一定的转义规则,转义的好处是可以更好得在互联网上进行传播,而不会丢失数据。

把每块 [] 解析转义之后,如果没有错误,则是一个正确的 URL

net/url 包简介

这个包对外主要提供了 URL 的解析 Parsequery 数据的转义与反转义 QueryEscape , QueryUnescape 。我认为,转义和解析是这个包的主要功能。只要理解了这两个功能,就已经理解这个包的具体功能了。对于 URL 标准也能有个粗略的了解决。

先看看转义 QueryEscape

URL 中的转义为把非安全的字符转义为包含一个百分号(%)(%)后面跟着两个表示字符 ASCII 码的十六进制数。

// 转义 [?query] 组件中需要转义的字符
func QueryEscape(s string) string {
	return escape(s, encodeQueryComponent)
}

// 转义的基本方法,按 mode 转义不同字符,mode 有(encodePath, encodePathSegment, encodeHost,
// encodeZone, encodeUserPassword, encodeQueryComponent, encodeFragment)
func escape(s string, mode encoding) string {
	// 用于计算整个字符串需要占用多少空间,及判断是否需要转义
	spaceCount, hexCount := 0, 0

	// 先遍历一次所有字符,计算空格及转义字符有多少
	for i := 0; i < len(s); i++ {
		c := s[i]
		if shouldEscape(c, mode) {
			if c == ' ' && mode == encodeQueryComponent {
				spaceCount++
			} else {
				hexCount++
			}
		}
	}

	// 没有需要转义的,直接返回字符串
	if spaceCount == 0 && hexCount == 0 {
		return s
	}

	// 转义后原字符会用 "%AB" 表示,所以长度增加了 2 倍的转义字符数
	t := make([]byte, len(s)+2*hexCount)
	j := 0 // t 的索引,记录写入的位置
	for i := 0; i < len(s); i++ {
		switch c := s[i]; {
		case c == ' ' && mode == encodeQueryComponent: // 可以看到转义对于 query 组件,会替换空格为 + 号
			t[j] = '+'
			j++
		case shouldEscape(c, mode): // 需要转义的字符
			// 可以看到转义算法很简单
			// 1. 添加一个百分号(%)
			// 2. 取这个字符的高 4 位对应的 16 进制
			// 3. 取这个字符的低 4 位对应的 16 进制
			t[j] = '%'
			t[j+1] = "0123456789ABCDEF"[c>>4]
			t[j+2] = "0123456789ABCDEF"[c&15]
			j += 3
		default: // 不需要转义的
			t[j] = s[i]
			j++
		}
	}
	return string(t)
}

// 根据 mode 类型判断 字符 c 是否需要转义,所有规则都在 RFC3986 中:https://tools.ietf.org/html/rfc3986
// 按规则判断是否转义,就不翻译了。
func shouldEscape(c byte, mode encoding) bool {
	// §2.3 Unreserved characters (alphanum)
	if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
		return false
	}

	if mode == encodeHost || mode == encodeZone {
		// §3.2.2 Host allows
		//	sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
		// as part of reg-name.
		// We add : because we include :port as part of host.
		// We add [ ] because we include [ipv6]:port as part of host.
		// We add < > because they're the only characters left that
		// we could possibly allow, and Parse will reject them if we
		// escape them (because hosts can't use %-encoding for
		// ASCII bytes).
		switch c {
		case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
			return false
		}
	}

	switch c {
	case '-', '_', '.', '~': // §2.3 Unreserved characters (mark)
		return false

	case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
		// Different sections of the URL allow a few of
		// the reserved characters to appear unescaped.
		switch mode {
		case encodePath: // §3.3
			// The RFC allows : @ & = + $ but saves / ; , for assigning
			// meaning to individual path segments. This package
			// only manipulates the path as a whole, so we allow those
			// last three as well. That leaves only ? to escape.
			return c == '?'

		case encodePathSegment: // §3.3
			// The RFC allows : @ & = + $ but saves / ; , for assigning
			// meaning to individual path segments.
			return c == '/' || c == ';' || c == ',' || c == '?'

		case encodeUserPassword: // §3.2.1
			// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in
			// userinfo, so we must escape only '@', '/', and '?'.
			// The parsing of userinfo treats ':' as special so we must escape
			// that too.
			return c == '@' || c == '/' || c == '?' || c == ':'

		case encodeQueryComponent: // §3.4
			// The RFC reserves (so we must escape) everything.
			return true

		case encodeFragment: // §4.1
			// The RFC text is silent but the grammar allows
			// everything, so escape nothing.
			return false
		}
	}

	if mode == encodeFragment {
		// RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are
		// included in reserved from RFC 2396 §2.2. The remaining sub-delims do not
		// need to be escaped. To minimize potential breakage, we apply two restrictions:
		// (1) we always escape sub-delims outside of the fragment, and (2) we always
		// escape single quote to avoid breaking callers that had previously assumed that
		// single quotes would be escaped. See issue #19917.
		switch c {
		case '!', '(', ')', '*':
			return false
		}
	}

	// Everything else must be escaped.
	return true
}

接着看看反转义 QueryUnescape

// 此方法是 `QueryEscape` 的逆运算
// 转换每三个像 "%AB" 的字符为十六进制 0xAB.
// 当百分号(%)后跟没有跟着正确的十六进制则抛出异常
func QueryUnescape(s string) (string, error) {
	return unescape(s, encodeQueryComponent)
}

// 按 mode 类型来反转义字符串 s,一般按组件来调用这个方法。
func unescape(s string, mode encoding) (string, error) {
	// 计数,百分号(%)的个数,也就是有多少个转义的字符数
	n := 0
	hasPlus := false // 记录 query 组件中是否出现加(+)号
	for i := 0; i < len(s); {
		switch s[i] {
		case '%':
			n++
			// 三种情况说明是不合法的 URL 转义
			// 1. 百分号(%)后不足 2 位。
			// 2. 百分号(%)后一位不是合法的十六进制字符
			// 3. 百分号(%)后二位不是合法的十六进制字符
			// 这里 ishex 其它就是判断当前字符是否在 "0123456789ABCDEF" 内
			// 用的字符直接比较,'0' <= c && c <= '9', 'a' <= c && c <= 'f'
			// 'A' <= c && c <= 'F',我想这么比较应该会比字符串比较来得更快。
			if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
				s = s[i:]
				if len(s) > 3 {
					s = s[:3]
				}
				return "", EscapeError(s)
			}
			// https://tools.ietf.org/html/rfc3986#page-21
			// 在 host 组件中 "%AB" 这种转义方式只能对于非 ASIIC 码中的字符
			// 不过在这个规范中 https://tools.ietf.org/html/rfc6874#section-2
			// 提及了 %25 允许在 host 组件的 IPv6作用域地址进行转义
			// unhex(s[i+1]) < 8 的意思是字符 s[i+1] 是 ASIIC 码中的字符
			// 我是这么理解的,unhex(s[i+1]) < 8 表示取值范围是 s[i+1] 的取值范围
			// 是 0-7, s[i+2] 取值为 0-15,正好是 8*16=128,十六进行的前 127 个为
			// ASIIC 码中的字符。
			if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" {
				return "", EscapeError(s[i : i+3])
			}
			if mode == encodeZone {
				// 这段没看懂,有朋友懂的还请告知,原注释如下
				// RFC 6874 says basically "anything goes" for zone identifiers
				// and that even non-ASCII can be redundantly escaped,
				// but it seems prudent to restrict %-escaped bytes here to those
				// that are valid host name bytes in their unescaped form.
				// That is, you can use escaping in the zone identifier but not
				// to introduce bytes you couldn't just write directly.
				// But Windows puts spaces here! Yay.
				v := unhex(s[i+1])<<4 | unhex(s[i+2])
				if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) {
					return "", EscapeError(s[i : i+3])
				}
			}
			i += 3
		case '+':
			hasPlus = mode == encodeQueryComponent
			i++
		default:
			if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
				return "", InvalidHostError(s[i : i+1])
			}
			i++
		}
	}

	// 没有被转义,且 query 组件中不含加号(+)
	if n == 0 && !hasPlus {
		return s, nil
	}

	// 分配最小的空间
	t := make([]byte, len(s)-2*n)
	j := 0
	for i := 0; i < len(s); {
		switch s[i] {
		case '%':
			// 之前 escape 的逆运算
			t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
			j++
			i += 3
		case '+':
			if mode == encodeQueryComponent {
				t[j] = ' '
			} else {
				t[j] = '+'
			}
			j++
			i++
		default:
			t[j] = s[i]
			j++
			i++
		}
	}
	return string(t), nil
}

再聊聊解析 Parse

// 解析 rawurl(URL 字符串,可以是相对路径,不包含 host 组件) 返回 URL 结构
func Parse(rawurl string) (*URL, error) {
	// 截取 # 之后的,获取 fragment 组件
	// 此 split 方法当第三个参数
	// 为 true 时,第二个值返回不包含 # 的两段;
	// 为 false 时, 第二个值返回包含 # 的两段;
	u, frag := split(rawurl, "#", true)
	url, err := parse(u, false)
	if err != nil {
		return nil, &Error{"parse", u, err}
	}
	if frag == "" {
		return url, nil
	}
	// 反转义 frag 成功则将解析出的 fragment 添加到 URL 结构体
	if url.Fragment, err = unescape(frag, encodeFragment); err != nil {
		return nil, &Error{"parse", rawurl, err}
	}
	return url, nil
}

// 解析 URL 基础方法,第二个参数 viaRequest 为 true 时只接受绝对地址,否则允许所有形式的相对url
// 此方法授受的 rawurl 不包含 fragment 组件
func parse(rawurl string, viaRequest bool) (*URL, error) {
	var rest string
	var err error

	if rawurl == "" && viaRequest {
		return nil, errors.New("empty url")
	}
	url := new(URL)

	if rawurl == "*" {
		url.Path = "*"
		return url, nil
	}

	// 获取 URL 中的 Scheme,例如:"http:", "mailto:", "ftp:"。不能包含转义字符。
	if url.Scheme, rest, err = getscheme(rawurl); err != nil {
		return nil, err
	}
	// 这里可以看到协议是不区分大小写的
	url.Scheme = strings.ToLower(url.Scheme)

	// 以问号(?)结尾,且只有一个问号(?)。reset 重置,去除问号(?)
	if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 {
		url.ForceQuery = true
		rest = rest[:len(rest)-1]
	} else {
		// 截取出 reset([//[userinfo@]host][/]path)和原始的 query 组件(url.RawQuery)的内容
		rest, url.RawQuery = split(rest, "?", true)
	}

	// reset 不是以 / 开头,按理说我们会截出像([//[userinfo@]host][/]path),会以 // 开头
	if !strings.HasPrefix(rest, "/") {
		if url.Scheme != "" {
			// We consider rootless paths per RFC 3986 as opaque.
			url.Opaque = rest
			return url, nil
		}
		if viaRequest {
			return nil, errors.New("invalid URI for request")
		}

		// Avoid confusion with malformed schemes, like cache_object:foo/bar.
		// See golang.org/issue/16822.
		//
		// RFC 3986, §3.3:
		// In addition, a URI reference (Section 4.1) may be a relative-path reference,
		// in which case the first path segment cannot contain a colon (":") character.
		colon := strings.Index(rest, ":")
		slash := strings.Index(rest, "/")
		if colon >= 0 && (slash < 0 || colon < slash) {
			// First path segment has colon. Not allowed in relative URL.
			return nil, errors.New("first path segment in URL cannot contain colon")
		}
	}

	if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
		var authority string

		// 下列对应关系
		// rest[2:] = [userinfo@]host][/]path
		// authority = [userinfo@]host
		// rest = [/]path
		authority, rest = split(rest[2:], "/", false)
		url.User, url.Host, err = parseAuthority(authority)
		if err != nil {
			return nil, err
		}
	}
	// 设置 URL 的 path 组件,如果 reset 被转义,则还会设置 URL 结构体的 RawPath 的值
	if err := url.setPath(rest); err != nil {
		return nil, err
	}
	return url, nil
}

// 设置 p 为 URL 的 path 组件,此方法将反转义 p
// p 被包含转义则 RawPath 为 p,Path 为反转义的 p
// 否则 RawPath 为空,Path 为 p
//  例:
// - setPath("/foo/bar")   Path="/foo/bar" , RawPath=""
// - setPath("/foo%2fbar") Path="/foo/bar" , RawPath="/foo%2fbar"
// p 包含不合法转义字符时,将抛出异常
func (u *URL) setPath(p string) error {
	path, err := unescape(p, encodePath)
	if err != nil {
		return err
	}
	u.Path = path
	if escp := escape(path, encodePath); p == escp {
		// 没有转义,原始的 path 既是空。
		u.RawPath = ""
	} else {
		u.RawPath = p
	}
	return nil
}

小结

net/url 包的主要实现就是这三个,其中还有一些辅助方法这里不多做介绍。

总结

看标准库对于字符的判断以及对字符空间的分配,当可确定长度的时候,坚决按需分配,减少不必要的浪费。

整体代码看起来不吃力,一来是因为着实简单,二来我觉得标准库写得确实清晰,也不仅仅是这个库。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Twenty Lectures on Algorithmic Game Theory

Twenty Lectures on Algorithmic Game Theory

Tim Roughgarden / Cambridge University Press / 2016-8-31 / USD 34.99

Computer science and economics have engaged in a lively interaction over the past fifteen years, resulting in the new field of algorithmic game theory. Many problems that are central to modern compute......一起来看看 《Twenty Lectures on Algorithmic Game Theory》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具