WebSocket协议理解
一、WebSocket协议是什么
1.1 WebSocket简介
WebSocket是基于TCP协议的一种应用层协议,通信模式是全双工道,也就是我们所说的长连接;TCP三次握手后,连接不断开,客户端和服务端都可以主动发起请求,两边通信互不影响,相互独立。
WebSocket是利用HTTP协议升级来实现,所以先执行HTTP请求,跟服务器一个握手过程后才正式升级。

1.2 HTTP如何升级为WebSocket(握手过程)
客户端先发起HTTP请求,HTTP协议头部内容如下:

关键参数解析:
Connection:必须设置为Upgrade,表示客户端希望连接升级。
Upgrade:Upgrade必须设置为WebSocket,表示在取得服务器响应之后,使用HTTP升级将HTTP协议转换(升级)为WebSocket协议。
Sec-WebSocket-Version:表示使用WebSocket的哪一个版本。
Sec-WebSocket-key:随机字符串,是一个 Base64 encode 的值。
服务端响应,响应内容如下:

关键参数解析:
HTTP/1.1 101 Web Socket Protocol Handshake:101状态码表示升级协议,在返回101状态码后,HTTP协议完成工作,转换为WebSocket协议。
Upgrade:Upgrade必须设置为WebSocket,表示在取得服务器响应之后,使用HTTP升级将HTTP协议转换(升级)为WebSocket协议。
Connection:Connection必须设置为Upgrade,表示客户端希望连接升级。
Sec-WebSocket-Location:与Host字段对应,表示请求WebSocket协议的地址。
Sec-WebSocket-Accept:根据Sec-WebSocket-Accept和特殊字符串计算。验证协议是否为WebSocket协议。
1.3 WebSocket协议通信格式解析
通信协议格式是WebSocket格式,服务器端采用Tcp Socket方式接收数据,进行解析,协议格式如下:

