Public account: MarkerHub (pay attention to get more project resources)

Eblog codebase: github.com/markerhub/e…

Eblog project video: www.bilibili.com/video/BV1ri…


Development Document Catalog:

(Eblog) 1. Set up the project architecture and initialized the home page

(EBLOG) 2. Integrate Redis and project elegant exception handling and return result encapsulation

(eblog) 3, using Redis zset ordered set to achieve a hot feature of the week

(EBLOG) 4, customize Freemaker label to achieve blog homepage data filling

(Eblog) 5, blog classification filling, login registration logic

(EBLOG) 6. Set up blog publishing collection and user center

(eblog) 7, message asynchronous notification, details adjustment

8. Blog search engine development and background selection

(Eblog) 9. Instant group chat development, chat history, etc


Front and back end separation project vueblog please click here: super detailed! 4 hours to develop a SpringBoot+ Vue front and back separated blog project!!


Group chat development

Today we will complete the function of a chat room. In the course, we talked about an example of springLayIM, which also integrates Layim into the Web version of chat. This time we are not so complicated, we will mainly learn the process of front and back interaction.

Technical selection:

  • Front-end Layim and Websocket

  • Backend T-IO WebSOCEkt version

First, let’s get Layim’s interface up and running. Layim is a paid module for Layui. First we put the static resource pack of Layui into static. As for Layim, since it is not fully open source, I will not give the specific package.

Layim website

  • www.layui.com/layim/

The introduction of layIM

First of all, the layim module plug-in is introduced. This plug-in is not open source, and online use needs to be donated. If you need to use it in business, you’d better donate.

Then according to the example given on the official website, let’s make the simplest Hello Word. Before that, we need to get the plugin first. In principle, we should get it through donation, but only for learning, so we directly search for a plugin from the Internet, and the relevant JS is as follows:

Then we look at the official documentation: www.layui.com/doc/modules…

Where should we put this section of JS? I think the effect is like this. There is a group chat button at the bottom of the home page. So in order for all pages to be able to chat, I put JS in the global template

  • templates/inc/layout.ftl
<script type="application/javascript">
    $(function () {
        layui.use('layim'.function(layim){// Start layim.config({brief:true// Whether parsimonious mode (iftrueDo not display the main panel),min:true
            }).chat({
                name: 'Customer Service sister'
                ,type: 'friend'
                ,avatar: 'http://tp1.sinaimg.cn/5619439268/180/40030060651/1'
                ,id: -2
            });
            layim.setChatMin(a); // Shrink the chat panel}); }); </script>Copy the code

Config for initial configuration, brief: true for simple mode with only one chat window,. Chat for declaring and opening a chat window, layim.setchatmin (); Indicates shrinking the chat panel.

Click the result:

Ok, the above is our simplest chat window, which can be displayed, but it has no function yet, so it can not chat with each other. Next, we will give each window an identity and then chat with each other.

T – IO integrated websocket

After we introduced Layim above, we can see a chat window. Let’s look at what we’re trying to accomplish:

demand

  • Achieve a non-differentiated group chat function

  • Users who log on to the site can start group chats

  • Users who are not logged in can chat anonymously

function

  • Chat messages are sent in groups

  • Historical message record

  • Anonymous chat

So let’s do it step by step.

First, we integrate t-IO and Websocket on the back end. Review the content of the T-IO course.

First t-IO uses helloWord:

(Initializing the server)

(Communication flow between client and server)

Then integrate the message logic processing to be implemented after T-IO:

Common class description:

From the above review, we know a few key classes, which are also the key classes for us to re-initialize the T-IO service. Let’s get t-IO up and running.

In t-io, the code we eventually call to start the service is: tioserver.start ();

Because the function we want to implement this time is t-IO integration webSocket. T-io has already integrated a set of code for us, eliminating protocol upgrades and so on, so that we don’t have to manually write a lot of protocol upgrades and so on like the examples in our course.

Can first feel the official website given the example of TIO-websocket-showcase:

  • Gitee.com/tywo45/tio-…

In this example we need code in the HTTP package, which is similar to springMVC usage, and T-IO also helps us write a set of MVC code. So how does this get started?

Many simple: Start the project by running the main method of ShowcaseWebsocketStarter, and then go to localhost to see the interface. Before you do your homework project, you can take a look at how the basic chat function is implemented. Then it will be much easier to go back and do our own projects.

Let’s move on to our assignment: websocket integration with T-IO. Because it is an integration framework directly, so I direct introduction of the latest version here: mvnrepository.com/artifact/or…

<! -- https://mvnrepository.com/artifact/org.t-io/tio-websocket-server --> <dependency> <groupId>org.t-io</groupId> < artifactId > tio - websocket server < / artifactId > < version > 3.2.5. V20190101 - RELEASE < / version > < / dependency >Copy the code

Then, with the project examples we just looked at, let’s start by explaining some of the more important classes

  • IWsMsgHandler (handshake, message handling classes)

  • This is the interface for message processing, including while handshake, after handshake, message processing, and so on

  • In the org. Tio. Websocket. Server. WsServerAioHandler calls, and this class implements the ServerAioHandler. There are three methods we are familiar with: decode, encode and handler.

  • WsServerStarter (WS service starter class)

  • For WS, TIO encapsulates many of the things involved to make configuration easier, as can be seen from its start method, which is actually the familiar tioServer.start.

  • ServerGroupContext (Configuration class)

  • This we are more familiar with, server configuration class, can configure heartbeat time and so on.

Those are the three classes we need to remove. With these three classes we can start our service and connect to WS.

So, let’s write a configuration class. com.homework.im.config.ImServerAutoConfig

You need to specify the port first, so add the code, and you’ll add the configuration on YML yourself.

@Value("${im.server.port}")
private Integer imPort;

Copy the code
  • application.yml
Im: server: IP: 127.0.0.1 Port: 9326Copy the code

Then let’s review what needs to be configured:

  • First, initialize IWsMsgHandler, ServerGroupContext, then configure to WsServerStarter, call start method to start the service.

  • The second step, because there are many message types (send message, heartbeat message, add friends, etc.), we also need to initialize the corresponding processing class container Map of the message type, so that we can directly find the corresponding message processing class call when processing the message according to the type.

All right, so once we have an idea we’re going to do it.

  • com.example.config.ImServerAutoConfig
@Slf4j
@Data
@Configuration
@Order(value = Integer.MAX_VALUE)
public class ImServerAutoConfig {
    @Value("${im.server.port}")
    private Integer imPort;
    @Bean
    public ImServerStarter imServerStarter() { try { ImServerStarter imServerStarter = new ImServerStarter(imPort); imServerStarter.start(); // Initialize the message handler project msghandlerFactory.init (); log.info("---------> im server started !");
            return imServerStarter;
        } catch (IOException e) {
            log.error("Failed to start im server ~~", e);
        }
        returnnull; }}Copy the code

IWsMsgHandler, ServerGroupContext, IWsMsgHandler, ServerGroupContext Then configure to WsServerStarter and so on. Then start() is the wsServerstarter.start () that calls tio after initialization. So that completes the first step. Let’s look at the code for the ImServerStarter class:

@Slf4j public class ImServerStarter { private ImWsMsgHandler imWsMsgHandler; private WsServerStarter wsServerStarter ; private ServerGroupContext serverGroupContext; public ImServerStarter(int imPort) throws IOException { imWsMsgHandler = new ImWsMsgHandler(); wsServerStarter = new WsServerStarter(imPort, imWsMsgHandler); serverGroupContext = wsServerStarter.getServerGroupContext(); serverGroupContext.setHeartbeatTimeout(1000 * 60); } public void start() throws IOException { this.wsServerStarter.start(); }}Copy the code

Looks very simple, I can adjust a few times to write out, ha ha. If you are not familiar with the usage of TIO, if you go back and review the content of our course, the usage of TIO is not difficult, there are only a few main classes, and the bottom layer has been encapsulated for us, so it is quite easy to use. Then look at the content of step 2:

// Initialize the message handler project msghandlerFactory.init ()Copy the code

So we’re going to initialize the message handler, and we said that messages can have different types, different processors, so we’re going to initialize it, put it in a static map, and then we’re going to call this factory method and get the handler from the map container.

  • com.example.im.handler.MsgHandlerFactory
Public class MsgHandlerFactory {private static Boolean isInit = public class MsgHandlerFactory {private static Boolean isInit = public class MsgHandlerFactory {private static Boolean isInit =false; private static Map<String, MsgHandler> handlerMap = new HashMap<>(); /** * preinitialize message handler */ public static voidinit() {if(isInit){ return; }
        handlerMap.put(Constant.IM_MESS_TYPE_CHAT, new ChatMsgHandler());
        handlerMap.put(Constant.IM_MESS_TYPE_PING, new PingMsgHandler());
        isInit = true;
    }
    public static MsgHandler getMsgHandler(String type) {
        return handlerMap.get(type); }}Copy the code

ChatMsgHandler (String type), PingMsgHandler (String type), PingMsgHandler (String type), PingMsgHandler (String type) The MsgHandler interface has a method handler, so all implementation classes implement this method (that is, the message handling logic). The processing logic will be discussed later. After the above steps, we can initialize the service that starts tio, and we have the message handler. The written task is to integrate the front-end WS with the back-end and process the corresponding messages (most of the rest is developed around ImWsMsgHandler).

Realize front-end and back-end joint tuning

1. Establish WS connection at the front and back ends

On the back end we use tio, the WS version, and on the front end we use WS to establish connections. To review the webSocket lessons we talked about earlier, establishing a WS connection at the front end is simple:

var
 socket 
=
 
new
 
WebSocket
(
'ws://localhost:9326'
);
Copy the code

Onmessage, socket.onclose, socket.onmessage, socket.onclose, etc. We have already written a simple example of Layim rendering the chat window. After the initial configuration of layim.config, we set up the WS connection. I need to adjust the structure of JS to make IT more in line with our Ideas of Java because I will also design logic processing such as heartbeat, disconnect and reconnect.

I created a new tio class, the tio.ws inner class, in the js file im.js. = {} means that this defines an object.

if (typeof(tio) == "undefined") {
    tio = {};
}
tio.ws = {};

Copy the code

Then we write methods in tio.ws, and create a new tio.ws where we need it later, and then call the corresponding method. Doesn’t fit into the idea of facing the object. Ha ha

// this is equivalent to the constructor ti.ws =function ($, layim) {
}

Copy the code

There are a couple of ways we can do this

  • Establish connection methods while listening for WS message acceptance, closure, exceptions, and so on

  • Heartbeat, disconnect and reconnect

  • Send a message

  • Initialize chat window data (such as window title, avatar, etc., offline chat history)

Let’s go through them all, but let’s start with making connections:

  • static/js/im.js

The logic actually revolves around several methods of WS. Layim. getMessage is layim’s interface, which lets us display the dialog JSON information in the next window.

2, heartbeat and disconnect reconnect

Each time we send a message, we will record the time when the last message is sent. For the heartbeat, if the message is not sent for too long, the heartbeat means that the ws connection will not be destroyed by the server.

When the server fails, the front end will always try to reconnect automatically, calling onClose, so we just need to im the reconnect method onclose.

Then look at the logic of ping

Start a timer and automatically send heartbeat packets if no message is sent for a long time. Note that the heartbeat message type is pingMessage, so that the server can obtain the processing of the corresponding processor.

When A WS exception occurs, we need to delete this timer, and then start the heartbeat when reconnecting.

3. Send a message

Then let’s look at the method of sending messages

this.sendChatMessage = function(res) {this.socket.send(json.stringify ({type: 'chatMessage'Data: res})); }Copy the code

As you can see, you’re using the socket.send method. Note the message type chatMessage. Inside the RES are messages to be sent. When is it triggered? This involves layim’s interface. We’ll see.

4. Complete the initialization of the whole front-end process

Above we defined a number of methods in the tio.ws object, establishing connections, heartbeat, sending messages, and so on. So let’s go ahead and use this object.

  • static/js/index.js
layui.use('layim'.function(layim) { var $ = layui.jquery; // Initialize layim layim.config({brief:true// Whether parsimonious mode (iftrueThe main panel is not displayed),voice:false
        ,members: {
            url: '/chat/getMembers'
        },
        chatLog: layui.cache.dir + 'css/modules/layim/html/chatlog.html'}); Var tiows = new tio.ws($, layim); var tiows = new tio.ws($, layim); tiows.connect(); Tiows.inithistroymess (); // Open the group chat window and initialize the personal information tiows.openChatWindow(); // Send message layim.on('sendMessage'.function(res){
        tiows.sendChatMessage(res);
    });
});

Copy the code

In the above code, we initialize Layim, establish A WS connection, initialize group and personal messages, then open the group chat window, and finally listen to Layim’s send message callback method, which is triggered when to call the send message.

5. Data initialization

We also called the relevant interface to initialize the data above, as a sidebar. Layim has certain formatting requirements. You can take a look at the documentation:

  • www.layui.com/doc/modules…

Including personal information, temporary window information and so on.

this.initChatData = function () {
    $.ajax({
        url: '/chat/getMineAndGroupData',
        async: false,
        success: function(data) { mine = data.data.mine; group = data.data.group; }}); }Copy the code

There are two back-end interfaces involved:

  • Get user information and group chat information getMineAndGroupData

  • Get the group membership list getMembers

We need to install the format of the official document to return the corresponding JSON data, so I made some corresponding data encapsulation classes (VO), such as: ImUser, etc..

  • com.example.controller.ChatController
@RestController
@RequestMapping("/chat")
public class ChatController extends BaseController {
    @Autowired
    ChatService chatService;
    @GetMapping("/getMineAndGroupData") public Result getMineAndGroupData (it request) {/ / get the user information ImUser user. = chatService getCurrentImUser ();  // Default group Map<String, Object> group = new HashMap<>(); group.put("name"."Community Group Chat");
        group.put("type"."group");
        group.put("avatar"."http://tp1.sinaimg.cn/5619439268/180/40030060651/1");
        group.put("id", Constant.IM_DEFAULT_GROUP_ID);
        group.put("members", 0);
        return Result.succ(MapUtil.builder()
                .put("mine", user)
                .put("group", group) .map()); }}Copy the code

Here the ImUser user = chatService. GetCurrentImUser (); It is to get the current user information, divided into two cases, logged in and not logged in, so I made a distinction, anonymous user gave a random ID, and then saved in the session, so that the anonymous session will remain the ID until the window closed.

  • com.example.service.impl.ChatServiceImpl
@Override
public ImUser getCurrentImUser() {
    AccountProfile profile = (AccountProfile)SecurityUtils.getSubject().getPrincipal();
    ImUser user = new ImUser();
    if(profile ! = null) { user.setId(profile.getId()); user.setAvatar(profile.getAvatar()); user.setUsername(profile.getUsername()); user.setMine(true);
        user.setStatus(ImUser.ONLINE_STATUS);
    } else {
        user.setAvatar("http://tp1.sinaimg.cn/5619439268/180/40030060651/1"); Long imUserId = (Long) securityutils.getSubject ().getSession().getAttribute()"imUserId"); user.setId(imUserId ! = null ? imUserId : RandomUtil.randomLong()); SecurityUtils.getSubject().getSession().setAttribute("imUserId", user.getId());
        user.setSign("never give up!");
        user.setUsername(Anonymous User);
        user.setStatus(ImUser.ONLINE_STATUS);
    }
    return user;
}

Copy the code

And then there’s an interface to get online users which is basically a method: ChatService. FindAllOnlineMembers (), users and online upgrade handshake complete agreement, we will keep the current user’s information to the redis, so this method, we just can be obtained from the redis, we’ll talk about at the back. The above is the process of front-end and back-end joint adjustment. Start the server, open the home page, you can see the chat window, F12 see ws link has been established. After sending a message, we in the com. Homework. Im. Server ImWsMsgHandler# onText method can be received in the message.

Let’s expand on receiving the message and processing it

Back-end message processing

