内容简介:的熟悉和对规范在实际应用中处于何种地位有至关重要的作用。
摘要
URL(Uniform Resource Locator)
统一资源定位符如何定义有一套完整的规范。 URL
的使用需要注意什么, js
的 encodeURI
又做了哪些转义?学习 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
使用分而治之的方式会让代码更加清晰,实现起来也更加简单。
Golang
把 URL
分为如下结构:
[scheme:][//[userinfo@]host][/]path[?query][#fragment]
我们可以称每个 []
为一个组件,下文提到组件均为些概念。
规范中对于每一个 []
中都有一定的转义规则,转义的好处是可以更好得在互联网上进行传播,而不会丢失数据。
把每块 []
解析转义之后,如果没有错误,则是一个正确的 URL
net/url
包简介
这个包对外主要提供了 URL
的解析 Parse
, query
数据的转义与反转义 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
包的主要实现就是这三个,其中还有一些辅助方法这里不多做介绍。
总结
看标准库对于字符的判断以及对字符空间的分配,当可确定长度的时候,坚决按需分配,减少不必要的浪费。
整体代码看起来不吃力,一来是因为着实简单,二来我觉得标准库写得确实清晰,也不仅仅是这个库。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Object-Oriented Design Heuristics
Arthur J. Riel / Addison-Wesley Professional / 1996-05-10 / USD 64.99
Product Description Here is the first object-oriented development book to provide specific experience-based guidelines to help developers make the right design decisions. This book offers the next ......一起来看看 《Object-Oriented Design Heuristics》 这本书的介绍吧!