第一个字节(8位):
FIN:占1位,用来描述消息是否结束,为1的话则是该消息为消息尾部,为0则还有后续数据包;
RSV,RSV2,RSV3:各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0;
opcode:占4位,用于表示消息接收类型;0x0表示附加数据帧;0x1表示文本数据帧;0x2表示二进制数据帧;
第二个字节(8位):
MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1,数据需要解码。
Payload len:后7位;Payload len ==x
如果 x值在0-125,则是Payload len的真实长度。
如果 x值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。
如果 x值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。
Payload len之后的字节:
前四个字节是掩码,之后的字节就是业务数据;
二、使用 PHP 和 Golang 分别实现 websocket
客户端代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
<title>websocket</title>
</head>
<body>
<input id="text" value="">
<input type="submit" value="send" onclick="start()">
<input type="submit" value="close" onclick="close()">
<div id="msg"></div>
<script>
/**
webSocket.readyState
0:未连接
1:连接成功,可通讯
2:正在关闭
3:连接已关闭或无法打开
*/
//创建一个webSocket 实例
var webSocket = new WebSocket("ws://127.0.0.1:9999");
webSocket.onerror = function (event){
onError(event);
};
// 打开websocket
webSocket.onopen = function (event){
onOpen(event);
};
//监听消息
webSocket.onmessage = function (event){
onMessage(event);
};
webSocket.onclose = function (event){ //服务端关闭后 触发
onClose(event);
}
//关闭监听websocket
function onError(event){
document.getElementById("msg").innerHTML = "<p>close</p>";
console.log("error:"+event.data);
};
function onOpen(event){
console.log("open:"+sockState());
document.getElementById("msg").innerHTML = "<p>Connect to Service</p>";
};
function onMessage(event){
document.getElementById("msg").innerHTML += "<p>response:"+event.data+"</p>"
};
function onClose(event){
document.getElementById("msg").innerHTML = "<p>close</p>";
console.log("close:"+sockState());
webSocket.close();
}
function sockState(){
var status = ['未连接','连接成功,可通讯','正在关闭','连接已关闭或无法打开'];
return status[webSocket.readyState];
}
function start(event){
console.log(webSocket);
var msg = document.getElementById('text').value;
document.getElementById('text').value = '';
for(i=0;i<=0;i++){
webSocket.send(msg);
}
};
function close(event){
webSocket.close();
}
</script>
</body>
</html>
PHP服务端代码:
<?php
class SocketService
{
private $address = '0.0.0.0';
private $port = 9999;
private $_sockets;
private $clients = [];
public function __construct($address = '', $port='')
{
if(!empty($address)){
$this->address = $address;
}
if(!empty($port)) {
$this->port = $port;
}
}
public function service(){
//获取tcp协议号码。
$tcp = getprotobyname("tcp");
$sock = socket_create(AF_INET, SOCK_STREAM, $tcp);
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
if($sock < 0)
{
throw new Exception("failed to create socket: ".socket_strerror($sock)."\n");
}
socket_bind($sock, $this->address, $this->port);
socket_listen($sock, $this->port);
echo "listen on $this->address $this->port ... \n";
$this->_sockets = $sock;
}
public function run(){
$this->service();
$this->clients[] = $this->_sockets;
while (true){
$select = $this->clients;
print_r($select);
$write = NULL;
$except = NULL;
socket_select($select, $write, $except, NULL);
foreach ($select as $key => $_sock){
// print_r(json_encode($select)."\n");
print_r("选择的当前连接资源:".$key." : ".$_sock."\n");
if($this->_sockets == $_sock){ //判断是不是新接入的socket
if(($newClient = socket_accept($_sock)) === false){
die('failed to accept socket: '.socket_strerror($_sock)."\n");
}
$line = trim(socket_read($newClient, 1024));
if($line === false){
socket_shutdown($newClient);
socket_close($newClient);
return;
}
//判断是否ws
if(substr($line,0,3) == 'GET'){
$type = 1;
$this->handshaking($newClient, $line);
}else{
$type = 2;
}
//获取client ip
socket_getpeername ($newClient, $ip);
$this->clients[$ip.":".rand(0,1000).":".$type] = $newClient;
echo "Client ip:{$ip} \n";
echo "Client msg:{$line} \n";
} else {
$parm = explode(":",$key);
if($parm[2] == 1){
$this->message($_sock,$key);
}else{
$data = null;
$len_read = socket_recv($_sock, $data, 1024, 0);
if($len_read === 0 ) {
// socket closed
echo "socket error:" . socket_last_error() . ",error msg:" . socket_strerror(socket_last_error()) . "key:" . $key. PHP_EOL;
socket_close($_sock);
unset($this->clients[$key]);
break;
}
echo "Client msg:{$data} \n";
}
}
}
}
}
/**
* 握手处理
* @param $newClient socket
* @return int 接收到的信息
*/
public function handshaking($newClient, $line){
$headers = array();
$lines = preg_split("/\r\n/", $line);
foreach($lines as $line)
{
$line = rtrim($line);
if(preg_match('/^(\S+): (.*)$/', $line, $matches))
{
$headers[$matches[1]] = $matches[2];
}
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $this->address\r\n" .
"WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n".
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
return socket_write($newClient, $upgrade, strlen($upgrade));
}
/**
* 解析接收数据
* @param $buffer
* @return null|string
*/
public function message($_sock,$key){
while(true){
socket_set_nonblock($_sock);
$header = $realLen = $len = $masks = $data = $decoded = $cacheData = null;
$len_read = socket_recv($_sock, $header, 2, 0);
if ($len_read === false) {
// no data
echo "no data".PHP_EOL;
break;
}
if($len_read === 0 ) {
// socket closed
echo "socket error:" . socket_last_error() . ",error msg:" . socket_strerror(socket_last_error()) . "key:" . $key. PHP_EOL;
socket_close($_sock);
unset($this->clients[$key]);
break;
}
//判断分隔符
if(ord($header[0]) != 129){
echo "first error:".ord($header[0]).PHP_EOL;
socket_close($_sock);
unset($this->clients[$key]);
break;
}
$len = ord($header[1]) & 127;
// var_dump($len);
if ($len === 126) {
//数据长度
socket_recv($_sock, $realLen, 2, 0);
//掩码
socket_recv($_sock, $masks, 4, 0);
//真实数据
$packLen = unpack('n',$realLen)[1];
$readLen = socket_recv($_sock, $data, $packLen, 0);
} else if ($len === 127) {
//数据长度
socket_recv($_sock, $realLen, 8, 0);
//掩码
socket_recv($_sock, $masks, 4, 0);
//真实数据
$packLen = unpack('n',$realLen)[1];
$readLen = socket_recv($_sock, $data, $packLen, 0);
}else{
//掩码
socket_recv($_sock, $masks, 4, 0);
$packLen = $len;
//真实数据
$readLen = socket_recv($_sock, $data, $packLen, 0);
}
echo 'len:'.$len.' readLen:'.$readLen.' packLen:'.$packLen."\n";
for(;;){
if($readLen<$packLen){
socket_set_block($_sock);//没有数据则阻塞
$cacheLen = $packLen-$readLen;
socket_recv($_sock, $cacheData, $cacheLen, 0);
echo 'cacheLen:'.$cacheLen."\n";
$data.=$cacheData;
$readLen = strlen($data);
}else{
break;
}
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
//在这里业务代码
echo "{$key} clinet msg:",$decoded,"\n";
$msg = date('Y-m-d H:i:s',time());
$this->send($_sock,$msg);
}
}
/**
* 发送数据
* @param $newClinet 新接入的socket
* @param $msg 要发送的数据
* @return int|string
*/
public function send($newClinet, $msg){
$msg = $this->frame($msg);
socket_write($newClinet, $msg, strlen($msg));
}
public function frame($s) {
$len = strlen($s);
if($len > 126 && $len <= 65535){
return chr(129) . chr(126) . pack('n',$len).$s;
}elseif($len > 65535){
return chr(129) . chr(127) . chr(0).chr(0) . chr(0) . chr(0) . pack('N',$len) . $s;
}else{
return chr(129) . chr(strlen($s)) . $s;
}
}
/**
* 关闭socket
*/
public function close(){
return socket_close($this->_sockets);
}
}
$sock = new SocketService();
$sock->run();
golang服务端代码:
package main
import (
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"log"
"net"
"runtime"
"strings"
)
func main() {
runtime.GOMAXPROCS(1)
ln, err := net.Listen("tcp", ":9999")
if err != nil {
log.Panic(err)
}
for {
conn, err := ln.Accept()
if err != nil {
log.Println("Accept err:", err)
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
content := make([]byte, 1024)
_, err := conn.Read(content)
if err != nil {
log.Println(err)
}
isHttp := false
if string(content[0:3]) == "GET" {
isHttp = true
}
if isHttp {
headers := parseHandshake(string(content))
secWebsocketKey := headers["Sec-WebSocket-Key"]
// NOTE:这里省略其他的验证
guid := "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// 计算Sec-WebSocket-Accept
h := sha1.New()
h.Write([]byte(secWebsocketKey + guid))
bs := h.Sum(nil)
accept := base64.StdEncoding.EncodeToString([]byte(bs))
response := "HTTP/1.1 101 Switching Protocols\r\n"
response = response + "Sec-WebSocket-Accept: " + string(accept) + "\r\n"
response = response + "Connection: Upgrade\r\n"
response = response + "Upgrade: websocket\r\n\r\n"
if lenth, err := conn.Write([]byte(response)); err != nil {
log.Println(err)
} else {
log.Println("send len:", lenth)
barr := make([]byte, 4)
barr = conn.RemoteAddr().(*net.TCPAddr).IP.To4()
log.Println("IP:", barr)
}
wssocket := NewWsSocket(conn)
for {
data, err := wssocket.ReadIframe()
if err != nil {
log.Println("readIframe err:", err)
return
}
log.Println("read data:", string(data))
err = wssocket.SendIframe([]byte("何梓凡"))
if err != nil {
log.Println("sendIframe err:", err)
return
}
}
} else {
log.Println(string(content))
// 直接读取
}
}
type WsSocket struct {
Conn net.Conn
}
func NewWsSocket(conn net.Conn) *WsSocket {
return &WsSocket{Conn: conn}
}
func (this *WsSocket) SendIframe(data []byte) error {
buf := make([]byte, 0, len(data)+14)
lenth := len(data)
buf = append(buf, byte(129))
if lenth < 126 {
buf = append(buf, byte(lenth))
} else if lenth < 0xFFFF {
buf = append(buf, 126|0, 0, 0)
binary.BigEndian.PutUint16(buf[len(buf)-2:], uint16(lenth))
} else {
buf = append(buf, 127|0, 0, 0, 0, 0, 0, 0, 0, 0)
binary.BigEndian.PutUint64(buf[len(buf)-8:], uint64(lenth))
}
buf = append(buf, data...)
this.Conn.Write(buf)
return nil
}
func (this *WsSocket) ReadIframe() (message []byte, err error) {
buf := make([]byte, 8, 8)
this.Conn.Read(buf[:2])
_, payload := buf[0], buf[1]
payloadlen := uint64(payload & 0x7f)
//解析frame长度
var frameLength uint64
if payloadlen < 126 {
frameLength = payloadlen
} else if payloadlen == 126 {
this.Conn.Read(buf[:2])
frameLength = uint64(binary.BigEndian.Uint16(buf[:2]))
} else { //payloadlen == 127
this.Conn.Read(buf[:8])
frameLength = binary.BigEndian.Uint64(buf[:8])
}
maskFrame := payload&0x80 != 0
frameMask := make([]byte, 4, 4)
if maskFrame {
this.Conn.Read(frameMask)
}
message = make([]byte, frameLength, frameLength)
if frameLength > 0 {
this.Conn.Read(message)
for i := range message {
message[i] ^= frameMask[i%4]
}
}
return message, nil
}
func parseHandshake(content string) map[string]string {
headers := make(map[string]string, 10)
lines := strings.Split(content, "\r\n")
for _, line := range lines {
if len(line) >= 0 {
words := strings.Split(line, ":")
if len(words) == 2 {
headers[strings.Trim(words[0], " ")] = strings.Trim(words[1], " ")
}
}
}
return headers
}

浙公网安备 33010602011771号