Строительство WebSocket + Нетти программа веб-чат

WebSocket

Традиционный способ взаимодействия между браузером и сервером на основе режиме запроса / ответ, хотя вы можете использовать передачу Js синхронизацию задачи позволяют браузер на сервере, но тянуть злоупотребления Очевидно, что первый является неизбежной задержкой с последующим частым запрос на сервер, чтобы обновить давление внезапно

WebSocket не Н5 новый протокол используется для построения режима связи не ограничен длиной соединение между браузером и сервером уже не ограничивается запрос / ответ модели, сервер может активно толкать сообщение клиента, ( в игре есть игрок выигрышного заграждения) на основе этой характеристики, мы можем строить нашу программу коммуникации в режиме реального времени

Детали соглашения:

WebSocket Когда соединение установлено, запрос HTTP, посланный браузером, следующее сообщение:

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
  • Первый запрос GET это wsначало
  • В котором заголовок запроса Upgrade: websocket Connection: Upgradeуказывает на то, что попытка установления соединения WebSocket

Для соответствующего сервера данных

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

Который 101не представляет сервер поддерживает протокол WebSocket, обе стороны на основе запроса HTTP, успешно установлено соединение WebSocket, связь между двумя сторонами уже не через HTTP

объекты JS инкапсулирования WebSocket

Для объекта JS WebSocket, является , обще 4 метода обратного вызова , и два метода активных

имя метода эффект
OnOpen () И сервер успешно подключился обратный вызов
OnMessage (е) После приема обратного вызова сервера сообщений, адрес является объектом сообщения
OnError () Link ненормальные обратные вызовы, такие как сервера закрыли
OnClose () При подключении клиента обратного одностороннего отключения
отправить (е) Инициатива выдвинуть сообщения на сервер
близко() Закройте активный канал

Инкапсуляция снова WebSocket

Знать возможности функции обратного вызова обратных вызова, нам нужно сделать, это в другом обратном вызове весь его жизненный цикл, добавьте наше указанное действие ОК, вот определение окна с помощью глобального чата партнера ЧАТА

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));
    
    // 定时执行函数, 其他操作
    // 拉取未读消息
    // 拉取好友信息
}

}

конвенции Тип сообщения

WebSocket объекты через send(msg); метод представляет данные к заднему концу, общие данные следующим образом :

  • Клиент посылает сообщение в чате
  • Клиент подписывает сообщение
  • Клиент посылает пакет пульса
  • Клиент запрашивает установить соединение

Для того, чтобы сделать заднюю часть полученных данных различных типов из различных действий, поэтому мы договорились типа Сообща передачи;

// 消息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;
}

Как отправить данные?

Мы используем JS, связывается с кнопкой Отправить событие нажатия, когда срабатывает, параметры, мы должны выйти из кэша, вызова

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

Нетти бэкенд анализирует тип dataContent дальнейшей обработки

Как подписывать сообщения, отправленные друзьями, когда не подключен к серверу?

  • Время поступления сообщения:
    причина не будет подписывать сообщение, потому что клиент не устанавливает соединение WebSocket с сервером , когда сервер определяет группу каналов он не поддерживается ни одного получателя каналу, не передает данные, но данные сохранялись в базу данных, и флаг = маркировать непрочитанным, поэтому , естественно , мы подписываем информацию о функции обратного вызова клиента и сервера , чтобы установить выполнение соединения

  • шаги:
    • запрос Js клиента, и вытягивать все связанное с ними флагом = непрочитанные сообщения список объекта
    • Из числа функции обратного вызова, данные взяты из списка, локальный кэш
    • Список значительных обратных данных на странице HTML
    • И задняя условность, список идентификаторов всех примеров отозваны, разделенные запятые сращены в строку , чтобы action=SIGNEDотправить к заднему концу пути, пусть это будет знак

поддержка Нетти WebSocket

Во-первых, каждый из которых серверные программы Нетти очень похожи, за исключением того, вы хотите создать другой сервер, вы должны собрать трубопровод Нетти Handler

Для программ чата, лечение типа Строки Json информации, мы выбрали SimpleChannelInboundHandler, он типичный въездной процессор, и если бы мы не вышли данные, она поможет нам восстановить переписывают внутри него не реализует абстрактные методы, эти методы также являются абстрактным обратным вызовом Способ, когда новый канал поступает, зарегистрировать его в процесс Selector, обратный вызов будут разными абстрактными методами

имя метода Ответная возможность
handlerAdded (ChannelHandlerContext CTX) Pepiline добавление было завершено обратного вызова обработчика
channelRegistered (ChannelHandlerContext CTX) После регистрации канала обратного вызова в селекторе
channelActive (ChannelHandlerContext CTX) канал активен обратный вызов
channelReadComplete (ChannelHandlerContext CTX) канала, после считывания обратного вызова
userEventTriggered (ChannelHandlerContext CTX, ЭВТ объекта) При наличии обратного вызова пользовательских событий, таких, как чтение / запись
channelInactive (ChannelHandlerContext CTX) Клиентские обратные вызовы при отключенных
channelUnregistered (ChannelHandlerContext CTX) После обратного вызова для отключения клиента, чтобы отменить регистрацию канал
handlerRemoved (ChannelHandlerContext CTX) После отмены регистрации канала, канал обратного вызова после удаления группы каналов
exceptionCaught (ChannelHandlerContext CTX, Throwable, причина) Обратный вызов, когда происходит исключение

кодирование дизайн обработчика

Для того, чтобы сделать чат-точку, при условии , что сервер имеет полный канал чтение-запись , поскольку все данные зависят от него, и Нетти предоставил нам ChannelGroupсохранить все новые добавления в канале, в точке присоединения к точке чата, мы должны пользовательская информация и канала принадлежит к одному к одному связыванию, может точно сопоставления два дополнительных данных канала обмена, таким образом , добавление класса отображения 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());
        }
    }
}

Ставим отношения между Пользователем и хранения канала в виде пар ключ-значение в карте, после запуска сервера, программа будет поддерживать карты, поэтому вопрос? Добавить отношения отображения между ними когда? Посмотрите функция обратного вызова обработчика, мы выбираем , channelRead0()когда мы будем судить о том , что информация передается клиентскому CONNECTтипа, добавьте зависимость отображения

Вотhandler的处理编码

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());
}

... 其他方法

Передние и задние концы, чтобы поддерживать сердцебиение

Обе стороны установить соединение WebSocket, сервер должен четко понимать, поддерживать свой собственный канал в многих, которые были повешены, для того, чтобы повысить производительность, необходимость своевременного удаления отходов канала ChanelGroup

Клиент убить процесс, или открыть режим полета, то сервер знает, что он не может поддерживать канал уже не может быть использована, прежде всего, поддержание канала не может использоваться будет влиять на производительность, но когда друг каналу дал ему время, чтобы отправить сообщение, сервер считает пользователь онлайн, а затем обновить данные записываются в несуществующий канал, принесут дополнительные проблемы

Затем нам нужно добавить механизм сердцебиения, клиент настроить регулярные задачи, каждый период времени, чтобы отправить сердцебиения пакеты выходят в сторону сервиса, содержимое пакета пульса не фокус, что его роль, чтобы сообщить серверу, он был все еще активен, N более клиент отправляет на сервер каждый сердцебиение, это не увеличивает запрос сервера, так как запрос посылается WebSocket прошлого метод отправки, только dataContent типа KEEPALIVE, опять-таки это хорошее согласие впереди нас (в дополнении сервер посылает импульсы к клиенту оказывается ненужным)

При этом задний конец, мы посылаем сердцебиение пакеты будут такими, что канал, соответствующий текущим клиенту channelRead0 () метод обратного вызова, Нетти дает нам сердцебиение, связанному с Handler, каждый chanelRead0 () обратный вызов для чтения / событие записи, следующие Нетти достижения пульсации поддержки

/**
 * @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());
    }
}
}

Обработчик SimpleChannelInboundHandler мы больше не использовать, так как он является одним из методов абстрактные методы, но нам нужно время для обратного вызова функции является то, что каждый раз, когда обратного вызова, когда событие пользователя, такие как чтение, запись событий, которые могут оказаться канал все еще жив, соответствующий методuserEventTriggered()

Кроме того, ChannelInboundHandlerAdapter является Нетти, то режим отражается адаптер , который реализует все абстрактные методы, то он не в выполнении работы, но идти по этому падающему распространению, и теперь мы перепишем userEventTriggered()выполнение того , что мы логика

Кроме того, нам нужно добавить обработчик в трубопроводе

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

Инициатива сервера передавать данные клиента

Например, добавить друзей операцию, запрос по электронной почте отправляется в процессе добавления B, будет проходить через следующие шаги

  • А посылает запрос Ajax на сервер, его идентификатор, целевой идентификатор друзей сохраняются в базе данных, запрос таблицы friend_request
  • Пользователь B на линии, через JS, чтобы тянуть заднюю стол конца friend_request не имеет никакой информации о своем собственном, то запросе сервере A к B нажимного прошлому
  • В был обратно на переднем конце запроса A, B для дальнейшей обработки информации, на этот раз в обоих случаях
    • Недопущенный запрос B: задний конец четкой информации о таблице friend_request AB,
    • B А согласился на просьбу: firend_List таблицы в задней части, обе стороны информации AB является стойкой в, то мы можем воспользоваться фоновой методой, B нажимного до даты контактной информации, но это не относится к инициативе, чтобы подтолкнуть , потому что это клиент инициирует сеанс

Но я не знаю , A, B, были согласованы, и необходимость передавать данные Превентивный, как протолкнуть его? Нам нужно выше UserChannel отношения, канал отправителя из, а затем обратно в writeAndFlushсодержание, то на А В том , что было согласовано, перезарядка список друзей

рекомендация

отwww.cnblogs.com/ZhuChangwu/p/11184654.html
рекомендация