websocket协议初探

1 什么是websocket协议?

WebSocket protocol

是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。
相对于以前的http协议,websocket协议能够实现浏览器与服务器全双工通信(full-duplex),而http仅能实现单向通信。

2 浏览器如何与服务器建立socket连接?

  1. 主要连接流程如下

image

2 client与server建立socket时握手的会话内容,即request与response

a、client建立WebSocket时向服务器端请求的信息

  GET /chat HTTP/1.1 
  Host: server.example.com 
  Upgrade: websocket //告诉服务器现在发送的是WebSocket协议
  Connection: Upgrade 
  Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一个Base64 encode的值,这个是浏览器随机生成的,用于验证服务器端返回数据是否是WebSocket助理
  Sec-WebSocket-Protocol: chat, superchat 
  Sec-WebSocket-Version: 13 
  Origin: http://example.com
  b、服务器获取到client请求的信息后,根据WebSocket协议对数据进行处理并返回,其中要对Sec-WebSocket-Key进行加密等操作

  HTTP/1.1 101 Switching Protocols 
  Upgrade: websocket //依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket
  Connection: Upgrade 
  Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key,也就是client要求建立WebSocket验证的凭证

websocket数据传输格式(官方文档

5.2.  Base Framing Protocol

   This wire format for the data transfer part is described by the ABNF
   [RFC5234] given in detail in this section.  (Note that, unlike in
   other sections of this document, the ABNF in this section is
   operating on groups of bits.  The length of each group of bits is
   indicated in a comment.  When encoded on the wire, the most
   significant bit is the leftmost in the ABNF).  A high-level overview
   of the framing is given in the following figure.  In a case of
   conflict between the figure below and the ABNF specified later in
   this section, the figure is authoritative.

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

3 PHP建立socket过程详解

服务器端
<?php

error_reporting(E_ALL ^ E_NOTICE);

//ob_implicit_flush();

//SOCKET编程实例
$host="127.0.0.1";
$port=8080;
//创建socket

# resource socket_create ( int $domain , int $type , int $protocol )
/*
 * 可用的地址/协议 Domain 描述
   AF_INET IPv4 网络协议。TCP 和 UDP 都可使用此协议。
   AF_INET6 IPv6 网络协议。TCP 和 UDP 都可使用此协议。
   AF_UNIX 本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。

  * SOCK_STREAM 提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP 协议即基于这种流式套接字。
    SOCK_DGRAM 提供数据报文的支持。(无连接,不可靠、固定最大长度).UDP协议即基于这种数据报文套接字。
    SOCK_SEQPACKET 提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信;数据端通过接收每一个数据段来读取整个数据包。
    SOCK_RAW 提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现 ICMP 请求(例如 ping)。
    SOCK_RDM 提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。

  * protocol 参数,是设置指定 domain 套接字下的具体协议。这个值可以使用 getprotobyname() 函数进行读取。如果所需的协议是 TCP 或 UDP,可以直接使用常量 SOL_TCP 和 SOL_UDP 。

    常见协议 名称 描述
    icmp Internet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如"ping"命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。
    udp User Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。
    tcp Transmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。
 */
$socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP) or die('cannot create socket!!!');

socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的数据包
//绑定socket到端口主机
//创建的socket资源绑定到IP地址和端口号
$res=socket_bind($socket,$host,$port) or die('cannot bind to socket!!!');

//启动socket监听
//3为最大连接数
//等待客户端的连接
$res = socket_listen($socket, 3) or die('cannot set up socket listener!!!');

//接受连接
//返回连接$spawn
//这个函数会接受所建的socket传入的连接请求。在接受来自客户端socket的连接后,该函数返回另一个socket资源,实际上就是负责与相应的客户端socket通信。这里的“$spawn”就是负责与客户端socket通信的socket资源。
$spawn = socket_accept($socket) or die('cannot accept incoming connection!!!');

//得到第一次浏览器发出的http请求头
$input = socket_recv($spawn, $buffer,2048,0) or die('cannot read input!!!');

#*********************************************************
# 获得Sec-WebSocket-Key并加密后返回给浏览器Sec-WebSocket-Accept;
$buf  = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);

$key  = trim(substr($buf,0,strpos($buf,"\r\n")));

$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));

//按照协议组合信息进行返回
$new_message = "HTTP/1.1 101 Switching Protocols\r\n";

$new_message .= "Upgrade: websocket\r\n";

$new_message .= "Sec-WebSocket-Version: 13\r\n";

$new_message .= "Connection: Upgrade\r\n";

$new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";

socket_write($spawn,$new_message,strlen($new_message));//与浏览器进行第二次握手
#*************************************
//循环监听浏览器是否发送数据并进行处理
while(1){
    $len=0;
    $buffer='';
    //读取该socket的信息,注意:第二个参数是引用传参即接收数据,第三个参数是接收数据的长度
    do{
        @$l=socket_recv($spawn,$buf,1024,0);
        $len+=$l;
        $buffer.=$buf;
//                        var_dump($buffer);
    }while($l==1000);

    $re=decode($buf);

    echo iconv('UTF-8','GBK',$re);

    $send=code($re);

    echo $send;
//
    socket_write($spawn,$send,strlen($send));

    sleep(1);
}

//解码函数  将浏览器传输过来的数据帧进行解密
function decode($buffer)  {
    $len = $masks = $data = $decoded = null;
    $len = ord($buffer[1]) & 127;

    if ($len === 126)  {
        $masks = substr($buffer, 4, 4);
        $data = substr($buffer, 8);
    } else if ($len === 127)  {
        $masks = substr($buffer, 10, 4);
        $data = substr($buffer, 14);
    } else  {
        $masks = substr($buffer, 2, 4);
        $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
        $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return $decoded;
}

//加密  将要返回给浏览器的数据加密成数据帧
function code($msg){
    $frame = array();
    $frame[0] = '81';
    $len = strlen($msg);
    if($len < 126){
        $frame[1] = $len<16?'0'.dechex($len):dechex($len);
    }else if($len < 65025){
        $s=dechex($len);
        $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
    }else{
        $s=dechex($len);
        $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
    }
    $frame[2] = ord_hex($msg);
    $data = implode('',$frame);
    return pack("H*", $data);
}

function ord_hex($data)  {
    $msg = '';
    $l = strlen($data);
    for ($i= 0; $i<$l; $i++) {
        $msg .= dechex(ord($data{$i}));
    }
    return $msg;
}
客户端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript">
        var wsServer = 'ws://127.0.0.1:8080';
        var ws = new WebSocket(wsServer);
        //握手监听函数
        ws.onopen=function(){
            //状态为1证明握手成功,然后把client自定义的名字发送过去
            if(ws.readyState==1){
                //握手成功后对服务器发送信息
//                ws.send('type=add&ming');
            }
        }
        //错误返回信息函数
        ws.onerror = function(){
            console.log("error");
        };
        //监听服务器端推送的消息
        ws.onmessage = function (msg){
            console.log(msg.data);
        }

        function sendm() {
            var msg=document.getElementById('msg').value;
            ws.send(msg);
        }
        //断开WebSocket连接
        ws.onclose = function(){
            ws = false;
        }
    </script>
</head>
<body>
     <input type="text"  id="msg">
     <button onclick="sendm()">发送</button>
</body>
</html>

4 上线到服务器

  1. 防火墙设置访问端口
  2. 将socket.php文件监听ip设为0.0.0.0
  3. 命令行执行socket.php
  4. 客户端访问建立连接

5 测试

建立连接

image

发送消息到服务器接受 服务器原样返回给浏览器

image

控制台分析

image

6 参考

http://www.codeceo.com/article/php-socket-programming.html

https://developer.mozilla.org/zh-CN/docs/WebSockets/Writing_WebSocket_servers

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员若风+

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值