How it Works - Seunghoon-Oh/test GitHub Wiki

Used technology

  • jquery
  • Websocket(sockjs + stomp)
  • Spring-boot
  • Apache Kafka(Message Queue)
  • Zookeeper

Architecture

  • The following image shows what happens when you make a request to a service.

Architecture

Work Flow

  • The Websocket(STOMP message) is used between the web browser and the server in this application.
  • The server uses the kafka message queue.
  1. When user A input a message through a web browser and transmits it as a STOMP message to the server.
  2. When the server receives the STOMP message, it puts it in the kafka broker topic via the kafka producer.
  3. kafka Consumer pulls a new message into the topic, and send it through websocket.
  4. User B receive messages coming via websocket.

Message(STOMP)

  • 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?"
}

Source

Web

index.html

<!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>

index.js

  • 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 + '&nbsp;&nbsp;<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 + '&nbsp;&nbsp;<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);
  }
}

Server

ChattingController.java

  • 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());
    }
}

ChattingMessage.java

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;
    }

}

Sender.java

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);
    }
}

Receiver.java

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]));
    }
}
⚠️ **GitHub.com Fallback** ⚠️