6 min read
Original source

Real-time chat with WebSockets

With WebSockets, we can perform a two-way communication in real-time between the user and the server. Thanks to that, the browser can send messages to the…

With WebSockets, we can perform a two-way communication in real-time between the user and the server. Thanks to that, the browser can send messages to the server and listen to information from the other side. The principles of the WebSocket handshake WebSocket is a protocol that operates in a different way than HTTP. Even though that’s the case, establishing the connection begins with the client sending an HTTP call that we call a handshake. The server listens for incoming socket connections using a regular TCP socket. The client sends a GET request to the URL of our socket.Request URL: ws://localhost:8080/ Request Method: GET Request headers: Headers: Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: 2GruKa/C487njkWNw2HKxQ==Above, we can see the Connection: Upgrade and Upgrade: websocket headers. The server understands that the client requests to upgrade the protocol from HTTP to WebSocket. After receiving the above request, the server responds with an indication that the protocol will change from HTTP to WebSocket. The status code of the response is Status Code: 101 Switching Protocols. Response headers: Headers: Connection: Upgrade Upgrade: websocket Sec-WebSocket-Accept: aue6dyRHSJ/yBtny+BQRe0lHOu0=In the request, we can also see the Sec-WebSocket-Key header that contains random bytes. The browser adds it to prevent the cache proxy from responding with a previous WebSocket connection. The server hashes the value of the Sec-WebSocket-Key and sends the value through the Sec-WebSocket-Accept. Thanks to that, the client can make sure that it got the correct response. Implementing the chat functionality in NestJS In the Node.js world, there are two major solutions to implementing WebSockets. The first of them is called was, and it uses bare WebSockets protocol. The other one is socket.io that provides more features through an additional abstraction. Currently, the implementation of socket.io for NestJS seems to be more popular than the implementation of ws. Therefore, in this article, we use socket.io.npm install @nestjs/websockets @nestjs/platform-socket.io @types/socket.io Currently, NestJS does not use the version 3.x of socket.io. Therefore, you need to use the version 2.x of the socket.io-client library on your frontend The first step in working with WebSockets in NestJS is creating a gateway. Its job is to receive and send messages. chat.gateway.ts import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server } from 'socket.io'; @WebSocketGateway() export class ChatGateway { @WebSocketServer() server: Server; @SubscribeMessage('send_message') listenForMessages(@MessageBody() data: string) { this.server.sockets.emit('receive_message', data); } }In this simple example above, we listen to any incoming send_message events. When that happens, we populate this message to all connected clients. Doing that already gives us a straightforward chat functionality. In the 24th part of this series, we’ve learned how to use a cluster to run multiple instances of our application. If you implement that approach, you might have trouble when using Socket.IO. To deal with it, you would have to use socket.io-redis, as explained in the official documentation. Authenticating users The first thing that we would want to add above is authentication. The most straightforward way of approaching it in our current architecture would be to get the authentication token from the cookies. If you want to know how we implemented the authentication with cookies, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies From the first paragraph of this article, we know that the initial handshake is a regular HTTP request. We can access it along with its headers. To parse the cookie, we use the cookie library.npm install cookie @types/cookie chat.service.ts import { Injectable } from '@nestjs/common'; import { AuthenticationService } from '../authentication/authentication.service'; import { Socket } from 'socket.io'; import { parse } from 'cookie'; import { WsException } from '@nestjs/websockets'; @Injectable() export class ChatService { constructor( private readonly authenticationService: AuthenticationService, ) { } async getUserFromSocket(socket: Socket) { const cookie = socket.handshake.headers.cookie; const { Authentication: authenticationToken } = parse(cookie); const user = await this.authenticationService.getUserFromAuthenticationToken(authenticationToken); if (!user) { throw new WsException('Invalid credentials.'); } return user; } }Above, we use the authenticationService.getUserFromAuthenticationToken method. Let’s implement it also. authentication.service.ts import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import TokenPayload from './tokenPayload.interface'; @Injectable() export class AuthenticationService { constructor( private readonly usersService: UsersService, private readonly jwtService: JwtService, private readonly configService: ConfigService ) {} public async getUserFromAuthenticationToken(token: string) { const payload: TokenPayload = this.jwtService.verify(token, { secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET') }); if (payload.userId) { return this.usersService.getById(payload.userId); } } // ... }To use the getUserFromSocket method, we need to provide it with the current socket. We can do that in the handleConnection method of our ChatGateway if it implements the OnGatewayConnection interface. chat.gateway.ts import { MessageBody, OnGatewayConnection, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; @WebSocketGateway() export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server: Server; constructor( private readonly chatService: ChatService ) { } async handleConnection(socket: Socket) { await this.chatService.getUserFromSocket(socket); } @SubscribeMessage('send_message') listenForMessages(@MessageBody() data: string) { this.server.sockets.emit('receive_message', data); } }We can also use the above to authenticate users when they post messages. To do that, let’s modify our listenForMessages method. chat.gateway.ts import { ConnectedSocket, MessageBody, OnGatewayConnection, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; @WebSocketGateway() export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server: Server; constructor( private readonly chatService: ChatService ) { } async handleConnection(socket: Socket) { await this.chatService.getUserFromSocket(socket); } @SubscribeMessage('send_message') async listenForMessages( @MessageBody() content: string, @ConnectedSocket() socket: Socket, ) { const author = await this.chatService.getUserFromSocket(socket); this.server.sockets.emit('receive_message', { content, author }); } }Now, our users receive both the content of the messages in the chat and the information about the author. Persisting the messages in the database So far, we’ve only forwarded incoming messages to all of the connected users. Any new users that join the conversation wouldn’t be able to view its history. To improve that, we need to save all of the messages in the database. message.entity.ts import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import User from '../users/user.entity'; @Entity() class Message { @PrimaryGeneratedColumn() public id: number; @Column() public content: string; @ManyToOne(() => User) public author: User; } export default Message;We also need to implement the logic of saving and retrieving messages. Let’s do that in our ChatService: chat.service.ts import { Injectable } from '@nestjs/common'; import { AuthenticationService } from '../authentication/authentication.service'; import { InjectRepository } from '@nestjs/typeorm'; import Message from './message.entity'; import User from '../users/user.entity'; import { Repository } from 'typeorm'; @Injectable() export class ChatService { constructor( private readonly authenticationService: AuthenticationService, @InjectRepository(Message) private messagesRepository: Repository, ) { } async saveMessage(content: string, author: User) { const newMessage = await this.messagesRepository.create({ content, author }); await this.messagesRepository.save(newMessage); return newMessage; } async getAllMessages() { return this.messagesRepository.find({ relations: ['author'] }); } // ... }The last thing is to use the above functionalities in our ChatGateway: chat.gateway.ts import { ConnectedSocket, MessageBody, OnGatewayConnection, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; @WebSocketGateway() export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server: Server; constructor( private readonly chatService: ChatService ) { } async handleConnection(socket: Socket) { await this.chatService.getUserFromSocket(socket); } @SubscribeMessage('send_message') async listenForMessages( @MessageBody() content: string, @ConnectedSocket() socket: Socket, ) { const

Real-time chat with WebSockets | NestJS.io