WebSocket+Netty 1:1 instant messaging tool imitating wechat WebSocket+Netty 1:1 instant messaging tool imitating wechat

Posted by gaza165 on Tue, 28 Dec 2021 03:22:18 +0100

The technical foundation has been written before. For the through train, see the connection below Back end: WebSocket+Netty+SpringBoot+SpringMVC+SpringData+Mysql Middleware and third-party services: RabbitMQ+Redis + alicloud SMS + OSS object storage system + Nginx Netty is briefly introduced and its model basis The positioning of websocket and its difference from other connections Mass communication demo of Netty+Websocket front end: html5+vue + some UI links. You can see my previous front-end topics Specialized in vue Basics It's all right in the future. Learn more. It's convenient to make small toys by yourself in the future

Some functions currently implemented:

  • Mobile phone number login registration and password modification
  • Add friends (including some empty accounts, which is the judgment of friends)
  • Delete friends (including clearing friends and chat records)
  • Friend request approval
  • Friend details display
  • Message unread reminder
  • Heartbeat mechanism and read-write timeout
  • Data modification and avatar upload
  • Complaint feedback

In fact, the above is only a general function. In order to optimize the user experience, the project has done a lot of details For example, when users are required to delete friends, both their own list and each other's list should be deleted directly (similar to the timeliness of QQ deleting friends). Friends' requests are required to be sent here, and the other's friends request the list to respond immediately and display the quantity in real time, etc

The notes are very detailed. I hope they can help you. Let's see the effect picture

Login registration

Overall effect drawing

Click on your avatar to display information

Click the right side of the user's name or avatar to pop up the display details and friend operations

Click the extended function display of the navigation menu

When modifying personal information, there are a lot of information that can be modified. The section is long, and only part is displayed

picture upload

Click friend request, and the friend request display bar will pop up on the left

Message unread reminder

Another is full duplex, even if chatting, instant messaging is the same as our normal chat. It's not easy to show. Let's make up for it by ourselves Or contact me and I'll show you the test account

Let's talk about the code design

Server

server setting

Including the design of master-slave thread pool, server port and callback method (that is, the method provided for the following listeners) Of course, a channel initializer should be specified here, which will be described later

@Component
public class WebSocketServer {
    private EventLoopGroup bossGroup;       // Main process pool
    private EventLoopGroup workerGroup;     // Worker pool
    private ServerBootstrap server;         // The server
    private ChannelFuture future;           // Callback

    public void start() {
        future = server.bind(9090);
        System.out.println("netty server - Start successful");
    }

