聊聊WebSocket

  • WebSocket介绍

WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

  • HTTP和WebSocket之间的异同点。

WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介)。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。
与HTTP不同,WebSocket提供全双工通信。
WebSockets URI的格式如下:

ws-URI = “ws:” “//” host [ “:” port ] path [ “?” query ]
wss-URI = “wss:” “//” host [ “:” port ] path [ “?” query ]

ws和wss分别对应明文和加密连接(对比HTTP和HTTPS)。

  • WebSocket实战

因为WebSocket被设计为和HTTP/HTTPS一起工作在80/443端口并且支持HTTP代理和网关,所以一个服务端程序同时接收HTTP和WebSocket请求。对,没错,一个Server端程序可以同时接收HTTP请求和WebSocket请求。

Demo如下:

//处理普通的HTTP请求
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!"))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "index.html") 
})
http.HandleFunc("/ws/chat", func(w http.ResponseWriter, r *http.Request) {
    //这里写一个读取cookie的实例是告诉大家,在处理WebSocket的handshake请求时,服务端是可以读到该域名下的cookie的,具体细节和HTTP协议规范一致。 
    cookie, err := r.Cookie("key")
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("cookie:" + cookie.String())
    }
    //使用github.com/gorilla/websocket 
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        fmt.Println(err)
        return
    }
    //... 
    //此处conn就是一个websocket连接了,服务端可以通过conn来发送消息给客户端 
})

仔细一想,你就会明白。客户端发起ws请求,第一次握手(handshake)的请求和正常的HTTP请求体的格式是遵循的同一个规范——RFC2616。所以WebSocket的第一次握手请求正好被设计为要求和正常的HTTP请求体一样,这样就解答了为什么在一个普通的HTTP Server程序只监听一个端口的情况下可以同时处理HTTP请求和WebSocket请求了。同时也请注意对客户端而言,是建立了两个连接:一个HTTP连接,一个WebSocket连接。

如果你还是不明白,那么我换另外一种说法:

客户端发起了一个请求,可能是HTTP请求也可能是WebSocket,但是他们的请求体格式是一样的。服务端可以事先和客户端约定,根据path路径区分来源,进而决定哪一个是当HTTP请求处理:返回正常HTTP Response,哪一个当WebSocket请求处理:返回Upgrade,继而持有连接实现双向通信。

摘抄部分RFC内容如下:

The handshake MUST be a valid HTTP request as specified by [RFC2616].
The method of the request MUST be GET, and the HTTP version MUST be at least 1.1.
For example, if the WebSocket URI is “ws://example.com/chat”, the first line sent should be “GET /chat HTTP/1.1”.

 

References:

https://datatracker.ietf.org/doc/rfc6455

https://zh.wikipedia.org/wiki/WebSocket

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

 

一个关于PHP正则匹配汉字的问题

起因是一个网友提了一个问题:

$pattern='/^\w+$/';
$str="人1994";
$ret = preg_match($pattern, $str, $matches);

他想着\w是应该能够匹配到中文字符“人”的,但是实际执行结果却是不能匹配。网友们给了各种解释和解决方案,总结下来有两个可行方案:

  1. 使用/u模式修饰符[1]
  2. 在pattern中使用unicode编码表示两种方案可以解决如上问题。

代码及输出如下:

<?php


$pattern='/^\w+$/';


$str="人1994";


$ret = preg_match($pattern, $str, $matches);


var_dump($ret);


var_dump($matches);





$pattern='/^\w+$/u';


$str="人1994";


$ret = preg_match($pattern, $str, $matches);


var_dump($ret);


var_dump($matches);





//From PHP 7.0


$pattern='/^\w+$/u';


$str="\u{4eba}1994";//人 =>  \u4eba


$ret = preg_match($pattern, $str, $matches);


var_dump($ret);


var_dump($matches);








Output for hhvm-3.18.5 - 3.22.0, 7.1.0 - 7.2.4


int(0)


array(0) {


}





int(1)


array(1) {


[0]=>


string(7) "人1994"


}





int(1)


array(1) {


[0]=>


string(7) "人1994"


}








Output for 5.6.0 - 5.6.30


int(0)


array(0) {


}





int(1)


array(1) {


[0]=>


string(7) "人1994"


}





int(0)


array(0) {


}





注意:第三个例子中的语法从PHP 7.0开始支持。[2] [3]

还有一个网友提出了一个观点[4]

这个不是 PHP 的锅,而是 PCRE 库的配置导致,\w 的匹配官方文档是这么说的 

A “word” character is any letter or digit or the underscore character, that is, any character which can be part of a Perl “word”. The definition of letters and digits is controlled by PCRE’s character tables, and may vary if locale-specific matching is taking place. For example, in the “fr” (French) locale, some character codes greater than 128 are used for accented letters, and these are matched by \w.

也就是说, PCRE 库的 character tables 配置会影响到\w 的匹配

目前没有验证TA所说的是否正确。

 

目前来看最好的方式就在在写pattern时使用模式修饰符/u。

 

References:

[1]https://secure.php.net/manual/zh/reference.pcre.pattern.modifiers.php

 

[2] PHP 7.0 has introduced the “Unicode codepoint escape” syntax.

https://secure.php.net/manual/en/migration70.new-features.php#migration70.new-features.unicode-codepoint-escape-syntax

 

[3] To support large Unicode ranges (ie: [\x{E000}-\x{FFFD}] or \x{10FFFFF}) you must use the modifier ‘/u’ at the end of your expression.

https://secure.php.net/manual/en/function.preg-match.php#90771

 

[4] https://www.v2ex.com/t/262150

https://stackoverflow.com/questions/1330693/validate-username-as-alphanumeric-with-underscores

 

 

HTTP访问控制(CORS, Cross-Origin Resource Sharing)

简单来说,CORS请求分为简单请求(非术语)和预检请求(Preflight Request)。

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:

使用下列方法之一:

GET

HEAD

POST

Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:

Accept

Accept-Language

Content-Language

Content-Type (需要注意额外的限制)

DPR

Downlink

Save-Data

Viewport-Width

Width

Content-Type 的值仅限于下列三者之一:

text/plain

multipart/form-data

application/x-www-form-urlencoded

与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS   方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

 

当请求满足下述任一条件时,即应首先发送预检请求:

使用了下面任一 HTTP 方法:

PUT

DELETE

CONNECT

OPTIONS

TRACE

PATCH

人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

Accept

Accept-Language

Content-Language

Content-Type (but note the additional requirements below)

DPR

Downlink

Save-Data

Viewport-Width

Width

Content-Type 的值不属于下列之一:

application/x-www-form-urlencoded

multipart/form-data

text/plain

对于预检请求,浏览器会先发送一个预检请求给服务端,服务端正常响应(服务器是否同意客户端继续请求)后才会发送实际请求。

References:

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS