目前官方已经开放了
ThinkPHP5.1版本的swoole:
https://github.com/top-think/think-swoole

安装

服务器必须开启swoole扩展,如使用宝塔,则在对应的php版本—设置—安装扩展—选择swoole4安装。

安装完之后用命令执行php -m,看到swoole即代表安装完成。

在swoole扩展安装后,tp5的项目根目录下执行composer命令安装think-swoole:

1
composer require topthink/think-swoole

注意:对应的端口要放行,这里使用的端口是9501

服务端启动 swoole

1
php think swoole:server

相关数据表

如果有登录注册,无则忽略

用户表(简约版)

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `dy_user` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(64) NOT NULL COMMENT '密码',
`create_time` int(10) DEFAULT NULL COMMENT '创建时间',
`login_time` int(10) DEFAULT NULL COMMENT '上次登录时间',
`ip` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '注册ip',
`pass_time` int(11) DEFAULT NULL COMMENT '密码错误时间',
`pass_error` tinyint(1) unsigned DEFAULT '0' COMMENT '密码错误次数',
PRIMARY KEY (`uid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

token表

1
2
3
4
5
6
7
8
CREATE TABLE `dy_user_token` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL DEFAULT '0' COMMENT '用户uid',
`access_token` varchar(250) NOT NULL DEFAULT '' COMMENT '随机字符串',
`create_time` int(11) NOT NULL DEFAULT '0' COMMENT '添加时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `uid` (`uid`) USING BTREE COMMENT '用户ID'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='保存token随机字符串';

相关代码

服务端

在控制器下新建一个控制器继承Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
namespace app\index\controller;

// 必须 use 并继承 \think\swoole\Server 类
use think\swoole\Server;
use think\Db;
use think\facade\Cache;

class Swoole extends Server
{
protected static $token;
protected $host = '0.0.0.0';// 监听所有地址
protected $port = 9501;// 监听 9501 端口
protected $serverType = 'socket';
protected $mode = SWOOLE_PROCESS;// 指定运行模式为多进程
protected $sockType = SWOOLE_SOCK_TCP;// 指定 socket 的类型为 ipv4 的 tcp socket
protected static $uid = '';
protected $option = [
'worker_num' => 4, // 设置启动的Worker进程数
'daemonize' => false, //守护进程化。
'backlog' => 128, //Listen队列长度,
'dispatch_mode' => 2,
// 'heartbeat_check_interval' => 5, // 延时
// 'heartbeat_idle_time' => 100, // 心跳
];

//收到信息时回调函数
// public function onRequest($request, $response)
// {
// var_dump('qweqw');
// }

//建立连接时回调函数
public function onOpen($server,$req)
{
$fd = $req->fd;//客户端标识
$token = $req->get['token'];//客户端传递的用户登录token

$token_info = Db::name("user_token")->alias("a")
->join('user b',"b.uid=a.uid")
->where(['a.access_token'=>$token])
->field('a.create_time,b.uid,b.username')->find();

$uid = $token_info['uid'];
$username = $token_info['username'];

$end_time = $token_info['create_time'] + 60*60*24;//一天
if (time() > $end_time) {
$arr = array('status'=>2,'message'=>'登录已失效,请重新登录');
$server->push($fd, json_encode($arr));
$server->close($fd);
return;
}
//给用户绑定uid
//清除解绑
Cache::rm('_fd_'.$fd);

$data = ['fd'=>$fd,'uid'=>$uid,'username'=>$username,'token'=>$token];
//fb绑定uid
Cache::set('_fd_'.$fd, $data);

echo "用户{$uid}建立了连接,标识为{$fd}\n";
}

//接收数据时回调函数
public function onMessage($server,$frame)
{
$fd = $frame->fd;
$message = $frame->data;

//通过fd查询用户uid
$fd_data = Cache::get('_fd_'.$fd);
$data['token'] = $fd_data['token'];
$data['message'] = $fd_data['username'].'发送了:'.$message;
$data['post_time'] = date("m/d H:i",time());
$arr = array('status'=>1,'message'=>'success','data'=>$data);

//仅推送给当前连接用户
//$server->push($fd, json_encode($arr));

//推送给全部连接用户
foreach($server->connections as $fd) {
$server->push($fd, json_encode($arr));
}

}

//连接关闭时回调函数
function onClose(\swoole_server $server, int $fd, int $reactorId)
// public function onClose($server,$fd)
{
$fd_data = Cache::get('_fd_'.$fd);
Cache::rm('_fd_'.$fd);//清除fd绑定标识
echo "标识{$fd}关闭了连接\n";
}
}

客户端

这里使用的是网页

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>Chat</title>
<link rel="stylesheet" type="text/css" href="/public/static/index/css/bootstrap-grid.min.css" />
<link rel="stylesheet" type="text/css" href="/public/static/index/css/chat.css" />
<script type="text/javascript" src="/public/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/public/static/plugins/layer/layer.js"></script>
<script src="/public/static/index/js/flexible.js"></script>
</head>
<body>
<header class="header">
<!-- <a class="back" href="javascript:history.back()"></a> -->
<h5 class="tit">实时聊天</h5>
<!-- <div class="right">退出</div> -->
</header>

<!-- 聊天内容 start-->
<div class="message"></div>
<!-- 聊天内容 end-->

<!-- 底部 start-->
<div class="footer">
<img src="/public/static/index/images/hua.png" alt="暂无此功能" />
<img src="/public/static/index/images/xiaolian.png" alt="暂无此功能" />
<input type="text" maxlength="300" id="msg" />
<p id="sendBtn">发送</p>
</div>
<!-- 底部 end-->
</body>
</html>
<script type="text/javascript">
$(function () {
var token = "{$token}";//用户token,需要服务端新建一个控制器返回该页面token值

//判断浏览器是否支持WebSocket
var supportsWebSockets = 'WebSocket' in window || 'MozWebSocket' in window;
if (supportsWebSockets) {
//建立WebSocket连接(ip地址换成自己主机ip)
var ws = new WebSocket("ws://106.55.11.78:9501?token="+token);
ws.onopen = function () {
layer.msg('服务器连接成功',{shade:0.1,icon:1,time:600});
};
ws.onerror = function () {
layer.msg('服务器连接失败',{shade:0.1,icon:2,time:600});
};
ws.onmessage = function (evt) {
var data = $.parseJSON(evt.data);
//错误提示
if(data.status != 1){
layer.alert(data.message,{icon:2});
return;
}
//消息返回
if (data.status==1 && data.data.message!='') {
var html = "";
if (data.data.token == token) {
html += "<div class=\"show\"><div class=\"time\">"+data.data.post_time+"</div><div class=\"msg\"><img src=\""+data.data.head_img+"\" alt=\"\" /><p><i clas=\"msg_input\"></i>"+data.data.message+"</p></div></div>";
}else{
html += "<div class=\"send\"><div class=\"time\">"+data.data.post_time+"</div><div class=\"msg\"><img src=\""+data.data.head_img+"\" alt=\"\" /><p><i clas=\"msg_input\"></i>"+data.data.message+"</p></div></div>";
}
}
$(".message").append(html);
setTimeout(function () {
($('.message').children("div:last-child")[0]).scrollIntoView();//向上滚动
},100);
};
ws.onclose = function (res) {

};
//按钮发送
$("#sendBtn").click(function () {
var contents = $("#msg").val().trim();
if(contents == null || contents == ""){
layer.msg('内容为空',{shade:0.1,icon:2,time:600});
return false;
}else{
ws.send(contents);
$("#msg").val("");
}
});
//回车发送
$("#msg").keydown(function (evel) {
var that = $(this);
if (evel.keyCode == 13) {
evel.cancelBubble = true;
evel.preventDefault();
evel.stopPropagation();
var contents = that.val().trim();
if(contents == null || contents == ""){
layer.msg('内容为空',{shade:0.1,icon:2,time:600});
return false;
}else{
ws.send(contents);
that.val("");
}
}
});
}else{
layer.alert("您的浏览器不支持 WebSocket!");
}
});
</script>
<script type="text/javascript" charset="utf-8">
//发送按钮变色
$(function(){
$('.footer').on('keyup','input',function(){
if($(this).val().length>0){
$(this).next().css('background','#114F8E').prop('disabled',true);

}else{
$(this).next().css('background','#ddd').prop('disabled',false);
}
})
/*$('.footer p').click(function(){
show("/public/static/index/images/touxiangm.png",$(this).prev().val());
test();
})*/
})
</script>

css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
h5{
margin: 0;
}
img{
max-width: 100%;
vertical-align: middle
}
input{
outline: none;
}
body{
max-width: 720px;
margin: 0 auto;
background: #f1f1f1;
color:#333;
font-size: 0.26rem;
}
.header{
border-bottom: 1px solid #dfdfdf;
padding:0 0.2rem;
height: 1rem;
line-height: 1rem;
background: #fff;
position: fixed;
width: 100%;
max-width: 720px;
box-sizing: border-box;
z-index: 100;
}
.back{
position: absolute;
top: 0;
left: 0.3rem;
background:url(./images/left.png) no-repeat;
width: 0.2rem;
height:0.4rem;
margin-top: 0.34rem;
background-size: 0.2rem 0.4rem;
}
.header .tit{
font-size: 0.32rem;
vertical-align: middle;
text-align: center;
height: 1rem;
line-height: 1rem;
font-weight: normal;
}
.header .right{
position: absolute;
right: 0.3rem;
top: 0;
float: none;
font-size: 0.24rem;
line-height: 1.2rem;
}
.message{
background-color: #f1f1f1;
padding: 1.2rem 0.3rem 1rem 0.3rem;
}
.time{
font-size:0.24rem;
color:#999;
margin-bottom: 0.3rem;
text-align: center;
}

.footer{
position: fixed;
bottom: 0;
height:1rem;
background-color:#fff;
line-height:1rem;
width: 100%;
max-width: 720px;
border-top: 1px solid #ddd;
}
.footer img{
margin-left:0.2rem;
width: 0.5rem;
}
.footer input{
margin-left:0.2rem;
width:3.5rem;
height:0.64rem;
border-radius: 0.1rem;
border:0.01rem solid #ddd;
padding : 0 0.15rem;


}
.footer p{
width:1.2rem;
height:0.68rem;
font-size:0.3rem;
color:#fff;
line-height:0.68rem;
text-align:center;
background-color:#ddd;
border-radius: 0.1rem;
float:right;
margin-top:0.2rem;
margin-right:0.2rem;
}
.send:after,.show:after,.msg:after{
content: "";
clear: both;
display: table;
}

.msg>img{
width: 0.8rem;
float: left;
}
.msg>p{
float: left;
margin:0 0.4rem;
padding: 0.25rem;
background: #fff;
font-size: 0.3rem;
position: relative;
border-radius: 0.2rem;
max-width:5rem ;
box-sizing: border-box;
}

.msg_input{
position: absolute;
background: url(./images/msg-input.png) no-repeat;
background-size: 0.31rem auto;
width: 0.31rem;
height: 0.51rem;
left: -0.31rem;
top: 0.25rem;
}
.show .msg img,.show .msg p,.show .msg{
float: right;
}


.show .msg_input{
left: auto;
right: -0.11rem;
transform:rotate(180deg);
-ms-transform:rotate(180deg); /* IE 9 */
-moz-transform:rotate(180deg); /* Firefox */
-webkit-transform:rotate(180deg); /* Safari 和 Chrome */
-o-transform:rotate(180deg); /* Opera */
}
.send,.show{
padding-bottom: 0.3rem;
}
.alert_novip,.flower_num,.give_flower{
display: none;
padding: 0.3rem 0.5rem;
font-size: 0.28rem;
}
.alert_novip p,.flower_num p{
margin-bottom: 0.45rem;

.layui-layer-wrap button{
font-size: 0.28rem;
padding: 0.2rem 0.3rem;
margin-top: 0.1rem;
background: #f8f8f8;
border-radius: 10px;
}
}
.flower_num button{
padding: 0.2rem 0.5rem;
border-radius: 10px;
}
.layui-layer-wrap button:first-child{
float: left;
}
.layui-layer-wrap button:last-child{
float: right;
background: #FF7171;
color: #fff;
}
.alert_novip button{
padding: 0.2rem 0.3rem;
border-radius: 10px
}
.flower{
width: 0.8rem;
margin: 0 auto;
}
.give_flower{
text-align: center;
}
.give_flower p{
text-align: center;
line-height: 1.5;
}
.give_flower input{
width: 1rem;
margin-right: 0.1rem;
margin-top: 0.2rem;
}
.give_flower button{
display: block;
width: 3rem;
font-size: 0.28rem;
margin: 0 auto;
margin-top: 0.6rem;
float: none!important;
line-height: 0.65rem;
border-radius: 10px;
}

html样式js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function (doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
if(clientWidth>=720){
docEl.style.fontSize = '100px';
}else{
docEl.style.fontSize = 100 * (clientWidth / 720) + 'px';
}
};

if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);