# Laravel WebSocket 建置
1. Terminal輸入`composer create-project laravel/laravel=7.* XXX`
安裝Larave7最後版本 `Laravel_websocket`
2. 安裝登入系統
`composer require laravel/ui`
`php artisan ui vue --auth`
`npm install`
`npm run dev`
3. 建立資料庫
4. 安裝Laravel WebSocket套件
Terminal輸入`composer require beyondcode/laravel-websockets`
此時安裝版本有機率為2.0 beta版本

要在project裡的檔案composer.json中手動更改為1.14並且Terminal輸入`composer update`
更新至版本1.14

5. 設定 .env
```php=
DB_DATABASE='websocket_3'
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=room
PUSHER_APP_KEY=chatchat
PUSHER_APP_SECRET=chatchatchat
PUSHER_APP_CLUSTER=ap1
PUSHER_APP_PORT=6001
```
>DB_DATABASE='websocket_3'
>//資料庫名稱
>
>BROADCAST_DRIVER=pusher
>//使用pusher的部分功能
>
>PUSHER_APP_ID=room
>//可自行設定
>
>PUSHER_APP_KEY=chatchat
>//可自行設定
>
>PUSHER_APP_SECRET=chatchatchat
>//可自行設定
>
>PUSHER_APP_PORT=6001
>//預設port為6001
6. Terminal輸入
`php artisan vendor:publish provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"`
`php artisan migrate`
`php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"`
>發布套件相關設定
7. 安裝Pusher&Laravel Echo廣播套件
Terminal輸入:
`composer require pusher/pusher-php-server "~3.0"`
`npm install --save-dev laravel-echo pusher-js`
8. config/broadcasting.php
Server端Websocket設定
```php=
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
'host' => '127.0.0.1',
'port' => env('PUSHER_APP_PORT'),
'scheme' => 'http'
],
],
```
9. config/websockets.php
(可不設定)
```php=
<?php
return [
/*
* This package comes with multi tenancy out of the box. Here you can
* configure the different apps that can use the webSockets server.
*
* Optionally you can disable client events so clients cannot send
* messages to each other via the webSockets.
*/
'apps' => [
[
'id' => env('PUSHER_APP_ID'),
'name' => env('APP_NAME'),
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'enable_client_messages' => false,
'enable_statistics' => true,
],
],
/*
* This class is responsible for finding the apps. The default provider
* will use the apps defined in this config file.
*
* You can create a custom provider by implementing the
* `AppProvider` interface.
*/
'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,
/*
* This array contains the hosts of which you want to allow incoming requests.
* Leave this empty if you want to accept requests from all hosts.
*/
'allowed_origins' => [
//
],
/*
* The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
*/
'max_request_size_in_kb' => 250,
/*
* This path will be used to register the necessary routes for the package.
*/
'path' => 'laravel-websockets',
'statistics' => [
/*
* This model will be used to store the statistics of the WebSocketsServer.
* The only requirement is that the model should extend
* `WebSocketsStatisticsEntry` provided by this package.
*/
'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,
/*
* Here you can specify the interval in seconds at which statistics should be logged.
*/
'interval_in_seconds' => 60,
/*
* When the clean-command is executed, all recorded statistics older than
* the number of days specified here will be deleted.
*/
'delete_statistics_older_than_days' => 60,
/*
* Use an DNS resolver to make the requests to the statistics logger
* default is to resolve everything to 127.0.0.1.
*/
'perform_dns_lookup' => false,
],
/*
* Define the optional SSL context for your WebSocket connections.
* You can see all available options at: http://php.net/manual/en/context.ssl.php
*/
'ssl' => [
/*
* Path to local certificate file on filesystem. It must be a PEM encoded file which
* contains your certificate and private key. It can optionally contain the
* certificate chain of issuers. The private key also may be contained
* in a separate file specified by local_pk.
*/
'local_cert' => null,
/*
* Path to local private key file on filesystem in case of separate files for
* certificate (local_cert) and private key.
*/
'local_pk' => null,
/*
* Passphrase for your local_cert file.
*/
'passphrase' => null
],
];
```
10. resources/js/app.js
登入系統使用預設Vue來運作
但在本次不需要使用所以
Vue相關的都刪除or註解
```php=
// window.Vue = require('vue');
// Vue.component('example-component', require('./components/ExampleComponent.vue').default);
// const app = new Vue({
// el: '#app',
// });
```
11. resources/js/bootstrap.js
Client端WebSocket廣播系統設定
```php=
最上面新增import
import Echo from 'laravel-echo' ;
import Pusher from 'pusher-js' ;
---------------------------------
最下面新增
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
wsPath: '/liam/hao',
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
disableStats: true,
enabledTransports: ['ws', 'wss'],
});
```
12. routes/web.php
路由設定
```php=
use BeyondCode\LaravelWebSockets\Facades\WebSocketsRouter;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel;
Route::get('/', function () {
return view('websocket');
});
WebSocketsRouter::webSocket('/liam/hao/app/{appKey}', \App\Http\Controllers\MyWebsocketHandler::class);
```
13. resources/views/websocket.blade.php
Client端程式碼
```php=
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Websocket</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
}
</style>
</head>
<body>
<div class="mt-5">
<div class="d-inline-block ml-5 font-weight-bold">
<textarea class="d-inline-block font-weight-bold" cols="50" rows="10" id="txtShow" disabled></textarea>
<br>
<div class="d-inline-block">
<div id="idn" class="d-block">User = {{ auth()->user()?auth()->user()->name:'???' }}</div>
<input type="text" id="txtInput" class="d-block mb-2">
@if (auth()->user())
<input type="button" id="enter" class="d-inline-block" value="進入頻道">
@else
<div class="d-inline-block border border-dark p-1"> 無登入帳號( 無法訂閱頻道 ) </div>
@endif
<input type="submit" id="btnSend" class="d-inline-block" value="送出">
</div>
<div id="idch" class="d-block font-weight-bold">Status:</div>
<a href="{{route('login')}}"class="d-inline-block"><button>登入頁面</button></a>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js" integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js" integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script src="{{ asset('js/app.js') }}"></script>
<script>
let orderId = 20;
let keyinDom = $('#txtInput');
let showDom = $('#txtShow');
let name = "{{ auth()->user()?auth()->user()->name:'???' }}";
let chat = '';
let hr = '';
let min = '';
let sec = '';
let time = '';
function timeSet() {
hr = new Date().getHours() < 10 ? '0' + new Date().getHours() : new Date().getHours();
min = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() : new Date().getMinutes();
sec = new Date().getSeconds() < 10 ? '0' + new Date().getSeconds() : new Date().getSeconds();
time= hr + ":" + min + ":" + sec + " |";
}
// 送出留言
$(document).on('click','#btnSend',function () {
let txt = keyinDom.val();
console.log('click',txt);
Echo.connector.pusher.send_event('Send_Message', {
Name: name,
Message: keyinDom.val(),
});
keyinDom.val('');
})
// Enter鍵發送
$(document).on('keypress','#txtInput',function () {
let txt = keyinDom.val();
if (event.key === "Enter") {
// if (event.which === 13) {
if (txt.length === 0 || txt.trim() == "") {
} else {
event.preventDefault();
$("#btnSend").click();
}
}
})
// 訂閱頻道
$(document).on('click','#enter',function () {
Echo.private(`order.${orderId}`)
.listen('.pusher:Send_Message', (e) => {
console.log(e);
});
console.log('Echo Channel List:',Echo.connector.channels);
var Allchannel=[];
$.each(Echo.connector.channels,function (index,data) {
Allchannel.push(data.name);
});
$('#idch').html('Status:'+'<br>'+'已進入頻道:'+Allchannel);
})
// 接收到廣播的後續處理
Echo.connector.pusher.connection.bind('message',function (e) {
timeSet();
console.log(e);
if (e.event=='Send_Message') {
let commenter = e.data.Name;
let message = e.data.Message;
chat=`${chat} ${time} ${commenter} : ${message}\n`;
showDom.val(chat);
}
if (e.event=='pusher:subscribe') {
chat=`${chat} ${time} 使用者進入頻道\n`;
showDom.val(chat);
}
})
// 離開公共頻道
// Echo.leaveChannel('orders');
// 離開私人頻道
// Echo.leave('orders');
</script>
</body>
</html>
```
14. routes/channels.php
開啟頻道
```php=
Broadcast::channel('order.{orderId}', function () {
return true;
});
Broadcast::channel('orders.{ordersId}', function () {
return true;
});
```
15. app/http/Controllers/MyWebsocketHandler.php
WebSocket Server端核心處理器
```php=
<?php
namespace App\Http\Controllers;
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler;
use Illuminate\Broadcasting\PrivateChannel;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
class MyWebsocketHandler extends WebSocketHandler
{
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
{
parent::onMessage($connection, $message);
$payload=json_decode($message->getPayload());
// var_dump($payload->event);
$broadcast_channels = $this->channelManager->find($connection->app->id, 'private-order.20');
$send_data=$payload;
if ($payload->event !='pusher_internal:subscription_succeeded' && $payload->event !='pusher:ping' && $payload->event !='pusher:pong' && $broadcast_channels) {
foreach ($broadcast_channels->getSubscribedConnections() as $broadcast_channel) {
$broadcast_channel->send(json_encode($send_data));
}
}
}
}
```
16. config/app.php 把註解解除
`App\Providers\BroadcastServiceProvider::class,`
17. Terminal輸入`npm run dev` 開啟本地伺服器,在更動程式碼後進行更新
18. 實際運行後Terminal有可能會出現Error 如下圖↓

這是因為版本不同而造成的問題
在composer.lock檔 搜尋`react/promise`
在有安裝react/promise的情境下,
會有10個結果 第7個會如下圖一樣版本為3.0

在react/promise 3.0版本中 有模組已經被移除了,
但pusher-server還需要使用那個模組,所以造成找不到模組而Error
**解決方法**為 ==降檔==
在Terminal輸入指令`composer require react/promise:^2.7` 安裝react/promise 2.7版本
2.7版本中模組仍存在,Pusher-server也可以找的到,所以Error就不會發生
### ==[這個Error在Github上的討論串](https://github.com/beyondcode/laravel-websockets/issues/1142)==

19. 在登入頁面進行登入後,方可開始訂閱頻道

### Websocket Server端伺服器 開啟指令:`php artisan websockets:serve`
### Websocket Server端伺服器 開啟指令:`php artisan websockets:serve`
### Websocket Server端伺服器 開啟指令:`php artisan websockets:serve`
需要指定開在某個port的指令:
```shell=
php artisan websockets:serve --port=xxxx
example:
php artisan websockets:serve --port=7001
```
>參考資料
>https://beyondco.de/docs/laravel-websockets/getting-started/introduction
>https://learnku.com/articles/55724
>https://laravel.com/docs/7.x/broadcasting#using-example-application
>https://github.com/beyondcode/laravel-websockets/issues/1142