During initialization tio service said, when we are wait a message processing, shake hands around com. Example. Im. Server ImWsMsgHandler this class. There are a few ways to be aware of this:

  • Shaking hands before the handshake

  • After the handshake onAfterHandshaked

  • Character messages handle onText

What we are going to do now is a group chat function. Let’s sort out the requirements:

  • After the user logs in successfully, we bind the user ID to the TIO channel and remind all users that the user is online

  • When a user sends a message, all users can receive it

Ok, let’s see, the function of binding the user ID to TIO can remind all users before or after the handshake.

After receiving a message sent by a user, we will find the corresponding processor and then send it to all users in a group after processing it

Ok, done.

Let’s look at the code:

  • Before shaking hands

  • After shaking hands, there is still a problem, the chat window does not display system messages, there is a bug…

  • Message processing

About message types:

Sending a message is a chatMessage.

We found the processor:

  • com.example.im.handler.impl.ChatMsgHandler
@Slf4j
@Component
public class ChatMsgHandler implements MsgHandler {
    @Override
    public void handler(String data, WsRequest wsRequest, ChannelContext channelContext) {
        ChatInMess chatMess = JSONUtil.toBean(data, ChatInMess.class);
        log.info("-- -- -- -- -- -- -- -- -- -- -- -- -- - > {}", chatMess.toString());
        ImUser mine = chatMess.getMine();
        ImTo to = chatMess.getTo();
        ImMess responseMess = new ImMess();
        responseMess.setContent(mine.getContent());
        responseMess.setAvatar(mine.getAvatar());
        responseMess.setMine(false); Responsemess.setusername (mine.getusername ()); responseMess.setFromid(mine.getId()); responseMess.setTimestamp(new Date()); responseMess.setType(to.getType()); responseMess.setId(Constant.IM_DEFAULT_GROUP_ID); // Group ID ChatOutMess ChatOutMess = new ChatOutMess(constant. IM_MESS_TYPE_CHAT, responseMess); String responseData = JSONUtil.toJsonStr(chatOutMess); log.info("Group messaging =========> {}", responseData); WsResponse WsResponse = wsResponse.fromtext (responseData,"utf-8"); ChannelContextFilter filter = new ChannelContextFilterImpl(); ((ChannelContextFilterImpl) filter).setCurrentContext(channelContext); / / mass Tio. SendToGroup (channelContext. GroupContext, Constant IM_GROUP_NAME, wsResponse, filter); ChatService = (ChatService) springutil. getBean("chatService"); chatService.setGroupHistoryMsg(responseMess); }}Copy the code

In fact, it is simple, right? It is nothing more than data piecing together. Note that this has a channel filter, which means that messages sent by the user will not be sent to the user, but will be displayed directly in the front end, so we need to write a filter:

  • com.example.im.handler.filter.ChannelContextFilterImpl
/** * @data public class ChannelContextFilterImpl implements ChannelContextFilter {private ChannelContext currentContext; /** * filter itself, no need to send to yourself * @param channelContext * @return
     */
    @Override
    public boolean filter(ChannelContext channelContext) {
        if(currentContext.userid.equals(channelContext.userid)) {
            return false;
        }
        return true; }}Copy the code

This returns false if the channel userID is the same as the channel userID to be sent, meaning skip.

  • User exit

When the user exits, we need to close the channel, so that the group will not be sent to this inside, and at the same time, we can count the number of real-time online people. The code is simple: simply call Tio. Remove:

@Override
public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
    Tio.remove(channelContext, channelContext.userid + - Quit the group chat.);
    return null;
}

Copy the code

To summarize, we used several interfaces to TIO

  • Group sending interface tio. sendToGroup

  • BindUser interface tio.binduser

    • The user exits * tio.remove

Basically group chat we will only use the above API, the usage is not complicated. So let’s test that out. There’s a little bit of confusion here because of the user Id problem, but that’s ok, we just need to test the message sending and receiving group function first.

After running the project, open the link to get a feel for the chat room.

Gets group members and chat records

In the iteration of the last version, we have completed the basic communication between group members. In this version, we will complete the acquisition of group chat members and offline group information.

1. Get group members

Group member information acquisition is to monitor the online and offline users to determine whether members are online.

  • Online: Saves the member information to Redis

  • Offline: Deletes the member information from the cache

Let’s take a look at the data structure that Layim needs to return to get group members

  • Layim.layui.com/json/getMem…

As we know from the above, it is just adding the basic information of group members to the list. In fact, only ID, username and Avatar are required in the interface, and other elements are ok.

Now that we know the structure, let’s think about what the structure of this cache should be. So obviously this is a list, so you can use list, set, and to avoid repetition, set is better. When the three elements of ID, username and Avatar are exactly the same, it is regarded as the same element and will not be repeated. If the user changes their avatar or username, it will repeat, so we can’t let the list be affected by that, only the user’s ID won’t change, so in this list, we’ll just put the ID. However, if there is only ID, where can we obtain username and Avatar? We can use a hash structure to save the basic information of the user.

So to summarize our analysis above, we need to use two structures of Redis to complete this function

  • set

  • hash

Now let’s write code to do that.

First let’s write down the interface to get group members:

/** * all user information should be retrieved from the channel * @return
 */
@ResponseBody
@GetMapping("/getMembers")
public Result getMembers() {
    Set<Object> members = chatService.findAllOnlineMembers();
    log.info("Get group members ---------->" + JSONUtil.toJsonStr(members));
    return Result.ok(MapUtil.of("list", members));
}

Copy the code
  • Implementation class: com. Homework. Im. Service. Impl. ChatServiceImpl# findAllOnlineMembers
@Override
public Set<Object> findAllOnlineMembers() {
    Set<Object> ids = redisUtil.sGet(Constant.ONLINE_MEMBERS_KEY);
    Set<Object> results = new HashSet<>();
    if(ids == null) return results;
    ids.forEach((id) -> {
        Map<Object, Object> map = redisUtil.hmget((String) id);
        results.add(map);
    });
    return results;
}

Copy the code

In the previous step, we first fetched ids from the set list, then looped the user information into the list and returned it. When was the information saved? We said above that it monitored the online and offline of users. Let’s take a look:

  • online

  • offline

  • The main logic

Above we use the cache to complete this function, in our development, we can use more redis cache to help us complete the development, improve the speed of website access.

2. Get chat history

Next, let’s get the group history chat history. Also, let’s take a look at layim interface design:

Look at the CSS/modules/layim/HTML/chatlog. The HTML page in the directory HTML, js in json format specification.

Preferred configuration:

Let’s analyze it:

First we need to save the chat logs, which can also be recorded using our Redis cache. You can give it an expiration date, one day and so on. Here you can also use the list list to save the chat history. In fact, the format of historical messages is the same as the format of sending messages, so we can save the message to the cache when sending messages, when we open the chat window, read the message from the cache, and then display the window loop.

Let’s first write two interfaces for retrieving and saving messages:

  • Only the last count records are retrieved

  • com.example.service.impl.ChatServiceImpl#getGroupHistoryMsg

  • When to invoke: Expose fetch History message interface

@Override
public List<Object> getGroupHistoryMsg(int count) {
    long length = redisUtil.lGetListSize(Constant.GROUP_HISTROY_MSG_KEY);
    return redisUtil.lGet(Constant.GROUP_HISTROY_MSG_KEY, length - count < 0 ? 0 : length - count, length);
}

Copy the code

  • Save messages for 24 hours (flexible design)

  • com.example.service.impl.ChatServiceImpl#setGroupHistoryMsg

  • When to call: When to send a message

@Override
public boolean setGroupHistoryMsg(ImMess imMess) {
    return redisUtil.lSet(Constant.GROUP_HISTROY_MSG_KEY, imMess, 24 * 60 * 60);
}

Copy the code

I will not post the specific JS code, students can compare the code changes in the process of building.

Effect:

Job summary

Ok, finally, our course assignments have come to an end, and the students have worked hard. Hopefully, you’ll be able to consolidate some of the things that we’ve learned through this course assignment.

See you next time

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — (finish) — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Author: Lu Yiming

Official account: MarkerHub