Post

Creating an Anonymous Socializing Hub: A Guide to Building a Registration-Free Web-Based Chat Service Using WebRTC

Introducing a Professional Web-Based Chat Platform: Enabling Seamless Social Interaction Without Registration

GitHub repository

In my recent project, I developed an online chat service designed to facilitate social connections without requiring user registration. This platform utilizes WebRTC technology to randomly pair users in one-on-one chat sessions, ensuring anonymity and security.

Additionally, the application offers user registration functionality, although it is not obligatory for accessing the chat feature. Users have the option to register, enhancing their experience with additional features and personalized settings. Furthermore, the platform provides the opportunity for certain users to assume administrative roles, empowering them to oversee and moderate specific chat rooms.

While providing an exhaustive detail of this project in a single blog post isn’t feasible, I will focus on elucidating the backend user pairing logic. Rest assured, the entire project is thoroughly documented and available on GitHub. This enables you to explore and deploy the application locally or in the cloud, and delve deeper into the codebase, including the frontend and other backend components.

The primary technology stack comprises VueJS with Typescript for the frontend and Java, leveraging the Spring framework, for the backend architecture. Join me as I unravel the sophisticated backend mechanisms driving this innovative chat service.

returns

Architectural Overview

In my application, the client is composed of two significant parts: the user interface (UI) and the Real-Time Communication (RTC) interface. On the backend, the server comprises two pivotal components: the foundational HTTP server and a WebSocket server dedicated to orchestrating RTC-related communications.

This segmentation allows for a clear division of responsibilities, ensuring efficient management of both client-side interactions and server-side operations. Through this architectural design, I achieve a robust and responsive single-page chat application capable of seamless user experiences and reliable real-time communication.

I will refrain from delving into the intricacies of the front-end components and instead direct attention towards the backend user matching logic. Similarly, I will abstain from elaborating on cross-cutting concerns such as user registration, authentication using JWT, logging, etc., as interested readers may reference the accessible source code for comprehensive insights into these aspects.

Matching Logic

As previously noted, visitors to the site will have the capability to initiate a chat session by clicking the “Start Chat” button. They will seamlessly be paired with another random site visitor who is currently available and seeking a chat partner. I refer to a chat session as room that two seeking parties can join.

The initial step for a client entails establishing a connection with the backend server. Subsequently, upon successful connection, the backend server dispatches a unique UUID to the client. This UUID serves as a reference for bookkeeping purposes, enabling the client to identify itself within the system.

1
2
3
4
5
6
7
8
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    Ready ready = new Ready();
    ready.setType("ready");
    ready.setVersion("1");
    ready.setId(UUID.randomUUID().toString());
    send(session, ready);
    super.afterConnectionEstablished(session);
}

Ready denotes a data transfer object dispatched to the frontend, encapsulating the UUID for transmission.

Upon receipt of the ready message, the client proceeds to extract the server-generated UUID. Subsequently, in conjunction with a user-selected name, the client transmits an identify message to the backend server. This message includes the UUID as the client ID, alongside the provided name.

