强调一下:websocket 是一个应用层协议,协议标识符为ws 即websocket的缩写,wss 为 websocket security的缩写
websocket步骤
来自客户端的握手,在服务端看来,就是客户端发过来一段HTTP报头
GET / HTTP/1.1
Host: 101.200.142.148:8880
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
这时,服务端需要响应如下信息给客户端,这样握手就成功了。
#一个demo
客户端:
<!--
* @Author: your name
* @Date: 2020-09-08 16:06:27
* @LastEditTime: 2020-09-08 16:34:47
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /undefined/Users/mac/Desktop/index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<p>这是一个websocket的demo</p>
</body>
<script>
socket = new WebSocket("ws://101.200.142.148:8880");
console.log(socket);
//握手成功后执行
socket.onopen = function(){
console.log("连接成功");
//向服务器发送数据
socket.send("hello server");
}
//当服务端有消息发来时执行
socket.onmessage = function(e){
console.log(e);
console.log(e.data);
}
</script>
</html>
服务端:
<?php
/*
* @Author: your name
* @Date: 2020-09-08 16:19:05
* @LastEditTime: 2020-09-08 16:29:20
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /undefined/Users/mac/Desktop/websocket_demo/server.php
*/
$socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,true);
socket_bind($socket,0,8880);
socket_listen($socket);
while(true){
$connSock = socket_accept($socket);
$request = socket_read($connSock,1024);
$response = handShake($request);
echo $response;
//响应客户端的握手请求
socket_write($connSock,$response,strlen($response));
//接收客户端传来的数据
$res = socket_read($connSock,1024);
//解码数据帧
echo decode($res);
//向客户端发送数据
$str = "hello Client";
//编码数据帧
$str = frame($str);
socket_write($connSock,$str,strlen($str));
}
function handShake($request){
preg_match("/Sec-WebSocket-Key:(.*)\r\n/",$request,$match);
$key = trim($match[1]);
var_dump($match);
$new_key =base64_encode(sha1($key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response.= "Upgrade: websocket\r\n";
$response.= "Connection: Upgrade\r\n";
$response.= "Sec-WebSocket-Accept: $new_key\r\n";
$response.= "Sec-WebSocket-Protocol: chat\r\n\r\n";
return $response;
}
//解析客户端发来的数据帧
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 frame($s) {
$a = str_split($s, 125);
if (count($a) == 1) {
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o) {
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
#Web群聊聊天室DEMO
大体思路:
使用IO多路复用 socket_select
如果当前活跃的是 套接字 就建立连接 ,并初始化用户列表
如果当前活跃的是 连接 ,就ws握手,如果已经握手,就解码客户端
传来的数据帧,然后调用send方法,将数据帧推送给在线的每个用户。
Ws.php
<?php
/*
* @Author: your name
* @Date: 2020-09-08 16:19:05
* @LastEditTime: 2020-09-09 10:58:37
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /undefined/Users/mac/Desktop/websocket_demo/server.php
*/
class Ws
{
public $socket = null;
public $sockets = [];
public $write = null;
public $except = null;
public $users = [];
public function __construct($allow_ip, $port)
{
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($this->socket, $allow_ip, $port);
socket_listen($this->socket);
$this->sockets[] = $this->socket;
while (true) {
$tmp_sockets = $this->sockets;
socket_select($tmp_sockets, $this->write, $this->except, null);
foreach ($tmp_sockets as $sock) {
if ($sock == $this->socket) {
$connSock = socket_accept($this->socket);
$this->sockets[] = $connSock;
$this->users[] = [
'socket' => $connSock,
'hand_shake' => false,
];
} else {
$index = $this->getUserIndex($sock);
$request = socket_read($sock, 1024);
if(strlen($request) == 8){
$this->logout($index);
}else{
if ($this->users[$index]['hand_shake'] == false) {
$response = $this->handShake($request);
//响应客户端的握手请求
socket_write($sock, $response, strlen($response));
$this->users[$index]['hand_shake'] = true;
} else {
//解码数据帧
$msg = $this->decode($request);
$this->send($msg, $index);
}
}
}
}
}
}
private function logout($user_index){
socket_close($this->users[$user_index]['socket']);
unset($this->users[$user_index]);
$this->sockets = null;
$this->sockets[] = $this->socket;
foreach($this->users as $user){
$this->sockets[] = $user['socket'];
}
}
public function send($msg, $index)
{
$arr = explode('===', $msg);
switch ($arr[0]) {
case 'login':
$this->users[$index]['name'] = $arr[1];
$res['msg'] = $arr[1] . ':登录成功';
$res['type'] = 'login';
//获取所有用户名
$names['userlist'] = $this->getUserNames();
$names['type'] = 'user';
$names = $this->frame(json_encode($names));
//向每个用户推送
foreach ($this->users as $user) {
socket_write($user['socket'], $names, strlen($names));
}
break;
case 'content':
$res['content'] = $arr[1];
$res['name'] = $this->users[$index]['name'];
$res['time'] = date('Y-m-d H:i:s',time());
$res['type'] = 'content';
break;
}
$res = $this->frame(json_encode($res));
//向每个用户推送
foreach ($this->users as $user) {
socket_write($user['socket'], $res, strlen($res));
}
}
private function getUserNames()
{
foreach ($this->users as $user) {
$names[] = $user['name'];
}
return $names;
}
public function getUserIndex($sock)
{
foreach ($this->users as $key => $user) {
if ($user['socket'] == $sock) {
return $key;
}
}
}
public function handShake($request)
{
preg_match("/Sec-WebSocket-Key:(.*)\r\n/", $request, $match);
$key = trim($match[1]);
var_dump($match);
$new_key = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response .= "Upgrade: websocket\r\n";
$response .= "Connection: Upgrade\r\n";
$response .= "Sec-WebSocket-Accept: $new_key\r\n";
$response .= "Sec-WebSocket-Protocol: chat\r\n\r\n";
return $response;
}
//解析客户端发来的数据帧
public 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;
}
//编码数据帧
public function frame($s)
{
$a = str_split($s, 125);
if (count($a) == 1) {
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o) {
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
}
server.php
<?php
/*
* @Author: your name
* @Date: 2020-09-08 16:59:14
* @LastEditTime: 2020-09-08 17:00:00
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /websocket_demo/server.php
*/
require_once 'Ws.php';
$allow_ip = 0;
$port = 8880;
$ws = new Ws($allow_ip,$port);
index.html
<!--
* @Author: your name
* @Date: 2020-09-08 16:06:27
* @LastEditTime: 2020-09-09 10:26:36
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /undefined/Users/mac/Desktop/index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>西电221聊骚室</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<style>
#content {
height: 100px;
resize: none;
}
#msg-box {
height: 450px;
}
#user-box {
height: 450px;
}
.title {
padding: 20px;
font-size: 20px;
}
</style>
</head>
<body>
<div class='container'>
<div class='text-center title'><strong>西电221聊骚室</strong></div>
<div class='row'>
<div class='col-sm-8 '>
<div class='panel panel-default'>
<div class="panel-heading">消息框</div>
<div class='panel-body' id='msg-box'></div>
</div>
</div>
<div class='col-sm-4'>
<div class='panel panel-default'>
<div class='panel-heading'>用户列表</div>
<div class='panel-body' id='user-box'></div>
</div>
</div>
</div>
<div class='form-group'>
<textarea class='form-control' name="" id="content" cols="30" rows="5"></textarea>
</div>
<button id='send' class='btn btn-primary'>发送消息</button>
</div>
</body>
<script>
var name = prompt("欢迎进入西电暗网,请输入用户名:")
socket = new WebSocket("ws://101.200.142.148:8880");
console.log(socket);
//握手成功后执行
socket.onopen = function () {
console.log("连接成功");
//向服务器发送数据
socket.send("login===" + name);
}
//当服务端有消息发来时执行
socket.onmessage = function (e) {
var data = JSON.parse(e.data);
console.log(e);
console.log(e.data);
let msg_box = document.getElementById('msg-box');
switch (data.type) {
case "login":
msg_box.innerHTML += '<div>' + (new Date()).getTime() + ' ' + data.msg + '</div>';
break;
case "user":
let user_box = document.getElementById('user-box');
let str = '';
for (let i = 0; i < data.userlist.length; i++) {
str += '<div>' + data.userlist[i] + '</div>';
}
user_box.innerHTML = str;
break;
case "content":
msg_box.innerHTML += '<div>' + data.time + ' ' + data.name + ':' + data.content + '</div>';
}
}
let btn = document.getElementById('send');
function send(){
let content = document.getElementById('content');
let data = content.value;
if(data=='') return;
socket.send("content===" + data);
content.value='';
}
btn.onclick = ()=>{
send();
};
document.onkeydown = (e) => {
switch (e.keyCode) {
case 13:
send();
break;
}
}
</script>
</html>