    public WebSocketServer() {
        bossGroup = new NioEventLoopGroup(); //Main process pool
        workerGroup = new NioEventLoopGroup();//From thread pool

        //Create Netty server startup object
        server = new ServerBootstrap();

        server.group(bossGroup, workerGroup)//Specify and configure the master-slave thread pool for the netty server
                .channel(NioServerSocketChannel.class)//Specifies the netty channel type
                //Specifies that the channel initializer is used to load when the channel receives a message
                //How to process business
                .childHandler(new WebSocketChannelInitializer());
    }


}
Channel initializer
/**
 * Function Description: channel initializer
 * Used to load the channel handler
 * @Author: Zyh
 * @Date: 2020/1/22 20:31
 */
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    //Initialize channel
    //Load the corresponding ChannelHandler in this method
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        /* Fixed writing part*/
        //Get the pipeline and add one ChannelHandler to the pipeline
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        //channelpipeline can be understood as an interceptor
        //When our socketChannel data comes in, we will call our ChannelHandler in turn

        //Add an http codec
        channelPipeline.addLast(new HttpServerCodec());
        //Add big data flow support
        channelPipeline.addLast(new ChunkedWriteHandler());
        //Add aggregators to aggregate our httpmaessage into fullhttprequest / response - add aggregators if you want to get requests and responses
        channelPipeline.addLast(new HttpObjectAggregator(1024*24));//Specify cache size
        /* Fixed writing part*/

        //Specifies the route to receive the request
        //Specifies that a url ending in ws must be used to access
        channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        //Add a custom handler for business processing
        channelPipeline.addLast(new ChatHandler());


    }
}
Application listener, so that our netty starts when the spring container is loaded
@Component
public class NettyListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private WebSocketServer websocketServer;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if(event.getApplicationContext().getParent() == null) {
            try {
                websocketServer.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

In addition, it should be noted here that after using Spring and spring mvc, the system will have two contexts, ApplicationContext and webapplicationcontext. In the web project (spring mvc), the system will have two containers, one is the root application context, and the other is our own ProjectName servlet context (as a sub container of the root application context).

In this case, the onApplicationEvent method will be executed twice. To avoid the problems mentioned above, we can call the logic code only after the initialization of root application context. The initialization of the other containers is done without any processing and modification.

As follows:

 @Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    if(event.getApplicationContext().getParent() == null) {
        try {
            websocketServer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

There may be some situations in which the above judgment will be executed many times (I'm normal, so I don't know what the problem is). Some predecessors found that using the following judgment is more accurate: event.getApplicationContext().getDisplayName().equals("Root WebApplicationContext")

About monitoring reference https://www.cnblogs.com/a757956132/p/5039438.html https://www.iteye.com/blog/zhaoshijie-1974682

Continue with the channel initializer method of netty

Including adding codecs, aggregators (to get requests and responses), and data flow support The most important thing is to obtain the pipeline (after the client comes, there will be a pipeline from the client to Netty, and its importance can be imagined) and define the method of processing the pipeline Define the route to receive the request

/**
 * Function Description: channel initializer
 * Used to load the channel handler
 * @Author: Zyh
 * @Date: 2020/1/22 20:31
 */
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    //Initialize channel
    //Load the corresponding ChannelHandler in this method
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        /* Fixed writing part*/
        //Get the pipeline and add one ChannelHandler to the pipeline
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        //channelpipeline can be understood as an interceptor
        //When our socketChannel data comes in, we will call our ChannelHandler in turn

        //Add an http codec
        channelPipeline.addLast(new HttpServerCodec());
        //Add big data flow support
        channelPipeline.addLast(new ChunkedWriteHandler());
        //Add aggregators to aggregate our httpmaessage into fullhttprequest / response - add aggregators if you want to get requests and responses
        channelPipeline.addLast(new HttpObjectAggregator(1024*24));//Specify cache size
        /* Fixed writing part*/

        //Specifies the route to receive the request
        //Specifies that a url ending in ws must be used to access
        channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        //Add a custom handler for business processing
        channelPipeline.addLast(new ChatHandler());


    }
}

Communication protocol and message broker

@Configuration
@EnableWebSocketMessageBroker//@The enable websocketmessage broker annotation indicates that it is enabled to use the STOMP protocol to transmit agent-based messages. Broker means agent.
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        //2. The registerstampendpoints method represents the node that registers the STOMP protocol and specifies the mapped URL.

        //3.stompEndpointRegistry.addEndpoint("/endpointSang").withSockJS(); This line of code is used to register the STOMP protocol node and specify the use of SockJS protocol.
        stompEndpointRegistry.addEndpoint("/ws/endpointChat").withSockJS();

    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //4. The configuremessagebroker method is used to configure the message broker. Since we implement the push function, the message broker here is / topic
        registry.enableSimpleBroker("/queue","/topic");
        //Here I do not use the native websocket protocol, but use the stopp sub protocol of websocket, which is more convenient.
        // The message broker uses both / queue and / topic, mainly because I have both point-to-point single chat (queue) and group chat (topic) for sending system messages.
    }
}

Message body

First of all There are many kinds of messages from our websocket, including simple connection establishment, private message function and message storage function that need to forward messages, disconnection and so on