Upon receipt of the identify message from the client, the server parses the transmitted ID and name. If the client is new and lacks an existing session, the server responds with a hello message and proceeds to store the session information associated with the respective client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException, Exception {
SignalMessage signalMessage = Utils.getObject(message.getPayload());
Map<String, String> data = (Map<String, String>) signalMessage.getData();
String type = signalMessage.getType();
if ("identify".equals(type)) {
    Peer client = new Peer();
    client.setId(data.get("id"));
    client.setName(data.get("name"));

    if (!this.sessions.containsKey(session.getId())) {
        // This is a new client -- tell it about any existing peers
        SignalMessage responseHello = new SignalMessage();
        responseHello.setType("hello");
        send(session, responseHello);

        LOG.info("New client " + client);
    }

    this.sessions.put(session.getId(), new SessionData(new ConcurrentWebSocketSessionDecorator(session, 2000, 4096), client));
} else if ("room".equals(type)) {
  ...

Subsequently, upon receiving the hello message, the client initiates a request for a new room by transmitting a room message to the server.

Upon reception of the “room” message, the server iterates through the list of idle sessions in search of a potential roommate. Upon finding an available session, the server generates a new random room name. Subsequently, the server responds to both the requesting client and the new roommate with the generated room name, facilitating their connection.

Prior to proceeding with matching the requesting client with a potential roommate, the server must verify that the selected roommate maintains an active session. This precaution accounts for scenarios where a user may have closed their browser, and the backend has not yet received an update regarding the terminated connection. To address this, the server iterates through the idle list, ensuring that the selected roommate possesses an open session. In the process, any inactive sessions encountered are promptly removed, either until a suitable roommate is found or until the idle list is exhausted.

In the event that no roommate is available—either due to no other users being connected or all others already being engaged in chat rooms—the server will include the requesting client itself in the idle list, thereby maintaining its availability for future pairing opportunities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...
} else if ("room".equals(type)) {
  Peer client = this.sessions.get(session.getId()).getPeer();
  LOG.info(client.getName() + " seeks next");
  // Leave current room
  this.leaveCurrentRoom(session);

  SessionData clientSessionData = this.sessions.get(session.getId());
  SessionData roommateSessionData = null;
  if (idle.size() >= 1) {
      roommateSessionData = nextIdleClient(this.sessions.get(session.getId()));

      // Remove any ghost peers that left without saying good bye
      while (roommateSessionData != null && (!roommateSessionData.getWebsocketSession().isOpen() || roommateSessionData.equals(clientSessionData))) {
          if (!roommateSessionData.equals(clientSessionData)) {
              this.leaveCurrentRoom(roommateSessionData.getWebsocketSession());
              this.sessions.remove(roommateSessionData.getWebsocketSession().getId());
          }
          roommateSessionData = nextIdleClient(this.sessions.get(session.getId()));
      }
  }

  if (roommateSessionData != null) {
      LOG.info("Random picked " + roommateSessionData.getPeer().getName() + " for " + client.getName() + ". Idles now are: " + toString(this.idle));

      // Get a new room
      String roomName = UUID.randomUUID().toString();
      Set<SessionData> roommates = new HashSet<>();

      this.idle.remove(this.sessions.get(session.getId()));
      this.idle.remove(this.sessions.get(roommateSessionData.getWebsocketSession().getId()));

      this.rooms.put(roomName, roommates);

      this.sessions.get(session.getId()).getPeer().setCurrentRoom(roomName);
      this.sessions.get(roommateSessionData.getWebsocketSession().getId()).getPeer().setCurrentRoom(roomName);

      SignalMessage responseRoom = new SignalMessage();
      responseRoom.setType("room");
      responseRoom.setData(Map.ofEntries(entry("name", roomName)));

      send(session, responseRoom);
      send(roommateSessionData.getWebsocketSession(), responseRoom);

      LOG.info("Generated new room " + roomName + " for " + roommateSessionData.getPeer().getName() + " and " + client.getName());
  } else {
      LOG.info("No roommates available for " + client.getName());
      this.idle.add(this.sessions.get(session.getId()));
  }
} else if ("join".equals(type)) {
  ...

Subsequently, each client receives a room message, encompassing the room identifier along with supplementary details about the chat partner, such as their name. The client then responds with a join message to indicate their intent to join the newly allocated room.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
...
else if ("join".equals(type)) {
  Peer client = this.sessions.get(session.getId()).getPeer();

  String roomName = data.get("name");
  Set<SessionData> roommates = this.rooms.get(roomName);

  roommates.add(this.sessions.get(session.getId()));
  LOG.info("Client " + this.sessions.get(session.getId()).getPeer() + " entered room " + roomName + " of size " + roommates.size());

  for (SessionData otherSessionData : roommates) {
      Peer peer = otherSessionData.getPeer();
      if (!peer.getId().equals(client.getId())) {
          // This is a new client -- tell it about any existing peers
          this.notifyClient(session, client, peer, false);

          // Announce the new peer to other peers already in room
          this.notifyClient(otherSessionData.getWebsocketSession(), peer, client, false);

          SignalMessage inviteSignal = new SignalMessage();
          inviteSignal.setType("invite");
          inviteSignal.setData(Map.ofEntries(
                  entry("id", client.getId())
                  , entry("name", client.getName())));
          send(otherSessionData.getWebsocketSession(), inviteSignal);
      }
  }
} else {
  if (data.containsKey("target")) {
      Optional<WebSocketSession> con = getConnection(data.get("target"));
      if (con.isPresent()) {
          send(con.get(), signalMessage);
      }
  }
}

Upon receipt of the join request from each client, the server conducts essential bookkeeping by logging the sessions/participants present in the room. Subsequently, it dispatches an invite message to each participant within the room. Upon reception of the invite message containing the other participant’s ID, each client initiates the establishment of a WebRTC connection. The backend server acts as an intermediary facilitator, relaying messages between clients until a peer-to-peer connection has been successfully established among all participants. The final “else” clause in the provided code snippet serves as a catch-all mechanism for handling various message types, such as peer, offer, accept, and so forth.

Final Thoughts

returns

This post was crafted with the intention of providing insight into the capabilities of WebRTC and inspiring readers with the possibilities offered by various technologies in combination. Rather than a comprehensive guide, it aims to offer a conceptual understanding of developing a random chat application utilizing tools such as VueJS, Spring, JWT, and WebSocket. The objective is to illustrate the process of implementing core features, including the main matching algorithm.

For those interested, the accompanying source code contains numerous advanced features, including real-time chatting, backend chat monitoring functionality designed for administrative oversight, and a user-friendly dashboard providing snapshots of ongoing video chats for content moderation purposes. Additionally, user registration functionality enhances the overall user experience.

I trust that this brief exploration has been enlightening and that readers have gleaned valuable insights from it.

This post is licensed under CC BY 4.0 by the author.