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
}

posted @ 2021-06-24 15:01  吹_神  阅读(750)  评论(0)    收藏  举报