Construction WebSocket + Netty web chat program

WebSocket

The traditional mode of interaction between the browser and the server is based on the request / response mode, although you can use js transmission timing task lets the browser on the server, but pull malpractice Obviously, the first is the unavoidable delay, followed by frequent request for the server to upgrade the pressure suddenly

WebSocket H5 new protocol is used to construct the communication mode is not limited length connection between the browser and the server is no longer confined request / response model, the server may actively push the message to the client, ( the game has a player winning a barrage) based on this characteristic, we can build our real-time communication program

Agreement details:

Websocket When connection is established, an HTTP request sent by the browser, the following message:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
  • First GET request is wsthe beginning of the
  • Wherein the request header Upgrade: websocket Connection: Upgradeindicates that the attempt to establish connection WebSocket

For the corresponding data server

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

Which 101represents the server supports WebSocket protocol, the two sides based on Http request, successfully established a WebSocket connection, the communication between the two sides are no longer via HTTP

JS objects of encapsulating WebSocket

For the object of JS WebSocket, it commonly 4 callback method , and two active method

Method name effect
onopen() And the server has successfully connected callback
onmessage(e) After receiving the message server callback, e is the message object
onerror() Link abnormal callbacks, such as server shut down
onclose() When connecting the client callback unilateral disconnection
send(e) Initiative to push messages to the server
close() Close the active channel

Encapsulation again WebSocket

Know the callback function callback opportunity, we need to do is in a different callback his entire life cycle, add our specified action on ok, here is the definition of a Window by global chat partner CHAT

window.CHAT={
var socket = null;
// 初始化socket
init:function(){
// 判断当前的浏览器是否支持WebSocket
if(window.WebSocket){
    // 检验当前的webSocket是否存在,以及连接的状态,如已经连接,直接返回
    if(CHAT.socket!=null&&CHAT.socket!=undefined&&CHAT.socket.readyState==WebSocket.OPEN){
        return false;
    }else{// 实例化 , 第二个ws是我们可以自定义的, 根据后端的路由来
        CHAT.socket=new WebSocket("ws://192.168.43.10:9999/ws");
        // 初始化WebSocket原生的方法
        CHAT.socket.onopen=CHAT.myopen();
        CHAT.socket.onmessage=CHAT.mymessage();
        CHAT.socket.onerror=CHAT.myerror();
        CHAT.socket.onclose=CHAT.myclose(); 
    
    }
}else{
    alert("当前设备不支持WebSocket");
}
}
// 发送聊天消息
chat:function(msg){
    // 如果的当前的WebSocket是连接的状态,直接发送 否则从新连接
    if(CHAT.socket.readyState==WebSocket.OPEN&&CHAT.socket!=null&&CHAT.socket!=undefined){
        socket.send(msg);
    }else{
        // 重新连接
        CHAT.init();
        // 延迟一会,从新发送
        setTimeout(1000);
        CHAT.send(msg);
    }
}
// 当连接建立完成后对调
myopen:function(){
    // 拉取连接建立之前的未签收的消息记录
    // 发送心跳包
}
mymessage:function(msg){
    // 因为服务端可以主动的推送消息,我们提前定义和后端统一msg的类型, 如,拉取好友信息的消息,或 聊天的消息
    if(msg==聊天内容){
    // 发送请求签收消息,改变请求的状态
    // 将消息缓存到本地
    // 将msg 转换成消息对象, 植入html进行渲染
    }else if(msg==拉取好友列表){
    // 发送请求更新好友列表
    }
    
}
myerror:function(){
    console.log("连接出现异常...");
}
myclose:function(){
    console.log("连接关闭...");
}
keepalive: function() {
    // 构建对象
    var dataContent = new app.DataContent(app.KEEPALIVE, null, null);
    // 发送心跳
    CHAT.chat(JSON.stringify(dataContent));
    
    // 定时执行函数, 其他操作
    // 拉取未读消息
    // 拉取好友信息
}

}

