HTML5 socket
client:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<style>
p {
text-align: left;
padding-left: 20px;
}
</style>
</head>
<body>
<div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
<h1>websocket聊天室</h1>
<div style="width: 800px;border: 1px solid gray;height: 300px;">
<div style="width: 200px;height: 300px;float: left;text-align: left;">
<p><span>当前在线:</span><span id="user_num">0</span></p>
<div id="user_list" style="overflow: auto;"> </div>
</div>
<div id="msg_list" style="width: 598px;border: 1px solid gray; height: 300px;overflow: scroll;float: left;">
</div>
</div>
<br>
<textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
<input type="button" value="发送" onclick="send()">
</div>
</body>
</html> <script type="text/javascript">
// 存储用户名到全局变量,握手成功后发送给服务器
var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
var ws = new WebSocket("ws://127.0.0.1:8080");
ws.onopen = function () {
var data = "系统消息:建立连接成功";
listMsg(data);
}; /**
* 分析服务器返回信息
*
* msg.type : user 普通信息;system 系统信息;handshake 握手信息;login 登陆信息; logout 退出信息;
* msg.from : 消息来源
* msg.content: 消息内容
*/
ws.onmessage = function (e) {
var msg = JSON.parse(e.data);
var sender, user_name, name_list, change_type; switch (msg.type) {
case 'system':
sender = '系统消息: ';
break;
case 'user':
sender = msg.from + ': ';
break;
case 'handshake':
var user_info = {'type': 'login', 'content': uname};
sendMsg(user_info);
return;
case 'login':
case 'logout':
user_name = msg.content;
name_list = msg.user_list;
change_type = msg.type;
dealUser(user_name, change_type, name_list);
return;
} var data = sender + msg.content;
listMsg(data);
}; ws.onerror = function () {
var data = "系统消息 : 出错了,请退出重试.";
listMsg(data);
}; /**
* 在输入框内按下回车键时发送消息
*
* @param event
*
* @returns {boolean}
*/
function confirm(event) {
var key_num = event.keyCode;
if (13 == key_num) {
send();
} else {
return false;
}
} /**
* 发送并清空消息输入框内的消息
*/
function send() {
var msg_box = document.getElementById("msg_box");
var content = msg_box.value;
var reg = new RegExp("\r\n", "g");
content = content.replace(reg, "");
var msg = {'content': content.trim(), 'type': 'user'};
sendMsg(msg);
msg_box.value = '';
// todo 清除换行符
} /**
* 将消息内容添加到输出框中,并将滚动条滚动到最下方
*/
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p"); msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
} /**
* 处理用户登陆消息
*
* @param user_name 用户名
* @param type login/logout
* @param name_list 用户列表
*/
function dealUser(user_name, type, name_list) {
var user_list = document.getElementById("user_list");
var user_num = document.getElementById("user_num");
while(user_list.hasChildNodes()) {
user_list.removeChild(user_list.firstChild);
} for (var index in name_list) {
var user = document.createElement("p");
user.innerHTML = name_list[index];
user_list.appendChild(user);
}
user_num.innerHTML = name_list.length;
user_list.scrollTop = user_list.scrollHeight; var change = type == 'login' ? '上线' : '下线'; var data = '系统消息: ' + user_name + ' 已' + change;
listMsg(data);
} /**
* 将数据转为json并发送
* @param msg
*/
function sendMsg(msg) {
var data = JSON.stringify(msg);
ws.send(data);
} /**
* 生产一个全局唯一ID作为用户名的默认值;
*
* @param len
* @param radix
* @returns {string}
*/
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length; if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
var r; uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4'; for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
} return uuid.join('');
}
</script>
server:
error_reporting(E_ALL);
set_time_limit(0);// 设置超时时间为无限,防止超时
date_default_timezone_set('Asia/shanghai'); class WebSocket {
const LOG_PATH = '/tmp/';
const LISTEN_SOCKET_NUM = 9; /**
* @var array $sockets
* [
* (int)$socket => [
* info
* ]
* ]
* todo 解释socket与file号对应
*/
private $sockets = [];
private $master; public function __construct($host, $port) {
try {
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 设置IP和端口重用,在重启服务器后能重新使用此端口;
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1);
// 将IP和端口绑定在服务器socket上;
socket_bind($this->master, $host, $port);
// listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
socket_listen($this->master, self::LISTEN_SOCKET_NUM);
} catch (\Exception $e) {
$err_code = socket_last_error();
$err_msg = socket_strerror($err_code); $this->error([
'error_init_server',
$err_code,
$err_msg
]);
} $this->sockets[0] = ['resource' => $this->master];
//$pid = posix_getpid(); linux
$pid = get_current_user(); //windows $this->debug(["server: {$this->master} started,pid: {$pid}"]); while (true) {
try {
$this->doServer();
} catch (\Exception $e) {
$this->error([
'error_do_server',
$e->getCode(),
$e->getMessage()
]);
}
}
} private function doServer() {
$write = $except = NULL;
$sockets = array_column($this->sockets, 'resource');
$read_num = socket_select($sockets, $write, $except, NULL);
// select作为监视函数,参数分别是(监视可读,可写,异常,超时时间),返回可操作数目,出错时返回false;
if (false === $read_num) {
$this->error([
'error_select',
$err_code = socket_last_error(),
socket_strerror($err_code)
]);
return;
} foreach ($sockets as $socket) {
// 如果可读的是服务器socket,则处理连接逻辑
if ($socket == $this->master) {
$client = socket_accept($this->master);
// 创建,绑定,监听后accept函数将会接受socket要来的连接,一旦有一个连接成功,将会返回一个新的socket资源用以交互,如果是一个多个连接的队列,只会处理第一个,如果没有连接的话,进程将会被阻塞,直到连接上.如果用set_socket_blocking或socket_set_noblock()设置了阻塞,会返回false;返回资源后,将会持续等待连接。
if (false === $client) {
$this->error([
'err_accept',
$err_code = socket_last_error(),
socket_strerror($err_code)
]);
continue;
} else {
self::connect($client);
continue;
}
} else {
// 如果可读的是其他已连接socket,则读取其数据,并处理应答逻辑
$bytes = @socket_recv($socket, $buffer, 2048, 0);
if ($bytes < 9) {
$recv_msg = $this->disconnect($socket);
} else {
if (!$this->sockets[(int)$socket]['handshake']) {
self::handShake($socket, $buffer);
continue;
} else {
$recv_msg = self::parse($buffer);
}
}
array_unshift($recv_msg, 'receive_msg');
$msg = self::dealMsg($socket, $recv_msg); $this->broadcast($msg);
}
}
} /**
* 将socket添加到已连接列表,但握手状态留空;
*
* @param $socket
*/
public function connect($socket) {
socket_getpeername($socket, $ip, $port);
$socket_info = [
'resource' => $socket,
'uname' => '',
'handshake' => false,
'ip' => $ip,
'port' => $port,
];
$this->sockets[(int)$socket] = $socket_info;
$this->debug(array_merge(['socket_connect'], $socket_info));
} /**
* 客户端关闭连接
*
* @param $socket
*
* @return array
*/
private function disconnect($socket) {
$recv_msg = [
'type' => 'logout',
'content' => $this->sockets[(int)$socket]['uname'],
];
unset($this->sockets[(int)$socket]); return $recv_msg;
} /**
* 用公共握手算法握手
*
* @param $socket
* @param $buffer
*
* @return bool
*/
public function handShake($socket, $buffer) {
// 获取到客户端的升级密匙
$line_with_key = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
$key = trim(substr($line_with_key, 0, strpos($line_with_key, "\r\n"))); // 生成升级密匙,并拼接websocket升级头
$upgrade_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));// 升级key的算法
$upgrade_message = "HTTP/1.1 101 Switching Protocols\r\n";
$upgrade_message .= "Upgrade: websocket\r\n";
$upgrade_message .= "Sec-WebSocket-Version: 13\r\n";
$upgrade_message .= "Connection: Upgrade\r\n";
$upgrade_message .= "Sec-WebSocket-Accept:" . $upgrade_key . "\r\n\r\n"; socket_write($socket, $upgrade_message, strlen($upgrade_message));// 向socket里写入升级信息
$this->sockets[(int)$socket]['handshake'] = true; socket_getpeername($socket, $ip, $port);
$this->debug([
'hand_shake',
$socket,
$ip,
$port
]); // 向客户端发送握手成功消息,以触发客户端发送用户名动作;
$msg = [
'type' => 'handshake',
'content' => 'done',
];
$msg = $this->build(json_encode($msg));
socket_write($socket, $msg, strlen($msg));
return true;
} /**
* 解析数据
*
* @param $buffer
*
* @return bool|string
*/
private function parse($buffer) {
$decoded = '';
$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 json_decode($decoded, true);
} /**
* 将普通信息组装成websocket数据帧
*
* @param $msg
*
* @return string
*/
private function build($msg) {
$frame = [];
$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;
} $data = '';
$l = strlen($msg);
for ($i = 0; $i < $l; $i++) {
$data .= dechex(ord($msg{$i}));
}
$frame[2] = $data; $data = implode('', $frame); return pack("H*", $data);
} /**
* 拼装信息
*
* @param $socket
* @param $recv_msg
* [
* 'type'=>user/login
* 'content'=>content
* ]
*
* @return string
*/
private function dealMsg($socket, $recv_msg) {
$msg_type = $recv_msg['type'];
$msg_content = $recv_msg['content'];
$response = []; switch ($msg_type) {
case 'login':
$this->sockets[(int)$socket]['uname'] = $msg_content;
// 取得最新的名字记录
$user_list = array_column($this->sockets, 'uname');
$response['type'] = 'login';
$response['content'] = $msg_content;
$response['user_list'] = $user_list;
break;
case 'logout':
$user_list = array_column($this->sockets, 'uname');
$response['type'] = 'logout';
$response['content'] = $msg_content;
$response['user_list'] = $user_list;
break;
case 'user':
$uname = $this->sockets[(int)$socket]['uname'];
$response['type'] = 'user';
$response['from'] = $uname;
$response['content'] = $msg_content;
break;
} return $this->build(json_encode($response));
} /**
* 广播消息
*
* @param $data
*/
private function broadcast($data) {
foreach ($this->sockets as $socket) {
if ($socket['resource'] == $this->master) {
continue;
}
socket_write($socket['resource'], $data, strlen($data));
}
} /**
* 记录debug信息
*
* @param array $info
*/
private function debug(array $info) {
$time = date('Y-m-d H:i:s');
array_unshift($info, $time); $info = array_map('json_encode', $info);
file_put_contents(self::LOG_PATH . 'websocket_debug.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
} /**
* 记录错误信息
*
* @param array $info
*/
private function error(array $info) {
$time = date('Y-m-d H:i:s');
array_unshift($info, $time); $info = array_map('json_encode', $info);
file_put_contents(self::LOG_PATH . 'websocket_error.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
}
} $ws = new WebSocket("127.0.0.1", "8080");
参考博主:[网页实时聊天之PHP实现websocket](http://www.cnblogs.com/zhenbianshu/p/6111257.html)
HTML5 socket的更多相关文章
- HTML5 Socket通信
HTML5 Socket通信使用起来也是相当不从的,先将部分JS代码与大家分享: var socket; function connect() { var host = "ws://&quo ...
- 利用HTML5+Socket.io实现摇一摇控制PC端歌曲切换
我比较喜欢听音乐,特别是周末的时候,电脑开着百度随心听fm,随机播放歌曲,躺在床上享受.但碰到了一个烦人的事情,想切掉不喜欢的曲子,还得起床去操作电脑换歌.于是思考能不能用手机控制电脑切换歌曲,经过一 ...
- HTML5 + SOCKET视频传输
<html> <head> <meta http-equiv="content-type" content="text/html; char ...
- js技术javascript 该重视
当下,js大有挤压php java asp.net之类后端语言的趋势,直接js html5 socket与后端python c c++ 等通信 更不用提二维 三维计算展示 方面 医院呼叫 报警 处理 ...
- Demo源码放送:打通B/S与C/S !让HTML5 WebSocket与.NET Socket公用同一个服务端!
随着HTML5 WebSocket技术的日益成熟与普及,我们可以借助WebSocket来更加方便地打通BS与CS -- 因为B/S中的WebSocket可以直接连接到C/S的服务端,并进行双向通信.如 ...
- 打通B/S与C/S !让HTML5 WebSocket与.NET Socket公用同一个服务端!
随着HTML5 WebSocket技术的日益成熟与普及,我们可以借助WebSocket来更加方便地打通BS与CS -- 因为B/S中的WebSocket可以直接连接到C/S的服务端,并进行双向通信.如 ...
- HTML5 Web socket和socket.io
what is websockets Two-way communication over ont TCP socket, a type of PUSH technology HTML5的新特性,用于 ...
- 【我的前端自学之路】【HTML5】Web Socket
以下为自学笔记内容,仅供参考. 转发请保留原文链接:https://www.cnblogs.com/it-dennis/p/10508118.html 什么是Web Socket WebSocket ...
- 基于node.js+socket.io+html5实现的斗地主游戏(1)概述
一.游戏描述 说是斗地主游戏,其实是寝室自创的"捉双A",跟很多地方的捉红10.打红A差不多,大概规则是: 1.基础牌型和斗地主一样,但没有大小王,共52张牌,每人13张,这也是为 ...
随机推荐
- 《手把手教你》系列基础篇之(一)-java+ selenium自动化测试-环境搭建(上)(详细教程)
1.简介 jmeter系列的文章结束,本来想趁热打铁顺别将Jmeter和接口测试介绍一下,但是感觉Jmeter时间太长了怕大家吃腻了,还有一个原因就是许多小伙伴们或者童鞋们私信问宏哥什么时候可以有ja ...
- 【单调栈】【前缀和】【二分查找】8.28题解-long
long 题目描述 AP神牛准备给自己盖一座很华丽的宫殿.于是,他看中了一块N*M的矩形空地.空地中每个格子都有自己的海拔高度.AP想让他的宫殿的平均海拔在海平面之上(假设海平面的高度是0,平均数都会 ...
- NOIP模拟测试13「矩阵游戏·跳房子·优美序列」
矩阵游戏 考试时思路一度和正解一样,考试到最后还是打了80分思路,结果80分打炸了只得了40分暴力分 题解 算出来第一列的总值,每次通过加每两列之间的差值得出下一列的总值 算第一列我们只需要让当前点* ...
- 2021年Wordpress博客装修美化(二)
使用古腾堡来排版页面 废话不多说,我们直接开始吧,今天主要来聊聊如何使用Wordpress自带的可视化页面编辑器.我相信只有驾驭最基础的可视化排版,后面才能对高级版本的扩展可视化编辑器elemento ...
- StringUtils中的常量
//空格字符串 public static final String SPACE = " "; //空字符串 public static final String EMPTY = ...
- 在微信小程序中使用阿里图标库Iconfont
首先想要使用图标,只用上图的五个iconfont相关文件就可以了.(下下来的文件iconfont.wxss开始是.css的后缀,手动改成.wxss就可以在小程序中使用) 然后在app.wxss中引入i ...
- STP协议与MSTP协议
STP协议与MSTP协议 目录 一.STP概述 1.1.交换网络环路的产生 1.2.STP简介 二.生成树算法 2.1.生成树算法的步骤 2.2.网桥 ID 2.3.选择根端口 2.4.选择指定端口 ...
- 试着给VuePress添加渐进式Web应用(PWA)支持,基于vuepress/plugin-pwa,点亮离线访问
背景 有时候,我们也希望VuePress构建的文档中心能支持离线访问,这时候我们需要给他添加渐进式Web应用(PWA,Progressive Web App)的支持,根据官方文档指引,我们可以借助插件 ...
- Git&Gitlab开发流程与运维管理
Git&Gitlab开发流程与运维管理 作者 刘畅 时间 2020-10-31 实验系统版本centos7.5 主机名称 ip地址 配置 安装软件 controlnode 172.16.1.1 ...
- WebContent的子目录里面的jsp文件无法将数据传递给Servlet
在WebContent下创建子目录FormCheck,register.jsp将跳转到RegisterServlet这个Servlet中去 分两种情况:在web.xml里面配置 和 使用注解 1.在w ...