Laravel + connmix develop distributed WebSocket chat room
Star
https://github.com/connmix/examples for the latest version of the example
connmix is a distributed long connection engine based on go + lua to develop message-oriented programming, which can be used for the development of the Internet, instant messaging, APP development, online games, hardware communication, smart home, Internet of Things and other fields.
Clients in various languages such as java, php, go, nodejs, etc.
Laravel is recognized as the most elegant traditional framework in the PHP industry. Of course, you can also choose other frameworks such as thinkphp.
The combination of the two can quickly develop a distributed websocket
long connection service with strong performance, which is very suitable for the development of IM, chat room, customer service system, live broadcast barrage, page games and other needs.
Install
- Install
CONNMIX
engine: https://connmix.com/docs/1.0/#/zh-cn/install-engine - Install the latest version of the
Laravel
framework
composer create-project laravel/laravel laravel-chat
- Then install the connmix-php client
cd laravel-chat
composer require connmix/connmix
solution
- Use in the command line
connmix
client consumes the memory queue (WebSocket messages sent by the front end). - We choose Laravel's command line mode, which is
console
to write business logic, so that we can use Laravel's DB, Redis and other ecological libraries.
API Design
As a chat room, we need to design the WebSocket API interface before we start. We use the most widely used json
format to transmit data, and the interaction adopts the classic pubsub
mode.
Features | Format |
---|---|
User login | {"op":"auth","args":["name","pwd"]} |
Subscribe to room channel | {"op":"subscribe","args":["room_101"]} |
Subscribe to user channel | {"op":"subscribe","args":["user_10001"]} |
Subscribe to radio channels | {"op":"subscribe","args":["broadcast"]} |
Unsubscribe from a channel | {"op":"unsubscribe","args":["room_101"]} |
Receive room messages | {"event":"subscribe","channel":"room_101","data":"hello,world!"} |
Receive user messages | {"event":"subscribe","channel":"user_10001","data":"hello,world!"} |
receive broadcast messages | {"event":"subscribe","channel":"broadcast","data":"hello,world!"} |
send message to room | {"op":"sendtoroom","args":["room_101","hello,world!"]} |
send message to user | {"op":"sendtouser","args":["user_10001","hello,world!"]} |
send broadcast | {"op":"sendbroadcast","args":["hello,world!"]} |
success | {"op":" * ","success":true} |
mistake | {"op":"***","error":" * "} |
Database Design
We need to do login, so we need a users
table to handle authentication, here is just for demonstration so the table design is deliberately simplified.
- File Path: users.sql
CREATE TABLE `users`
(
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_n` (`name`)
);
The room table is temporarily not designed here, everyone can expand it by themselves.
Modified entry.lua
User login needs to be added to the lua protocol conn:wait_context_value
to complete, we modify entry.lua
as follows:
- File path: entry.lua
-
protocol_input
Modify the bound url path -
on_message
Add blocking wait context
require("prettyprint")
local mix_log = mix.log
local mix_DEBUG = mix.DEBUG
local websocket = require("protocols/websocket")
local queue_name = "chat"
function init()
mix.queue.new(queue_name, 100)
end
function on_connect(conn)
end
function on_close(err, conn)
--print(err)
end
--buf为一个对象,是一个副本
--返回值必须为int, 返回包截止的长度 0=继续等待,-1=断开连接
function protocol_input(buf, conn)
return websocket.input(buf, conn, "/chat")
end
--返回值支持任意类型, 当返回数据为nil时,on_message将不会被触发
function protocol_decode(str, conn)
return websocket.decode(conn)
end
--返回值必须为string, 当返回数据不是string, 或者为空, 发送消息时将返回失败错误
function protocol_encode(str, conn)
return websocket.encode(str)
end
--data为任意类型, 值等于protocol_decode返回值
function on_message(data, conn)
--print(data)
if data["type"] ~= "text" then
return
end
local auth_op = "auth"
local auth_key = "uid"
local s, err = mix.json_encode({ frame = data, uid = conn:context()[auth_key] })
if err then
mix_log(mix_DEBUG, "json_encode error: " .. err)
return
end
local tb, err = mix.json_decode(data["data"])
if err then
mix_log(mix_DEBUG, "json_decode error: " .. err)
return
end
local n, err = mix.queue.push(queue_name, s)
if err then
mix_log(mix_DEBUG, "queue push error: " .. err)
return
end
if tb["op"] == auth_op then
conn:wait_context_value(auth_key)
end
end
Write business logic
Then we write code in console
to generate a command line class
php artisan make:command Chat
- File Path: Console/Commands/Chat.php
We use the connmix-php
client to handle the consumption of the memory queue.
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Nette\Utils\ArrayHash;
use phpDocumentor\Reflection\DocBlock\Tags\BaseTag;
class Chat extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:chat';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$client = \Connmix\ClientBuilder::create()
->setHost('127.0.0.1:6787')
->build();
$onConnect = function (\Connmix\AsyncNodeInterface $node) {
// 消费内存队列
$node->consume('chat');
};
$onReceive = function (\Connmix\AsyncNodeInterface $node) {
$message = $node->message();
switch ($message->type()) {
case "consume":
$clientID = $message->clientID();
$data = $message->data();
// 解析
$json = json_decode($data['frame']['data'], true);
if (empty($json)) {
$node->meshSend($clientID, '{"error":"Json format error"}');
return;
}
$op = $json['op'] ?? '';
$args = $json['args'] ?? [];
$uid = $data['uid'] ?? 0;
// 业务逻辑
switch ($op) {
case 'auth':
$this->auth($node, $clientID, $args);
break;
case 'subscribe':
$this->subscribe($node, $clientID, $args, $uid);
break;
case 'unsubscribe':
$this->unsubscribe($node, $clientID, $args, $uid);
break;
case 'sendtoroom':
$this->sendToRoom($node, $clientID, $args, $uid);
break;
case 'sendtouser':
$this->sendToUser($node, $clientID, $args, $uid);
break;
case 'sendbroadcast':
$this->sendBroadcast($node, $clientID, $args, $uid);
break;
default:
return;
}
break;
case "result":
$success = $message->success();
$fail = $message->fail();
$total = $message->total();
break;
case "error":
$error = $message->error();
break;
default:
$payload = $message->payload();
}
};
$onError = function (\Throwable $e) {
// handle error
print 'ERROR: ' . $e->getMessage() . PHP_EOL;
};
$client->do($onConnect, $onReceive, $onError);
return 0;
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @return void
*/
protected function auth(\Connmix\AsyncNodeInterface $node, int $clientID, array $args)
{
list($name, $password) = $args;
$row = \App\Models\User::query()->where('name', '=', $name)->where('password', '=', $password)->first();
if (empty($row)) {
// 验证失败,设置一个特殊值解除 lua 代码阻塞
$node->setContextValue($clientID, 'user_id', 0);
$node->meshSend($clientID, '{"op":"auth","error":"Invalid name or password"}');
return;
}
// 设置上下文解除 lua 代码阻塞
$node->setContextValue($clientID, 'uid', $row['id']);
$node->meshSend($clientID, '{"op":"auth","success":true}');
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @param int $uid
* @return void
*/
protected function subscribe(\Connmix\AsyncNodeInterface $node, int $clientID, array $args, int $uid)
{
// 登录判断
if (empty($uid)) {
$node->meshSend($clientID, '{"op":"subscribe","error":"No access"}');
return;
}
// 此处省略业务权限效验
// ...
$node->subscribe($clientID, ...$args);
$node->meshSend($clientID, '{"op":"subscribe","success":true}');
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @param int $uid
* @return void
*/
protected function unsubscribe(\Connmix\AsyncNodeInterface $node, int $clientID, array $args, int $uid)
{
// 登录判断
if (empty($uid)) {
$node->meshSend($clientID, '{"op":"unsubscribe","error":"No access"}');
return;
}
$node->unsubscribe($clientID, ...$args);
$node->meshSend($clientID, '{"op":"unsubscribe","success":true}');
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @param int $uid
* @return void
*/
protected function sendToRoom(\Connmix\AsyncNodeInterface $node, int $clientID, array $args, int $uid)
{
// 登录判断
if (empty($uid)) {
$node->meshSend($clientID, '{"op":"sendtoroom","error":"No access"}');
return;
}
// 此处省略业务权限效验
// ...
list($channel, $message) = $args;
$message = sprintf('uid:%d,message:%s', $uid, $message);
$node->meshPublish($channel, sprintf('{"event":"subscribe","channel":"%s","data":"%s"}', $channel, $message));
$node->meshSend($clientID, '{"op":"sendtoroom","success":true}');
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @param int $uid
* @return void
*/
protected function sendToUser(\Connmix\AsyncNodeInterface $node, int $clientID, array $args, int $uid)
{
// 登录判断
if (empty($uid)) {
$node->meshSend($clientID, '{"op":"sendtouser","error":"No access"}');
return;
}
// 此处省略业务权限效验
// ...
list($channel, $message) = $args;
$message = sprintf('uid:%d,message:%s', $uid, $message);
$node->meshPublish($channel, sprintf('{"event":"subscribe","channel":"%s","data":"%s"}', $channel, $message));
$node->meshSend($clientID, '{"op":"sendtouser","success":true}');
}
/**
* @param \Connmix\AsyncNodeInterface $node
* @param int $clientID
* @param array $args
* @param int $uid
* @return void
*/
protected function sendBroadcast(\Connmix\AsyncNodeInterface $node, int $clientID, array $args, int $uid)
{
// 登录判断
if (empty($uid)) {
$node->meshSend($clientID, '{"op":"sendbroadcast","error":"No access"}');
return;
}
// 此处省略业务权限效验
// ...
$channel = 'broadcast';
list($message) = $args;
$message = sprintf('uid:%d,message:%s', $uid, $message);
$node->meshPublish($channel, sprintf('{"event":"subscribe","channel":"%s","data":"%s"}', $channel, $message));
$node->meshSend($clientID, '{"op":"sendbroadcast","success":true}');
}
}
debugging
start the service
- start
connmix
engine
% bin/connmix dev -f conf/connmix.yaml
- Start
Laravel
command line (you can start multiple to increase performance)
% php artisan command:chat
WebSocket Client 1
Connection: ws://127.0.0.1:6790/chat
- Log in
send: {"op":"auth","args":["user1","123456"]}
receive: {"op":"auth","success":true}
- join room
send: {"op":"subscribe","args":["room_101"]}
receive: {"op":"subscribe","success":true}
- Send a message
send: {"op":"sendtoroom","args":["room_101","hello,world!"]}
receive: {"event":"subscribe","channel":"room_101","data":"uid:1,message:hello,world!"}
receive: {"op":"sendtoroom","success":true}
WebSocket Client 2
Connection: ws://127.0.0.1:6790/chat
- Log in
send: {"op":"auth","args":["user2","123456"]}
receive: {"op":"auth","success":true}
- join room
send: {"op":"subscribe","args":["room_101"]}
receive: {"op":"subscribe","success":true}
- receive message
receive: {"event":"subscribe","channel":"room_101","data":"uid:1,message:hello,world!"}
Epilogue
Based on connmix
client, we can quickly create a distributed long connection service with very little code.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。