Message type conventions

WebSocket objects through send(msg); method submits data to a rear end, the common data is as follows:

  • The client sends a chat message
  • The client sign a message
  • The client sends a heartbeat packet
  • The client requests to establish a connection

In order to make the rear end of the received data of different types made of different actions, so we agreed msg type of transmission;

// 消息action的枚举,这个枚举和后端约定好,统一值
CONNECT: 1,     // 第一次(或重连)初始化连接
CHAT: 2,        // 聊天消息
SIGNED: 3,      // 消息签收
KEEPALIVE: 4,   // 客户端保持心跳
PULL_FRIEND:5,  // 重新拉取好友

// 消息模型的构造函数
ChatMsg: function(senderId, receiverId, msg, msgId){
    this.senderId = senderId;
    this.receiverId = receiverId;
    this.msg = msg;
    this.msgId = msgId;
}

//  进一步封装两个得到最终版消息模型的构造函数
DataContent: function(action, chatMsg, extand){
    this.action = action;
    this.chatMsg = chatMsg;
    this.extand = extand;
}

How to send data?

We use js, bind to the Send button click event, once triggered, the parameters we need to get out from the cache, call

CHAT.chat(Json.stringify(dataContent));

Netty backend parses type dataContent further processing

How to sign messages sent by friends when not connected to the server?

  • The timing of receipt of the message:
    the reason there will not sign the message, because the client does not establish a WebSocket connection with the server when the server determines channel group he maintained no channel recipient, does not send data, but the data is persisted to the database, and flag = mark unread, so naturally we sign information on the callback function of the client and server to establish a connection execution

  • step:
    • Js request by the client, and pulling all of their associated flag = unread message entity list
    • From the number of the callback function, the data taken in the list, the local cache
    • The list of significant data back in the html page
    • And a rear convention, the id list all examples withdrawn, separated by commas spliced into a string to action=SIGNEDsend to the rear end of the way, let it be sign

Netty WebSocket support

First, each Netty server programs are quite similar, except you want to create a different server, you have to assemble a pipeline of Netty Handler

For chat programs, treatment of type String Json information, we selected SimpleChannelInboundHandler, he is a typical inbound processor, and if we did not come out the data, she will help us recover rewrite inside it does not implement abstract methods, these methods are also abstract callback method, when a new Channel coming in, register it into the Selector process, the callback will be different abstract methods

Method name Callback opportunity
handlerAdded(ChannelHandlerContext ctx) Pepiline the addition was completed callback Handler
channelRegistered(ChannelHandlerContext ctx) After registering the callback channel into Selector
channelActive(ChannelHandlerContext ctx) channel is active Callback
channelReadComplete(ChannelHandlerContext ctx) channel, after the read callback
userEventTriggered(ChannelHandlerContext ctx, Object evt) When there is a user event callback, such as read / write
channelInactive(ChannelHandlerContext ctx) The client callbacks when disconnected
channelUnregistered(ChannelHandlerContext ctx) After a callback to disconnect the client, to cancel the registration channel
handlerRemoved(ChannelHandlerContext ctx) After the cancellation of registration of the channel, the channel callback after removing ChannelGroup
exceptionCaught(ChannelHandlerContext ctx, Throwable cause) Callback when an exception occurs

handler design coding

To do chat-point, provided that the server has full read-write channel because all the data are dependent on it, and netty provided us ChannelGroupto save all the new add in the channel, in addition point to point chat, we need to user information and channel it belongs to one to one binding, can precisely matching two further data exchange channel, thus adding mapping class UserChannel

public class UserChanelRelationship {
    private static HashMap<String, Channel> manager = new HashMap<>();
    public static  void put(String sendId,Channel channel){
        manager.put(sendId,channel);
    }
    public static Channel get(String sendId){
        return  manager.get(sendId);
    }
    public static void outPut(){
        for (HashMap.Entry<String,Channel> entry:manager.entrySet()){
            System.out.println("UserId: "+entry.getKey() + "channelId: "+entry.getValue().id().asLongText());
        }
    }
}

