Real time chat using Angular, Spring boot, WebSocket and StompJs

What you’ll build

Project

in this article we'll build a real time app that provides chat between users in 2 formats (broadcast chat and private chat) using JAVA, Spring boot, Angular, WebSocket and StompJs (no security included).

The object that we will use in this project has the following format:

  • message: The message(String)
  • fromId: the id of the emetter (String). this id is used to identify the user in both part sending and receiving private messages.
  • toId: if specified the message will be sent to the user with that id, else it will be broadcasted to all users.

Keywords

Web Socket:

The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. [WebSocket]

STOMP JS:

STOMP is the Simple (or Streaming) Text Oriented Messaging Protocol.STOMP provides an interoperable wire format so that STOMP clients can communicate with any STOMP message broker to provide easy and widespread messaging interoperability among many languages, platforms and brokers.[STOMP]

Spring boot

Generate the project

* Using SPRING INITIALIZR

SPRING INITIALIZR

* Using Maven

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Create our java classes

  • WebSocketConfig.java: configuration class
  • SocketRest.java: websocket and stomp publisher and subscriber
package com.yamicode.socket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/socket-subscriber")
                .enableSimpleBroker("/socket-publisher");
    }

}
  • Endpoint: link to open the websocket communication.
  • setApplicationDestinationPrefixes: Link that the clients will send their messages to (Example from our app: /socket-subscriber/send/message). see angular part for more info
  • enableSimpleBroker: Link that the clients will subscribe to in order to receive their messages (Example from our app: we use "/socket-publisher" to broadcast a message and /socket-publisher/{userId} to send a private message)
package com.yamicode.socket.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Map;

@RestController
@RequestMapping(value = "/api/socket")
@CrossOrigin("*")
public class SocketRest {
    @Autowired

    private SimpMessagingTemplate simpMessagingTemplate;

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<?> useSimpleRest(@RequestBody  Map<String, String> message){
        if(message.containsKey("message")){
        	//if the toId is present the message will be sent privately else broadcast it to all users
            if(message.containsKey("toId") && message.get("toId")!=null && !message.get("toId").equals("")){
                this.simpMessagingTemplate.convertAndSend("/socket-publisher/"+message.get("toId"),message);
                this.simpMessagingTemplate.convertAndSend("/socket-publisher/"+message.get("fromId"),message);
            }else{
                this.simpMessagingTemplate.convertAndSend("/socket-publisher",message);
            }
            return new ResponseEntity<>(message, new HttpHeaders(), HttpStatus.OK);
        }
        return new ResponseEntity<>(new HttpHeaders(), HttpStatus.BAD_REQUEST);
    }

    @MessageMapping("/send/message")
    public Map<String, String> useSocketCommunication(String message){
        ObjectMapper mapper = new ObjectMapper();
        Map<String, String> messageConverted = null;
        try {
            messageConverted = mapper.readValue(message, Map.class);
        } catch (IOException e) {
            messageConverted = null;
        }
        if(messageConverted!=null){
            if(messageConverted.containsKey("toId") && messageConverted.get("toId")!=null && !messageConverted.get("toId").equals("")){
                this.simpMessagingTemplate.convertAndSend("/socket-publisher/"+messageConverted.get("toId"),messageConverted);
                this.simpMessagingTemplate.convertAndSend("/socket-publisher/"+messageConverted.get("fromId"),message);
            }else{
                this.simpMessagingTemplate.convertAndSend("/socket-publisher",messageConverted);
            }
        }
        return messageConverted;
    }

}
  • useSimpleRest: use a simple rest protocol to receive messages and then send it using stomp and web socket protocol.
  • useSocketCommunication: use the already opened socket communication.
  • ObjectMapper: it's just used to convert the string to our object (in our case we used a Map).

in each function we find 2 parts: if the object contains the toId then we send it to the user subscribed in the channel "/socket-publisher/{toId}" and to the sender "/socket-publisher/{fromId}", else we broadcast it to all users listening on channel "/socket-publisher"

The choice from those 2 ways of sending real time messages is yours. But you can use both of them it depends on your needs.

 

Angular 5

Generate the project

ng new socket-angular --routing
npm install stompjs --save
npm install sockjs-client --save
npm install ngx-toastr --save
ng g c components/yami-code-socket --spec false
ng g s services/socket --spec false

ngx-toastr: Used just for notifications (not mandatory).

Create our angular components

yami-code-socket.component.html is our graphical part of the app where we show and send our messages.

<form [formGroup]="userForm" (submit)="openSocket()" *ngIf="isLoaded">
  <p>User Information: (if user id(toId) is null the message will be sended to all users) </p>
  <div style="margin-bottom: 20px;">
    <input type="text" formControlName="fromId" name="fromId" placeholder="Your id">
    <input type="submit" [disabled]="userForm.invalid || isCustomSocketOpened" value="open socket to recieve custom messages">
  </div>
  <input type="text" formControlName="toId" name="toId" placeholder="User id to send the message to">