public class Message {
    //Message type -- 0 establish connection 1 send message
    private Integer type;
    private Record record;//Chat message
    private Object ext;//Extended type message

    public Integer getType() {
        return type;
    }

    public void setType(Integer type) {
        this.type = type;
    }

    public Record getRecord() {
        return record;
    }

    public void setRecord(Record record) {
        this.record = record;
    }

    public Object getExt() {
        return ext;
    }

    public void setExt(Object ext) {
        this.ext = ext;
    }

    @Override
    public String toString() {
        return "Message{" +
                "type='" + type + '\'' +
                ", record=" + record.toString() +
                ", ext=" + ext +
                '}';
    }
}

Mapping between users and pipes

Including establishing connection mapping relationship Dissolution Print mapping table And conditional search

import io.netty.channel.Channel;//neety channel, don't make a mistake

/**
 * Function Description: establish the mapping between user and Channel
 * @Param: 
 * @Return: 
 * @Author: Zyh
 * @Date: 2020/2/4 22:22
 */
public class UserChannelMap {
    //Mapping between user id and Channel
    private static Map<String, Channel> userchannelMap;

    //Static code block initialization mapping table
    static {
        userchannelMap=new HashMap<String, Channel>();
    }
    
    /**
     * Function Description: add the association between user id and channel
     * @Param: [userid, channel]
     * @Return: void
     * @Author: Zyh
     * @Date: 2020/2/4 22:27
     */
    public  static void put(String userid,Channel channel){
        userchannelMap.put(userid,channel);
    }
    /**
     * Function Description: remove the association between user and channel
     * @Param: [userid]
     * @Return: void
     * @Author: Zyh
     * @Date: 2020/2/4 22:28
     */
    public static void  remove(String userid){
        userchannelMap.remove(userid);
    }

    /**
     * Function Description: print mapping table - mapping table of user id and channel id
     * @Param: []
     * @Return: void
     * @Author: Zyh
     * @Date: 2020/2/4 22:31
     */
    public  static void printMap(){
        System.out.println("The mapping between the current surviving user and the channel is:");
         for (String userid: userchannelMap.keySet()){
            System.out.println("user id: "+userid+" passageway: "+userchannelMap.get(userid).id());
         }
    }

    /**
     * Function Description: disassociate the user from the channel according to the channel id
     * @Param: [channelid]
     * @Return: void
     * @Author: Zyh
     * @Date: 2020/2/7 1:35
     */
    public static void removeByChannelId(String channelid){
        //Pre judge and intercept empty channel s to prevent subsequent nullexp
        if (!StringUtils.isNotBlank(channelid)){
            return;
        }

        for (String userid: userchannelMap.keySet()){
           if (channelid.equals(userchannelMap.get(userid).id().asLongText())){
               System.out.println("Client disconnected,Cancel user:"+userid+"And channel:"+userchannelMap.get(userid).id()+"Relationship between");
               userchannelMap.remove(userid);
               break;
           }
        }
    }


    /**
     * Function Description: obtain the Channel according to the id
     * @Param: [friendid]
     * @Return: io.netty.channel.Channel
     * @Author: Zyh
     * @Date: 2020/2/7 18:57
     */
    public static Channel getChannelById(String friendid) {
        Channel channel = userchannelMap.get(friendid);
        return channel;
    }
}

Finally, the processor

//Extensions simplechannelinboundhandler < TextWebSocketFrame > enables us to encapsulate the received messages into a TextWebSocketFrame
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    //Used to save all client connections
    private static ChannelGroup clients=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    //Create a time generator
    private SimpleDateFormat sdf=new SimpleDateFormat("yyyy-mm-dd hh:MM");

    @Override //This aspect will be called automatically when data is received
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String text=msg.text();
        System.out.println("The received message body is: "+text);
        RecordService recordService=null;
        try{
            //Obtain the spring context container through the springUtil tool class,
            recordService = SpringUtil.getBean(RecordService.class);

        }catch (Exception e)
        {
            System.out.println("Container get exception");
            e.printStackTrace();
        }

        /*//Traverse clients (all clients, mass sending)
        for (Channel client:clients){
            //Send message and refresh channel
            client.writeAndFlush(new TextWebSocketFrame(sdf.format(new Date())+": "+text));
        }*/
        //Convert the incoming message into a json object
        Message message =null;
        try{
            message= JSON.parseObject(text, Message.class);

        }catch (Exception e)
        {
            System.out.println("message Get exception");
            e.printStackTrace();
        }

        switch (message.getType()){
            case 0:
                //Establish user channel Association
                String userid=message.getRecord().getUserid();
                //Stores the mapping between the user id and the channel
                UserChannelMap.put(userid,ctx.channel());
                System.out.println("Establish user id:"+userid+"And channels id:"+ctx.channel().id()+"Mapping between");
                break;
            case 1://type is send / receive message
                //1. Store chat messages
                Record record= message.getRecord();
                recordService.add(record);
                //2. The client sends and receives messages
                Channel friendChannel = UserChannelMap.getChannelById(record.getFriendid());
                if (friendChannel!=null){ //If the user is online, send it directly to friends
                    friendChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString( message)));
                }else {//If the user is not online, it will not be sent temporarily
                    System.out.println("user"+record.getFriendid()+"Not online");
                }
        }
    }

    @Override   //This method will be called automatically when a new client accesses the server
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        clients.add(ctx.channel());//Add a new connection to the channel
    }

    /**
     * Function Description: the original netty method is called when an exception occurs
     * Here, I set that when an exception occurs, we close the channel and contact the association between the user id and the channel in the map
     * @Param: [ctx, cause]
     * @Return: void
     * @Author: Zyh
     * @Date: 2020/2/7 1:45
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
             System.out.println("An exception occurred"+cause.toString());
             System.out.println("An exception occurred"+cause.getStackTrace());
             UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
             ctx.channel().close();
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {  
             System.out.println("Close channel");
             UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
             UserChannelMap.printMap();
    }
}

In addition, when we integrate netty and springboot, we need to get spring bean s

After netty receives the message from the client, we need to store and store the chat records, but our netty server cannot directly get some components defined by us, such as controller and service. If they are managed by the spring container, it is OK. However, new is used in some parts of my code and not managed by the spring IOC, so I have made a tool static member class here, Get the spring context object during initialization, and define some methods to get the bean

/**
 * @Description: Provides manual access to bean objects managed by spring
 */
@Component
public class SpringUtil implements ApplicationContextAware {
    
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    // Get applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    // Get Bean. By name
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    // Get Bean. class
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    // Return the specified Bean through name and Clazz
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}
The business layer is spring MVC + spring data. The code is cumbersome. We won't bother to fill in here
In fact, these are not difficult. The front-end js is more troublesome

(if you change a professional cow, it will be better than my design). My front end is always like a hot head. If I need this, I will get it once

The chat interface refreshes 34 requests 78ms at a time,

There is hardly any waiting, and there is no delay in real-time message communication. It seems good. However, with too many friends and too frequent messages, there are still many optimization designs to be solved. Later, I will see what nginx is. If the back-end is used, some security problems may be involved, which have not been considered and protected, and there is still a lot of room for progress

Think about it. It seems that a set of source code is not expensive. (the illusion is coming. I'm not the ownership of the purchase, but the right to use it!), Sure enough, knowledge is power. Repeat it three times. Knowledge is power. Knowledge is power. Knowledge is power. Study hard and make progress every day

After saying so much, in fact, the back-end does not involve details

In terms of some processing, the current end needs to consider a lot when it is done by itself, including the setting and storage of each parameter of each data, and even the time of its generation and expiration, that is, the life cycle. Here again, thank some vue for its two-way binding operation, clear life cycle and its perfect hook function, Let me, the front-end Xiaobai, also do a full stack