關於easyswoole實現websocket聊天室的步驟解析


在去年,我們公司內部實現了一個聊天室系統,實現了一個即時在線聊天室功能,可以進行群組,私聊,發圖片,文字,語音等功能,那么,這個聊天室是怎么實現的呢?后端又是怎么實現的呢?

后端框架

在后端框架上,我選用了php的easyswoole,easyswoole作為swoole中最簡單易學的框架,上手簡單,文檔齊全,社區活躍

仙士可博客

 

直接通過easyswoole官方文檔的例子,即可實現一個websocket服務器,並且還實現了對控制器的轉發等:

https://www.easyswoole.com/Cn/Socket/webSocket.html

 

前后端通信協議

由於考慮到聊天室的業務邏輯復雜,我們使用了http+websocket 2種協議,分別用在以下幾個地方:

登錄注冊,個人信息修改,好友申請等,使用http 接口實現

私聊,群聊消息推送,系統消息申請等,使用websocket即時推送

 

websocket即時推送封包方式

在websocket中,為了區分客戶端不同的操作(發送群消息,發送私聊消息等),我們定義了一個數據格式:

1
2
3
4
5
op  命令
- args 額外參數
- msg 消息內容
- msgType 消息類型(默認為1)
- flagId 消息標識符(前端隨機生成一個標識符,后台處理完該消息之后,會返回相同的標識符給與前端確認)

使用json字符串方式傳遞

同樣,為了區分服務端不同的推送,我們定義了服務端的響應格式:

1
2
3
4
5
op  命令(響應類型)
- args 額外參數
- msg 消息內容(成功時為OK)
- msgType 消息類型(默認為1)
- flagId 將返回和前端一致的標識符,告知前端該次請求 成功/失敗

 

例如:

1
2
3
4
5
6
7
## 發送消息
私聊消息:
`{ "op" :1001, "args" :{ "userId" :12}, "msg" : "test" , "flagId" :10086}`
將回復:
`{ "op" :1000, "args" :[], "msg" : "ok" , "flagId" :10086}`
目標用戶將收到:
`{ "op" :1101, "args" :{ "fromUserId" : "12" , "msgId" :16}, "msg" : "test" }`

下文有許多op:xxx的數據,可以忽略xxx的數據,直接聯系上下文獲得op的命令類型

 

聊天記錄存儲

根據消息的類型,我們區分了 私聊消息,群消息,系統消息 3種消息,設計了3個表
為了使得客戶端能夠正常顯示群消息,我們對群成員做了軟刪除處理,確保可以獲取到群成員頭像

用戶可通過http接口,獲得歷史聊天記錄

 

語音,圖片,視頻聊天

在上面我們可以看到,有一個msgType字段,它將決定了這條數據是文字消息,還是語音,視頻

當msgType為語音類型時,msg將附帶一個語音文件的地址(通過http接口上傳文件,到oss或者服務器)

客戶端進行判斷,如果是語音,則下載文件,點擊即可播放,視頻,圖片同理

 

心跳設置

由於tcp的特性,在長時間沒有通信時,操作系統可能會自動對tcp連接進行銷毀並且可能沒有close事件提示,所以我們在websocket中提供了ping的命令,該命令發起后,服務器將響應pong,完成一次通信:

1
2
3
4
## ping
發送:直接給客戶端發送  "ping" 即可
返回:
`{ "op" :1000, "args" :null, "msg" : "PONG" }`

 

網絡不穩定推送問題

當服務端推送消息時,為了確保用戶已經收到,提供了isRecv字段,默認為0
當用戶A向用戶B發送消息,服務器向B推送時,該條消息記錄初始isRecv為0,只有當B客戶端接收到消息,並且向服務器發送已接收命令時,才會置為1:

1
2
3
4
5
### 消息接收狀態
`{ "op" :4002, "args" :{ "msgId" :42}, "msg" : "" , "flagId" :111}`
 
