### 效果圖:

### 功能:
- 好友私聊
- 多人群聊
- 添加好友
- 加入群組
- 離線消息
- 未讀消息提醒
- 聊天對話窗口
### 流程:
- 客戶端第一次連接服務端,服務端保存連接和uid映射關系

- 主動發送消息給服務端,調用ws.send(),接受服務端發來消息時會觸發onmessage事件

#### 客戶端api
```javascript
var ws = new WebSocket();
ws.onopen() // 第一次連接服務端,會觸發
ws.onmessage() // 接受到服務端消息會觸發
ws.send() // 主動推送消息給服務端
ws.onclose() // 關閉客戶端會觸發
```
#### 服務端api
```php
$server = new swoole_websocket_server()
$server->on('open',functoin(){}); // 有客戶端連接時,會觸發
$server->on('message',function(){}) // 接受到客戶端消息會觸發
$server->on('close',function(){}) // 客戶端關閉會觸發,可以用來清除redis中fd和uid的映射關系
```
### 考慮的問題:
- 離線消息和聊天歷史記錄問題:
- 不在線的用戶也可以收到消息,用戶可以查看歷史聊天記錄,要實現這兩個功能點必須要求消息持久化處理,可以保存到mysql,mongodb,file文件;
- 消息可以分為兩種類型,一種是私聊,另一種是群聊,保存的時候要區分兩者
- 保存聊天記錄不影響我們的正常流程,所以可以用異步去處理數據,這樣系統可以更快的響應客戶端
- 保存用戶uid和連接fd映射關系:
- 客戶端每次打開新頁面都會有產生一個新的連接fd,也就說一個uid會對應多個fd,發送給uid時,我們需要把消息推送到uid對應的每一個fd,這里用redis的集合來存儲uid和fd關系
- 聊天對話:
- 聊天對話是相互的,例如a給b發消息,此時a和b擁有同個對話,我們a和b對話設置成一條記錄,找出對話框時可以用or來處理,避免生成兩條記錄,同時這個對話ID可以作為消息記錄外鍵,a和b都可以通過對話
ID查詢到彼此的聊天記錄
### 數據庫設計:
> 針對上面問題,我主要設置了聊天對話表和聊天記錄表
- 對話框表
### 表: chat_dialog 聊天對話表
| 字段 | 類型 | 備注 |
| :------: | :------: | :------: |
| id | int | 主鍵ID |
| from_uid | int | 對話發起者 |
| to_uid | int | 對話接受者 |
| group_id | int | 群組ID,默認為0 |
| type | tinyint | 對話類型,1表示人對人,2表示人對群 |
| status | tinyint | 狀態,0正常,1關閉 |
獲取某個人所有對話(包括人對人,人對群):
```sql
select * from chat_dialog where from_uid =1 or to_uid = 1;
```
> 說明下:例如a發消息給b,此時生成一條chat_dialog記錄,from_uid是a,to_uid是b;需要獲取a所有對話時,可以用select * from chat_dialog where from_uid = a or to_uid =a;
同理,需要獲取b聊天對話時,可以執行select * from chat_dialog where from_uid = b or to_uid =b;這樣雙方都能獲得同一個對話消息
### 表: chat_records 消息記錄表
| 字段 | 類型 | 備注 |
| :------: | :------: | :------: |
| id | int | 主鍵ID |
| user_id | int | 消息發送者 |
| dialog_id | int | 對話ID,包括私聊和群聊 |
| group_id | int | 群組ID,默認為0 |
| status | tinyint | 狀態,0正常,1關閉 |
獲取私聊對話歷史消息:
```sql
select * from chat_records where dialog_id = 1;
```
獲取群聊歷史消息
```sql
select * from chat_records where group_id = 1;
```