</form>
<form [formGroup]="form" (submit)="sendMessageUsingSocket()">
  <p>Message: </p>
  <input type="text" formControlName="message" name="message" placeholder="Message">
  <input type="submit" [disabled]="form.invalid || userForm.invalid" value="Send using socket subscription">
  <input type="button" [disabled]="form.invalid || userForm.invalid" value="Send using a rest controller" (click)="sendMessageUsingRest()">
</form>
<ul>
  <li *ngFor="let message of messages">From: {{message.fromId}}, To: {{message.toId}}, Message: {{message.message}}</li>
</ul>
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';
import { environment } from '../../../environments/environment';
import { SocketService } from '../../services/socket.service';
import { ToastrService } from 'ngx-toastr';
import { Message } from '../../model/message';

@Component({
  selector: 'app-yami-code-socket',
  templateUrl: './yami-code-socket.component.html',
  styleUrls: ['./yami-code-socket.component.css']
})
export class YamiCodeSocketComponent implements OnInit {
  private serverUrl = environment.url + 'socket'
  isLoaded: boolean = false;
  isCustomSocketOpened = false;
  private stompClient;
  private form: FormGroup;
  private userForm: FormGroup;
  messages: Message[] = [];
  constructor(private socketService: SocketService, private toastr: ToastrService
  ) { }

  ngOnInit() {
    this.form = new FormGroup({
      message: new FormControl(null, [Validators.required])
    })
    this.userForm = new FormGroup({
      fromId: new FormControl(null, [Validators.required]),
      toId: new FormControl(null)
    })
    this.initializeWebSocketConnection();
  }

  sendMessageUsingSocket() {
    if (this.form.valid) {
      let message: Message = { message: this.form.value.message, fromId: this.userForm.value.fromId, toId: this.userForm.value.toId };
      this.stompClient.send("/socket-subscriber/send/message", {}, JSON.stringify(message));
    }
  }

  sendMessageUsingRest() {
    if (this.form.valid) {
      let message: Message = { message: this.form.value.message, fromId: this.userForm.value.fromId, toId: this.userForm.value.toId };
      this.socketService.post(message).subscribe(res => {
        console.log(res);
      })
    }
  }

  initializeWebSocketConnection() {
    let ws = new SockJS(this.serverUrl);
    this.stompClient = Stomp.over(ws);
    let that = this;
    this.stompClient.connect({}, function (frame) {
      that.isLoaded = true;
      that.openGlobalSocket()
    });
  }

  openGlobalSocket() {
    this.stompClient.subscribe("/socket-publisher", (message) => {
      this.handleResult(message);
    });
  }

  openSocket() {
    if (this.isLoaded) {
      this.isCustomSocketOpened = true;
      this.stompClient.subscribe("/socket-publisher/"+this.userForm.value.fromId, (message) => {
        this.handleResult(message);
      });
    }
  }

  handleResult(message){
    if (message.body) {
      let messageResult: Message = JSON.parse(message.body);
      console.log(messageResult);
      this.messages.push(messageResult);
      this.toastr.success("new message recieved", null, {
        'timeOut': 3000
      });
    }
  }

}
  • initializeWebSocketConnection: open the websocket communication on the "/socket" link provided by our spring app.
  • openGlobalSocket: open broadcast channel.
  • openSocket: if the communication is done and we specify our userId in the html part we can access to our private chanel to receive private messages.
  • sendMessageUsingSocket: send the message using the already existing websocket communication
  • sendMessageUsingRest: send message using rest like any other form sending
  • handleResult: just add the messages to our list of messages to show and show a notification using ngx-toastr that we received a new message.
  • SocketService: service that use HttpClientModule to post requests.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { Message } from '../model/message';

@Injectable()
export class SocketService {
  url: string = environment.url + "api/socket";

  constructor(private http: HttpClient) { }

  post(data: Message) {
    return this.http.post(this.url, data)
      .map((data: Message) => { return data; })
      .catch(error => {
        return new ErrorObservable(error);
      })
      ;
  }
}
export interface Message {
    message: string,
    fromId: string,
    toId: string,
}
export const environment = {
  production: false,
  url: "http://localhost:8080/"
};
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { YamiCodeSocketComponent } from './components/yami-code-socket/yami-code-socket.component';
import { ToastrModule } from 'ngx-toastr';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { SocketService } from './services/socket.service';
import { ReactiveFormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";


@NgModule({
  declarations: [
    AppComponent,
    YamiCodeSocketComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    ToastrModule.forRoot({ timeOut: 3000 }),
    ReactiveFormsModule,
    HttpClientModule
  ],
  providers: [SocketService],
  bootstrap: [AppComponent]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { YamiCodeSocketComponent } from './components/yami-code-socket/yami-code-socket.component';

const routes: Routes = [
  { path: '', component: YamiCodeSocketComponent, pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Demo:

Source code:

The full implementation of this artcile can be found in the GitHub project. Download and unzip the source repository for this guide GIT, or clone it using Git: git clone https://github.com/YamiCode2016/real-time-chat-spring-angular.git