- jquery
- Websocket(sockjs + stomp)
- Spring-boot
- Apache Kafka(Message Queue)
- Zookeeper
- The following image shows what happens when you make a request to a service.
- The Websocket(STOMP message) is used between the web browser and the server in this application.
- The server uses the kafka message queue.
- When user A input a message through a web browser and transmits it as a STOMP message to the server.
- When the server receives the STOMP message, it puts it in the kafka broker topic via the kafka producer.
- kafka Consumer pulls a new message into the topic, and send it through websocket.
- User B receive messages coming via websocket.
- The service will accept messages containing a name in a STOMP message whose body is a JSON object. If the message given is "Hi~ Bread!!, How are u?", then the message might look something like this:
{
"user": "Seunghoon Oh",
"message": "Hi~ Bread!!, How are u?"
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chatting App</title>
<link rel="stylesheet" href="css/normalize.min.css">
<link rel='stylesheet prefetch' href='https://fonts.googleapis.com/css?family=Open+Sans'>
<link rel='stylesheet prefetch' href='css/jquery.mCustomScrollbar.min.css'>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="chat">
<div class="chat-title">
<h1 id="full-name"></h1>
<h2 id="nick-name"></h2>
<figure class="avatar">
<img id="user-img" src=""/></figure>
</div>
<div class="messages">
<div class="messages-content"></div>
</div>
<div class="message-box">
<textarea type="text" class="message-input" placeholder="Type message..."></textarea>
<button type="submit" class="message-submit">Send</button>
<button type="submit" class="message-file" onclick="getFile();">file</button>
</div>
<div style='height: 0px;width: 0px; overflow:hidden;'><input id="upfile" type="file" onchange="sub()"/></div>
</div>
<div class="bg"></div>
<script src='js/jquery.min.js'></script>
<script src='js/jquery.mCustomScrollbar.concat.min.js'></script>
<script src="js/index.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</body>
</html>
- Create a web socket and send and receive messages.
var user;
$(function(){
user = prompt("Please enter your name.", "steve");
if(user != "steve" && user != "bread"){
alert(user + " is an unauthorized user!!");
window.close();
}else{
alert(user + "!! Wellcome!!");
}
});
var $messages = $('.messages-content'),
d, h, m,
i = 0;
$(window).load(function() {
$messages.mCustomScrollbar();
});
$( window ).ready(function() {
setInfo();
connect();
});
var info = [
'Brad Pitt|Bread|profile-80.jpg',
'Seunghoon Oh|Steve|steve.jpg'
]
var userInfo;
var friendInfo;
var stompClient = null;
function setInfo() {
if(user == "steve"){
friendInfo = info[0].split('|');
userInfo = info[1].split('|');
}else{
friendInfo = info[1].split('|');
userInfo = info[0].split('|');
}
document.getElementById("full-name").innerHTML = friendInfo[0];
document.getElementById("nick-name").innerHTML = friendInfo[1];
document.getElementById('user-img').src="./img/"+friendInfo[2];
}
function updateScrollbar() {
$messages.mCustomScrollbar("update").mCustomScrollbar('scrollTo', 'bottom', {
scrollInertia: 10,
timeout: 0
});
}
function setDate(){
d = new Date()
m = d.getMinutes();
$('<div class="timestamp">' + d.getHours() + ':' + m + '</div>').appendTo($('.message:last'));
}
function insertMessage() {
msg = $('.message-input').val();
if ($.trim(msg) == '') {
return false;
}
console.log("data",JSON.stringify({ 'message': msg, 'user': userInfo[0] }))
stompClient.send("/app/message", {}, JSON.stringify({ 'message': msg, 'user': userInfo[0] }));
$('.message-input').val(null);
}
$('.message-submit').click(function() {
insertMessage();
});
$(window).on('keydown', function(e) {
if (e.which == 13) {
insertMessage();
return false;
}
})
function getFile(){
document.getElementById("upfile").click();
}
function sub(){
var file = document.getElementById('upfile').files[0];
if(file.name != ""){
var reader = new FileReader();
var rawData = new ArrayBuffer();
reader.loadend = function() {
}
reader.onload = function(e) {
rawData = e.target.result;
stompClient.send("/app/file", {}, JSON.stringify({'rawData': rawData, 'fileName': file.name, 'user': userInfo[0] }));
}
reader.readAsBinaryString(file);
}
}
function connect() {
var socket = new SockJS('/chatting');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
stompClient.subscribe('/topic/chatting', function (greeting) {
console.log(greeting);
var data = JSON.parse(greeting.body);
console.log(data);
if(data.message != null){
showMessage(data.user, data.message);
} else {
localStorage.setItem(data.fileName, data.rawData);
showMessage2(data.user, data.fileName, data.rawData);
}
});
});
}
function saveFile(fileName) {
var arrayBuffer = localStorage.getItem(fileName);
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var parts = [];
parts.push(arrayBuffer);
url = window.URL.createObjectURL(new Blob(parts));
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
}
function showMessage(user, message) {
if(user == userInfo[0]){
$('<div class="message message-personal">' + message + '</div>').appendTo($('.mCSB_container')).addClass('new');
setDate();
updateScrollbar();
}else{
var friendImgSrc = "img/"+friendInfo[2];
$('<div class="message loading new"><figure class="avatar"><img src=\''+friendImgSrc+'\'/></figure><span></span></div>').appendTo($('.mCSB_container'));
updateScrollbar();
setTimeout(function() {
$('.message.loading').remove();
$('<div class="message new"><figure class="avatar"><img src=\''+friendImgSrc+'\' /></figure>' + message + '</div>').appendTo($('.mCSB_container')).addClass('new');
setDate();
updateScrollbar();
i++;
}, 1000 + (Math.random() * 20) * 100);
}
}
function showMessage2(user, fileName, rawData) {
if(user == userInfo[0]){
$('<div class="message message-personal">' + fileName + ' <img src="img/download.png" height="15px" width="15px" onclick="return saveFile(\''+fileName+'\')"/></div>').appendTo($('.mCSB_container')).addClass('new');
setDate();
updateScrollbar();
}else{
var friendImgSrc = "img/"+friendInfo[2];
$('<div class="message loading new"><figure class="avatar"><img src=\''+friendImgSrc+'\'/></figure><span></span></div>').appendTo($('.mCSB_container'));
updateScrollbar();
setTimeout(function() {
$('.message.loading').remove();
$('<div class="message new"><figure class="avatar"><img src=\''+friendImgSrc+'\' /></figure>' + fileName + ' <img src="img/download.png" height="15px" width="15px" onclick="return saveFile(\''+fileName+'\')"/></div>').appendTo($('.mCSB_container')).addClass('new');
setDate();
updateScrollbar();
i++;
}, 1000 + (Math.random() * 20) * 100);
}
}
- When a message is received, a controller that sends a message to the kafka broker via the kafka producer.
package chatting;
import chatting.kafka.producer.Sender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChattingController {
@Autowired
private Sender sender;
private static String BOOT_TOPIC = "chatting";
@MessageMapping("/message")
public void sendMessage(ChattingMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
sender.send(BOOT_TOPIC, message.getMessage() + "\\u0001" + message.getUser());
}
@MessageMapping("/file")
@SendTo("/topic/chatting")
public ChattingMessage sendFile(ChattingMessage message) throws Exception {
return new ChattingMessage(message.getFileName(), message.getRawData(), message.getUser());
}
}
package chatting;
public class ChattingMessage {
private String message;
private String user;
public String getUser() {
return user;
}
public void setMessage(String message) {
this.message = message;
}
public void setUser(String user) {
this.user = user;
}
public ChattingMessage(String message, String user) {
this.user = user;
this.message = message;
}
private String fileName;
private byte[] rawData;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getRawData() {
return rawData;
}
public void setRawData(byte[] rawData) {
this.rawData = rawData;
}
public ChattingMessage() {
}
public ChattingMessage(String fileName, byte[] rawData) {
this.fileName = fileName;
this.rawData = rawData;
}
public ChattingMessage(String fileName, byte[] rawData, String user) {
this.fileName = fileName;
this.rawData = rawData;
this.user = user;
}
public ChattingMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
package chatting.kafka.producer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class Sender {
private static final Logger LOGGER = LoggerFactory.getLogger(Sender.class);
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void send(String topic, String data) {
LOGGER.info("sending data='{}' to topic='{}'", data, topic);
kafkaTemplate.send(topic, data);
}
}
package chatting.kafka.consumer;
import chatting.ChattingMessage;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
@Service
public class Receiver {
private static final Logger LOGGER = LoggerFactory.getLogger(Receiver.class);
@Autowired
private SimpMessagingTemplate template;
@KafkaListener(topics = "${topic.boot}")
public void receive(ConsumerRecord<?, ?> consumerRecord) throws Exception {
LOGGER.info("received data='{}'", consumerRecord.toString());
String[] message = consumerRecord.value().toString().split("\\\\u0001");
LOGGER.info("message='{}'", Arrays.toString(message));
this.template.convertAndSend("/topic/chatting", new ChattingMessage(message[0], message[1]));
}
}