We put the relationship between the User and Channel storage in the form of key-value pairs into a Map, after the server is started, the program will maintain the map, so the question is? Add a mapping relationship between the two when? Look the handler callback function, we choose channelRead0()when we judge that the information is sent by the client CONNECTtype, add a mapping relationship

Here ishandler的处理编码

public class MyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用于管理整个客户端的 组
public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame frame) throws Exception {
Channel currentChanenl = channelHandlerContext.channel();

// 1. 获取客户端发送的消息
String content = frame.text();
System.out.println("  content:  "+content);

// 2. 判断不同的消息的类型, 根据不同的类型进行不同的处理
    // 当建立连接时, 第一次open , 初始化channel,将channel和数据库中的用户做一个唯一的关联
DataContent dataContent = JsonUtils.jsonToPojo(content,DataContent.class);
Integer action = dataContent.getAction();

if (action == MsgActionEnum.CHAT.type) {

    // 3. 把聊天记录保存到数据库
    // 4. 同时标记消息的签收状态 [未签收]
    // 5. 从我们的映射中获取接受方的chanel  发送消息
    // 6. 从 chanelGroup中查找 当前的channel是否存在于 group, 只有存在,我们才进行下一步发送
    //  6.1 如果没有接受者用户channel就不writeAndFlush, 等着用户上线后,通过js发起请求拉取未接受的信息
    //  6.2 如果没有接受者用户channel就不writeAndFlush, 可以选择推送

}else if (action == MsgActionEnum.CONNECT.type){
    // 当建立连接时, 第一次open , 初始化channel,将channel和数据库中的用户做一个唯一的关联
    String sendId = dataContent.getChatMsg().getSenderId();
    UserChanelRelationship.put(sendId,currentChanenl);
    
}else if(action == MsgActionEnum.SINGNED.type){
    // 7. 当用户没有上线时,发送消息的人把要发送的消息持久化在数据库,但是却没有把信息写回到接受者的channel, 把这种消息称为未签收的消息
    
    // 8. 签收消息, 就是修改数据库中消息的签收状态, 我们和前端约定,前端如何签收消息在上面有提到
    String extend = dataContent.getExtand();
    // 扩展字段在 signed类型代表 需要被签收的消息的id, 用逗号分隔
    String[] msgIdList = extend.split(",");
    List<String> msgIds = new ArrayList<>();
    Arrays.asList(msgIdList).forEach(s->{
        if (null!=s){
            msgIds.add(s);
        }
    });
    if (!msgIds.isEmpty()&&null!=msgIds&&msgIds.size()>0){
        // 批量签收
    }

}else if (action == MsgActionEnum.KEEPALIVE.type){
    // 6. 心跳类型
    System.out.println("收到来自channel 为" +currentChanenl+" 的心跳包... ");
}

}

// handler 添加完成后回调
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 获取链接, 并且若想要群发的话,就得往每一个channel中写数据, 因此我们得在创建连接时, 把channel保存起来
System.err.println("handlerAdded");
users .add(ctx.channel());
}

// 用户关闭了浏览器回调
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 断开连接后, channel会自动移除group
// 我们主动的关闭进行, channel会被移除, 但是我们如果是开启的飞行模式,不会被移除
System.err.println("客户端channel被移出: "+ctx.channel().id().asShortText());
users.remove(ctx.channel());
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 发生异常关闭channel, 并从ChannelGroup中移除Channel
ctx.channel().close();
users.remove(ctx.channel());
}

... 其他方法

Front and rear ends to maintain the heartbeat

The two sides establish a WebSocket connection, the server needs to clearly understand, maintain their own channel in the many, who have been hung up, in order to improve performance, the need for early removal of the waste channel ChanelGroup

The client kill the process, or open the flight mode, then the server is aware that it can not maintain a channel already has can not be used, first of all, the maintenance of a channel can not be used will affect performance, but when the channel's friend gave him time to send a message, the server believes users online, then refresh the data is written to a non-existent channel, will bring additional trouble

Then we need to add heartbeat mechanism, the client set up regular tasks, each period of time to send heartbeat packets go out into the service side, the contents of the heartbeat packet is not the focus of what its role is to tell the server he was still active, N more the client sends to the server every heartbeat, this does not increase the server's request, because the request is sent by the send method WebSocket past, only dataContent type is KEEPALIVE, again this is a good agreement ahead of us (in addition server sends a heartbeat to the client appears to be unnecessary)

Thus it is the rear end, we send the heartbeat packets will be such that the channel corresponding to the current client channelRead0 () callback method, Netty provides us heartbeat-related Handler, every chanelRead0 () callback are read / write events, the following is netty achieve the heartbeat of support

/**
 * @Author: Changwu
 * @Date: 2019/7/2 9:33
 * 我们的心跳handler不需要实现handler0方法,我们选择,直接继承SimpleInboundHandler的父类
*/
public class HeartHandler extends ChannelInboundHandlerAdapter {
// 我们重写  EventTrigger 方法
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 当出现read/write  读写写空闲时触发
if(evt instanceof IdleStateEvent){
    IdleStateEvent event = (IdleStateEvent) evt;

    if (event.state()== IdleState.READER_IDLE){ // 读空闲
        System.out.println(ctx.channel().id().asShortText()+" 读空闲... ");
    }else if (event.state()==IdleState.WRITER_IDLE){
        System.out.println(ctx.channel().id().asShortText()+" 写空闲... ");
    }else if (event.state()==IdleState.ALL_IDLE){
        System.out.println("channel 读写空闲, 准备关闭当前channel  , 当前UsersChanel的数量: "+MyHandler.users.size());
        Channel channel = ctx.channel();
        channel.close();
        System.out.println("channel 关闭后, UsersChanel的数量: "+MyHandler.users.size());
    }
}
}

Handler SimpleChannelInboundHandler we no longer use, because it is among the methods are abstract methods, but we need time to callback function is that every time the callback when a user event, such as read, write events, which may prove channel is still alive, corresponding method isuserEventTriggered()

In addition, ChannelInboundHandlerAdapter is netty, the reflected adapter mode , which implements all abstract methods, then he is not in the implementation of the work, but to go down this incident spread, and now we rewrite the userEventTriggered()execution of what we logic

In addition, we need to add a handler in the pipeline

    ... 
/ 添加netty为我们提供的 检测空闲的处理器,  每 20 40 60 秒, 会触发userEventTriggered事件的回调
pipeline.addLast(new IdleStateHandler(10,20,30));
// todo 添加心跳的支持
pipeline.addLast("heartHandler",new HeartHandler());

Server initiative to push data to the client

E.g., add friends operation, A friend request sent to the process of adding B, will go through the following steps

  • A sends ajax request to the server, his id, id target of friends persisted to the database, table request friend_request
  • User B on-line, through the js, to pull the rear end friend_request table has no information on its own, then the request server A to B push past
  • B was back at the front end of the request A, B for further processing the information, this time in both cases
    • A denied request B: the rear end of the clear information about the table friend_request AB,
    • B of A agreed to the request: firend_List table in the back end, the two sides AB information are persistent in, then we can take advantage of the back-end of the method, the B push to-date contact information, but it does not belong to the initiative to push , because this is a client initiates a session of

But I do not know A, B have been agreed, and the need to push data A proactive, how to push it? We need above UserChannel relationship, the sender channel out, then back in writeAndFlushthe content, then on A B that has been agreed, reload buddy list

Guess you like

Origin www.cnblogs.com/ZhuChangwu/p/11184654.html