服務器將響應:
`{ "op" :1000, "args" :[], "msg" : "ok" , "msgType" :1, "flagId" :111}`

每次重新連接websocket服務時,可通過發起好友未讀消息推送的命令,向服務器獲得之前的未讀消息(網絡不穩定斷線重連)

1
2
3
4
5
6
當ws連接成功時,可通過該命令獲取所有的未讀好友消息:
`{ "op" :4001, "args" :{ "userId" :null, "size" :5}, "msg" : "" , "flagId" :111}`
其中`userId` 為限制單獨一個好友的未讀消息,可不傳
其中`size`為每次響應條數,默認為5,可不傳
服務器將響應:
`{ "op" :4101, "args" :{ "total" :0, "list" :[]}, "msg" : "ok" , "msgType" :1, "flagId" :111}

每次推送完,都需要客戶端遍歷list,進行上面的已接收推送

 

聊天室流程講解

整個聊天室流程為:

- 用戶http接口登錄獲得授權

- 通過授權請求http接口獲得好友列表,不同好友的最后一條未讀消息以及未讀消息數(用於首頁顯示)

- 通過授權請求獲得群列表(群消息為了節省存儲空間沒有做已讀未讀)

- 建立ws鏈接

- 注冊斷線重連機制,當觸發close事件時,重連ws

- 建立ping定時器,每隔30秒進行一次ping

- 通過ws接口,獲得所有未讀消息,客戶端進行處理,推送到通知欄等

- 接收新消息推送,並顯示到消息列表

- 當點擊進某個群/好友消息界面時,自動獲取最新n條消息,用戶上拉時繼續獲取n條

 

不同設備數據同步

為了服務端性能問題,所有消息記錄,好友消息,群成員消息將緩存到客戶端,當用戶登錄成功時
直接顯示之前登錄時的所有狀態(消息列表,最后一條消息顯示等)
當新設備登錄時,只獲取未讀消息列表,其他消息需要點擊某個好友/群,才會進行顯示

 

fd->userId對應

當用戶登錄成功時,我們使用了swoole的Table進行存儲fd->userId以及userId->fd的對應

通過這2者對應的存儲,我們可以通過userId找到fd進行推送數據,也可以通過fd找到userId獲取用戶消息

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
<?php
 
 
namespace  App\Utility;
 
 
use  EasySwoole\Component\Singleton;
use  Swoole\Table;
 
class  FdManager
{
     use  Singleton;
 
     private  $fdUserId ; //fd=>userId
     private  $userIdFd ; //userId=>fd
 
     function  __construct(int  $size  = 1024*256)
     {
         $this ->fdUserId =  new  Table( $size );
         $this ->fdUserId->column( 'userId' ,Table::TYPE_STRING,25);
         $this ->fdUserId->create();
         $this ->userIdFd =  new  Table( $size );
         $this ->userIdFd->column( 'fd' ,Table::TYPE_INT,10);
         $this ->userIdFd->create();
     }
 
     function  bind(int  $fd ,int  $userId )
     {
         $this ->fdUserId->set( $fd ,[ 'userId' => $userId ]);
         $this ->userIdFd->set( $userId ,[ 'fd' => $fd ]);
     }
 
     function  delete (int  $fd )
     {
         $userId  $this ->fdUserId( $fd );
         if ( $userId ){
             $this ->userIdFd->del( $userId );
         }
         $this ->fdUserId->del( $fd );
     }
 
     function  fdUserId(int  $fd ):?string
     {
         $ret  $this ->fdUserId->get( $fd );
         if ( $ret ){
             return  $ret [ 'userId' ];
         } else {
             return  null;
         }
     }
 
     function  userIdFd(int  $userId ):?int
     {
         $ret  $this ->userIdFd->get( $userId );
         if ( $ret ){
             return  $ret [ 'fd' ];
         } else {
             return  null;
         }
     }
}

 

同理,當需要群發消息時,只需要獲得群成員的userId,即可獲得當前所有在線成員的fd,進行遍歷推送

服務端推送問題

當A客戶端在群發送一條消息時,由於群成員可能有很多,如果直接同步推送給所有群成員,會造成A客戶端等待響應時間過長的情況
所以需要使用task做異步推送:

當A客戶端發送一條消息,先存入數據庫,並調用task進行異步群發推送,同時給A客戶端響應ok,代表接收到此消息

通過easyswoole的task組件,進行推送:

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
namespace  App\Task;
 
 
use  App\HttpController\Api\User\Message\GroupMessage;
use  App\HttpController\Api\User\Message\SystemMessage;
use  App\Model\Group\GroupUserModel;
use  App\Model\Message\GroupMessageModel;
use  App\Model\Message\SystemMessageModel;
use  App\Model\Message\UserMessageModel;
use  App\Utility\FdManager;
use  App\WebSocket\Command;
use  EasySwoole\EasySwoole\ServerManager;
use  EasySwoole\Task\AbstractInterface\TaskInterface;
 
//消息異步推送
class  WebSocketPush  implements  TaskInterface
{
     protected  $messageModel ;
 
     function  __construct( $messageModel )
     {
         $this ->messageModel =  $messageModel ;
     }
 
     function  run(int  $taskId , int  $workerIndex )
     {
         $message  $this ->messageModel;
         $result  = false;
         //好友消息
         if  ( $message  instanceof  UserMessageModel) {
             $result  $this ->friendMsg( $message );
         }
         //群組消息
         if  ( $message  instanceof  GroupMessageModel) {
             $result  $this ->groupMsg( $message );
         }
         //系統消息
         if  ( $message  instanceof  SystemMessageModel) {
             $result  $this ->systemMsg( $message );
         }
         return  $result ;
     }
}

 

websocket驗權,提下線功能

用戶在連接ws服務時,需要帶上token進行驗權,
服務端在onopen事件時,會進行token驗權,如果驗證失敗則響應一條消息表示登錄過期:

1
2
3
4
5
6
7
{
     "op" : -1003,
     "args" : [],
     "msg" "登陸狀態失效" ,
     "msgType" : 1,
     "flagId" : null
}

 

當A用戶在客戶端1登錄成功后,又在客戶端2登錄時,將給客戶端1發送一條已被踢下線消息::

1
2
3
4
5
6
7
{
     "op" : -1002,
     "args" : [],
     "msg" "你的賬號在其他設備登陸,你已被強制下線" ,
     "msgType" : 1,
     "flagId" : null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    static  function  onOpen(Server $server, \Swoole\Http\Request $request)
     {
         $session = $request->get[ 'userSession' ] ?? null;
         $user = new UserModel();
         if  (!empty($session)) {
             $user->userSession = $session;
             $info = $user->getOneBySession();
             if  (empty($info)) {
                 self::pushSessionError($request->fd);
                 ServerManager::getInstance()->getSwooleServer()->close($request->fd);
                 return  true ;
             }
             // 如果已經有設備登陸,則強制退出
             self::userClose($info->userId);
             FdManager::getInstance()->bind($request->fd, $info->userId);
             // 推送消息
//                 self::pushMessage($request->fd,$info->userId);
         else  {
             self::pushSessionError($request->fd);
             ServerManager::getInstance()->getSwooleServer()->close($request->fd);
         }
     }

 

關於客戶端網絡不穩定時候的情況解析

當客戶端發送一條消息之前,需要生成一個flagId,發送消息時附帶flagId

服務端響應消息時,會附帶flagId

因此,當客戶端發送消息時,新增一個flagId的定時器,當定時器到期卻沒有接收到服務端響應消息時,判斷該條消息發送失敗,顯示紅色感嘆號,提示用戶重發

當服務端響應成功時,將取消這個定時器,並直接將消息置為發送成功狀態

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM