CookIM - is a distributed websocket chat applications based on akka
- Support private message and group message
- Support chat servers cluster communication
- Now we support send text message, file message and voice message. Thanks for ft115637850 's PR for voice message.
Category
###Demo
Demo on PC
Demo on Mobile
Demo link
Start multiple nodes CookIM in docker compose
Start docker compose
Change into CookIM directory, run command below, start multiple nodes CookIM servers in docker compose mode. This way will start 3 container: mongo, cookim1 and cookim2
$ git clone https://github.com/cookeem/CookIM.git
$ cd CookIM
$ sudo docker-compose up -d
Creating mongo
Creating cookim1
Creating cookim2
After run docker compose, use different browser to access the URLs below to connect to cookim1 and cookim2
Add nodes in docker compose
You can add config in docker-compose.yml
(in CookIM directory) to add CookIM server nodes, this example show how to add cookim3 in docker compose:
cookim3:
image: cookeem/cookim
container_name: cookim3
hostname: cookim3
environment:
HOST_NAME: cookim3
WEB_PORT: 8080
AKKA_PORT: 2551
SEED_NODES: cookim1:2551
MONGO_URI: mongodb://mongo:27017/local
ports:
- "8082:8080"
depends_on:
- mongo
- cookim1
Debug in docker container
View container cookim1
logs output
$ sudo docker logs -f cookim1
Exec into container cookim1
to debug
$ sudo docker exec -ti cookim1 bash
Stop docker compose
$ sudo docker-compose stop
$ sudo docker-compose rm
How to run
Prerequisites
- JDK 8+
- Scala 2.11+
- SBT 0.13.15
- MongoDB 2.6 - 3.4
Clone source code
git clone https://github.com/cookeem/CookIM.git
cd CookIM
Configuration and assembly
The configuration file locate at conf/application.conf
, please make sure your mongodb uri configuration.
mongodb {
dbname = "cookim"
uri = "mongodb://mongo:27017/local"
}
Assembly CookIM project to a fatjar, target jar locate at target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar
sbt clean assembly
Start CookIM server
CookIM use MongoDB to store chat messages and users data, make sure you startup MongoDB before you startup CookIM.
There are two ways to start CookIM server: sbt and java
a. sbt debug way:
$ cd #CookIM directory#
$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551"
b. pack and compile fat jar:
$ sbt assembly
c. java production way:
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.4-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551
Command above has start a web server listen port 8080 and akka system listen port 2551
Parameters:
-a -h [-m ] [-n] -s -w -a,--akka-port akka cluster node port -h,--host-name current web service external host name -m,--mongo-uri mongodb connection uri, example: mongodb://localhost:27017/local -n,--nat is nat network or in docker -s,--seed-nodes akka cluster seed nodes, seperate with comma, example: localhost:2551,localhost:2552 -w,--web-port web service port
Open browser and access web port 8080
Start another CookIM server
Open another terminal, start another CookIM server to test message communication between servers:
a. sbt debug way:
$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551"
b. java production way:
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551
Command above has start a web server listen port 8081 and akka system listen port 2552
Open browser and access web port 8081
Architecture
Architecture picture
**CookIM server make from 3 parts: **
- akka http: provide web service, browser connect distributed chat servers by websocket
- akka stream: akka http receive websocket message (websocket message include TextMessage and BinaryMessage), then send message to chatService by akka stream way, websocket message include JWT(Javascript web token), if JWT verify failure, chatService stream will return reject message; if JWT verify success, chatService stream will send message to ChatSessionActor
- akka cluster:akka stream send websocket message to akka cluster ChatSessionActor, ChatSessionActor use DistributedPubSub to subscribe and publish message in akka cluster. When user online session, it will subscribe the session; when user send message in session, it will publish message in akka cluster, the actors who subscribe the session will receive the publish message
akka stream websocket graph
- When akka http receive messsage from websocket, it will send message to chatService flow, here we use akka stream graph:
- Websocket message body include JWT, flowFromWS use to receive websocket message and decode JWT;
- When JWT verify failure, it will broadcast to filterFailure to filter to fail message; When JWT verify success, it will broadcast to filterSuccess to filter to success message;
- When akka stream created, builder.materializedValue will send message to connectedWs, connectedWs convert message receive to UserOnline message, then send to chatSinkActor finally send to ChatSessionActor;
- chatActorSink send message to chatSessionActor, when akka stream closed if will send UserOffline message to down stream;
- chatSource receive message back from ChatSessionActor, then send message back to flowAcceptBack;
- flowAcceptBack will let the websocket connection keepAlive;
- flowReject and flowAcceptBack messages finally send to flowBackWs, flowBackWs convert messages to websocket format then send back to users;
MongoDB tables specification
- users: users table
*login (login email)
nickname (nickname)
password (password SHA1)
gender (gender: unknow:0, boy:1, girl:2)
avatar (avatar abs path, example: /upload/avatar/201610/26/xxxx.JPG)
lastLogin (last login timestamp)
loginCount (login counts)
sessionsStatus (user joined sessions status)
[{sessionid: session id, newCount: unread message count in this session}]
friends (user's friends list: [friends uid])
dateline (register timestamp)
- sessions: sessions table
*createuid (creator uid)
*ouid (receiver uid, when session type is private available)
sessionIcon (session icon, when session type is group available)
sessionType (session type: 0:private, 1:group)
publicType (public type: 0:not public, 1:public)
sessionName (session name)
dateline (created timestamp)
usersStatus (users who joined this session status)
[{uid: uid, online: (true, false)}]
lastMsgid (last message id)
lastUpdate (last update timestamp)
- messages: messages tables
*uid (send user uid)
*sessionid (relative session id)
msgType (message type)
content (message content)
fileInfo (file information)
{
filePath
fileName
fileType
fileSize
fileThumb
}
*dateline (created timestamp)
- onlines: online users table
*uid (online user id)
dateline (last update timestamp)
- notifications: receive notifications table
noticeType (notification type: "joinFriend", "removeFriend", "inviteSession")
senduid (send user id)
*recvuid (receive user id)
sessionid (relative session id)
isRead (notification is read: 0:not read, 1:already read)
dateline (created timestamp)
Websocket message type
There are two websocket channel: ws-push and ws-chat
ws-push send sessions new message to users, when user not online the session, they still can receive which sessions has new messages
/ws-push channel
up message, use to subscribe push message:
{ userToken: "xxx" }
down message:
acceptMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "accept", content: "xxx", dateline: "xxx" }
rejectMsg: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }
message push to browser:
pushMsg: {
uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "xxx",
content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
dateline: "xxx"
}
ws-chat is session chat channel, user send and receive session messages in this channel
/ws-chat channel
up message:
onlineMsg: { userToken: "xxx", sessionToken: "xxx", msgType:"online", content:"" }
textMsg: { userToken: "xxx", sessionToken: "xxx", msgType:"text", content:"xxx" }
fileMsg: { userToken: "xxx", sessionToken: "xxx", msgType:"file", fileName:"xxx", fileSize: 999, fileType: "xxx" }<#BinaryInfo#>binary_file_array_buffer
down message:
rejectMsg: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg: { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }
message push to browser:
chatMsg: {
uid: "xxx", nickname: "xxx", avatar: "xxx", msgType: "xxx",
content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
dateline: "xxx"
}
ChangeLog
0.1.0-SNAPSHOT
0.2.0-SNAPSHOT
- CookIM now support MongoDB 3.4.4
- Upgrade akka version to 2.5.2
- Update docker-compose startup CookIM cluster readme
0.2.4-SNAPSHOT
- Now support send voice message, required WebRTC support browser, now Chrome Firefox and the new Safari11 available.
- Configurate mongodb connection params by command line.
- Update docker startup mode.