Websocket là một giao thức realtime được xây dựng trên giao thức HTTP và hiện nay đang được sử dụng tương đối rộng rãi. Điểm mạnh của nó là kế thừa được những gì HTTP có và chúng ta có thể tái sử dụng được các thư viện http mà chúng ta đã sử dụng, ví dụ như tomcat. Trong bài này Dũng sẽ cùng các bạn đi tìm hiểu về spring websocket nhé.

Khởi tạo module

Chúng ta sẽ khởi tạo module có tên websocket.

Cấu hình dự án

Chúng ta sẽ cần cập nhật tập tin websocket/pom.xml như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>vn.techmaster</groupId>
        <artifactId>mastering-spring-boot</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>websocket</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-websocket</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.websocket</groupId>
            <artifactId>jakarta.websocket-api</artifactId>
            <version>${jakarta.websocket.version}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.websocket</groupId>
            <artifactId>jakarta.websocket-client-api</artifactId>
            <version>${jakarta.websocket.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>
</project>

Ngoài các thư viện chúng ta đã từng đề cập ở các bài trước đó thì trong bài này chúng có sự xuất hiện của các thư viện mới sau:

  1. Spring-websocket: Thư viện cầu nối giữa spring và thư viện cài đặt websocket server thực tế.
  2. Spring-messaging: Một thư viện cung cấp các lớp liên quan đến message.
  3. Tomcat-embed-websocket: Thư viện cung cấp cài đặt websocket thật sự.
  4. jakarta.websocket-api: Cung cấp các interface tiêu chuẩn cho websocket server.
  5. jakarta.websocket-client-api: Cung cấp các interface tiêu chuẩn cho websocket client.
  6. Jackson-databind: Cung cấp các tính năng liên quan đến việc ánh xạ giữa các lớp java thuần và json.

Khởi tạo các lớp cấu hình

Như đã nói thì websocket được xây dựng dựa trên HTTP nên chúng ta cũng sẽ tái sử dụng được các lớp cấu hình của spring-mvc. Đầu tiên là lớp WebAppInitializer, mã nguồn của nó như sau:

package vn.techmaster.websocket.config;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context =
            new AnnotationConfigWebApplicationContext();
        context.setConfigLocation("vn.techmaster.websocket");
        ServletRegistration.Dynamic dispatcher = servletContext
            .addServlet("dispatcher", new DispatcherServlet(context));
        dispatcher.setLoadOnStartup(1);
        dispatcher.setAsyncSupported(true);
        dispatcher.addMapping("/");
    }
}

Tiếp theo chúng ta sao chép lớp WebConfig từ webmvc sang:

package vn.techmaster.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "vn.techmaster.websocket")
public class WebConfig {

    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/templates/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

Sau đó chúng ta có cài đặt mã nguồn WebSocketConfig để cấu hình websocket server:

package vn.techmaster.websocket.config;

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

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }
}

Chúng ta sẽ tạo một uri cho websocket có tên gs-guide-websocket, ngoài ra ở đây có những từ ngữ mới mà mình muốn giải thích với các bạn:

  • Stomp: Là viết tắt Simple (or Streaming) Text Orientated Messaging Protocol, một giao thức gửi nhận đơn giản. Bạn có thể tìm hiểu thêm về nó tại đây.
  • SockJS: Là một thư viện javascript cung cấp các API chung để chúng ta có thể sử dụng mà không cần quan tâm đến các trình duyệt cụ thể. Hàm withSockJS sẽ giúp chúng ta đồng bộ việc sử dụng API với SocketJS phía client.

Cài đặt các lớp controller

Đầu tiên chúng ta sẽ cài đặt lớp GreetingController để nhận và phản hồi yêu cẩu của người dùng thông qua websocket:

package vn.techmaster.websocket.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import vn.techmaster.websocket.model.Greeting;
import vn.techmaster.websocket.model.HelloMessage;

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) {
        return new Greeting("Hello, " + message.getName() + "!");
    }
}

ở đây chúng ta sẽ nhận yêu cầu của người dùng từ kênh /app/hello và sau đó phản hồi cho người dùng vào kênh /topic/greetings.
Mã nguồn của các lớp Greeting và HelloMessage lần lượt là:

package vn.techmaster.websocket.model;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class Greeting {
    private String content;
}
package vn.techmaster.websocket.model;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class HelloMessage {
    private String name;
}

Tiếp theo chúng ta sẽ tạo lớp HomeController để trả về trang chủ với mã nguồn như sau:

package vn.techmaster.websocket.controller;

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/")
@AllArgsConstructor
public class HomeController {

    @GetMapping
    public ModelAndView home() {
        return new ModelAndView("home");
    }
}

Mã nguồn jsp của trang chủ sẽ như sau:

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <script>
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
            document.getElementById('response').innerHTML = '';
        }

        function connect() {
            var socket = new SockJS('/gs-guide-websocket');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/greetings', function (greeting) {
                    showGreeting(JSON.parse(greeting.body).content);
                });
            });
        }

        function disconnect() {
            if (stompClient !== null) {
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }

        function sendName() {
            var name = document.getElementById('name').value;
            stompClient.send("/app/hello", {}, JSON.stringify({'name': name}));
        }

        function showGreeting(message) {
            var response = document.getElementById('response');
            var p = document.createElement('p');
            p.appendChild(document.createTextNode(message));
            response.appendChild(p);
        }
    </script>
</head>
<body>
    <div>
        <button id="connect" onclick="connect();">Connect</button>
        <button id="disconnect" onclick="disconnect();" disabled="disabled">Disconnect</button>
    </div>
    <div id="conversationDiv">
        <input type="text" id="name" />
        <button onclick="sendName();">Send</button>
        <p id="response"></p>
    </div>
</body>
</html>

Ở đây chúng ta đang khởi tạo một giao diện kết nối websocket đơn giản. Chúng ta cũng sử dụng các thư viện javascript là stomp và sockjs tương ứng với mã nguồn server để đồng bộ về mặt giao thức và API.

Khởi chạy chương trình

Để có thể chạy chương trình chúng ta sẽ tạo ra lớp WebsocketStartUp với mã nguồn như sau:

package vn.techmaster.websocket;

import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;

import java.io.File;

public class WebsocketStartUp {

    public static void main(String[] args) throws Exception {
        String webappDirLocation = "src/main/resources/";
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);
        tomcat.addWebapp(
            "/",
            new File(webappDirLocation).getAbsolutePath()
        );
        tomcat.start();
        System.out.println("server started");
        tomcat.getServer().await();
    }
}

Chúng ta vẫn sẽ sử dụng cổng 8080, và lần này cổng này được sử dụng cho cả http và websocket.
Khởi chạy chương trình chúng ta sẽ nhận được giao diện như sau khi truy cập vào http://localhost:8080/:

Nhấn connect sau đó thử gửi một tin nhắn chúng ta sẽ nhận được kết quả:

Tổng kết

Như vậy chúng ta đã cùng nhau:

  1. Tìm hiểu về spring websocket.
  2. Khởi tạo, cầu hình và cài đặt mã nguồn cho một dự án sử dụng spring websocket với tomcat.
  3. Khơi chạy và gửi nhận tin nhắn qua websocket.

Cám ơn bạn đã quan tâm đến bài viết|video. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a