Implementation of instant messaging between server and client based on Netty's WebSocket protocol and its problems

1, Introduction

Technology stack:
Java: 1.8
SpringBoot: 2.2.2.RELEASE
Netty: 4.1.32.Final

For basic understanding, refer to the following article: https://blog.csdn.net/DreamsArchitects/article/details/120177336

Current time: 17:02:38, December 31, 2021
As the last year of 2021, blog.

2, Question

2.1 start server mode

Previously, the server was started directly by using the main method:

After combining the SpringBoot project, it should be started in the main method of the main startup class:

It's OK to start in this way, but because this class is new, it can't be managed by the Spring container.
In subsequent business logic processing,

  1. The @ Value annotation cannot be used to get the Value of the configuration file
  2. Objects injected with @ Autowired will report null pointer exceptions, such as null pointer exceptions when calling with RestTemplate object.

Solution: use ConfigurableListableBeanFactory (Spring application context) to obtain the object (provided that the class needs to be annotated with @ Component and managed by Spring).

(the SpringUtils tool class is attached in the appendix)

Also note the previous wording:

improvement:

2.2 storage of client connection objects

Whenever a client connects, a ChannelHandlerContext object will be generated to store the connection information,
This object is used for instant communication between client and server. Therefore, you need to store the ChannelHandlerContext object for each connection.
resolvent:

Store in Map mode:

public  static Map<String, ChannelHandlerContext> map = new HashMap<String, ChannelHandlerContext>();


//You can use CTX channel(). The ID () method obtains the socket ID of each connection as the Key of the Map
String id = ctx.channel().id().toString();

When the client connects, store the ChannelHandlerContext object:

When the client disconnects, remove the ChannelHandlerContext object in the map:

2.3 the server actively communicates with the client

Premise: the client and server have established a socket connection.

The server can communicate with the client through the ChannelHandlerContext object.

 ctx.channel().write(new TextWebSocketFrame("Client message: Hello!"));
 ctx.flush();

2.4 @Sharable annotation

Problems encountered during reconnection of netty client: each time the client connects, a channel will be re established.

@Sharable is used to indicate whether the ChannelHandler can be directly shared and used in multiple channels. The same instance corresponding to the annotated ChannelHandler can be added to one or more ChannelPipelines one or more times without competition conditions.

3, Realize

channelActive() connection establishment
channelInactive() disconnected
The channelRead0 () method receives the client message and performs business processing
Exception occurred in exceptionCaught()

 /**
     * Called when the client and server create a link
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String id = ctx.channel().id().toString();
        WebSocketServer.map.put(id,ctx);
        log.info("Client connected--->id:{}",id);
    }
 /**
     * Called when the client is disconnected from the server
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //Remove map socket connection id
        String socketId = ctx.channel().id().toString();
        WebSocketServer.map.remove(socketId);
        log.info("Client disconnected, socket ID : {} ",socketId);

    }

 /**
     * Receive client messages
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        String id = ctx.channel().id().toString();
        log.info("Client send message--->id:{}",id);

        if (WebSocketServer.map.get(id) != null && WebSocketServer.map.get(id).equals(ctx)) {

        } else {
            //If there is no such ctx in the map, the connection will be stored in the map
            WebSocketServer.map.put(id, ctx);
        }
        //Traditional HTTP access: the first handshake message is carried by the HTTP protocol, so it is an HTTP message. Execute the handlehtttprequest method to process the WebSocket handshake request
        if (msg instanceof FullHttpRequest){
            log.info("handle WebSocket Handshake request");
            handleHttpRequest(ctx,(FullHttpRequest) msg);
        }else if (msg instanceof WebSocketFrame){
            //WebSocket access: operation after successful link establishment
            log.info("WebSocket Access,Operation after successful link establishment");
            handleWebSocketFrame(ctx,(WebSocketFrame) msg);
        }

    }

4, Appendix

4.1 pom file

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

    <groupId>com.haobai</groupId>
    <artifactId>besttone-newmsg-5g</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--mysql drive-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--JPA Persistence layer framework-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.32.Final</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.9</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--Skip test-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 SpringUtils

package com.haobai.utils;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

/**
 * spring The utility class facilitates obtaining bean s in a non spring managed environment
 * 
 * @author ruoyi
 */
@Component
public final class SpringUtils implements BeanFactoryPostProcessor
{
    /** Spring Application context */
    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    /**
     * Get object
     *
     * @param name
     * @return Object An instance of a bean registered with the given name
     * @throws org.springframework.beans.BeansException
     *
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    /**
     * Gets an object of type requiredType
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     *
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }

    /**
     * Returns true if the BeanFactory contains a bean definition that matches the given name
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return beanFactory.containsBean(name);
    }

    /**
     * Determine whether the bean definition registered with the given name is a singleton or a prototype. If the bean definition corresponding to the given name is not found, an exception (NoSuchBeanDefinitionException) will be thrown
     *
     * @param name
     * @return boolean
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.isSingleton(name);
    }

    /**
     * @param name
     * @return Class Type of registered object
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getType(name);
    }

    /**
     * If the given bean name has aliases in the bean definition, these aliases are returned
     *
     * @param name
     * @return
     * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
     *
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }

    /**
     * Get aop proxy object
     * 
     * @param invoker
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAopProxy(T invoker) {
        return (T) AopContext.currentProxy();
    }
}

Keywords: Java Netty Spring Boot websocket

Added by jodie on Sun, 02 Jan 2022 13:47:06 +0200