Sistema de mensajería instantánea IM [SpringBoot+Netty] - Combing (1)

Directorio de artículos

código fuente del proyecto

Tabla de contenido
Sistema de mensajería instantánea IM [SpringBoot+Netty] - Combing (2)
Sistema de mensajería instantánea IM [SpringBoot+Netty] - Combing (3)
Sistema de mensajería instantánea IM [SpringBoot+Netty] - Combing (4)
Sistema de mensajería instantánea IM [SpringBoot+Netty] - Combing (5)

1. Por qué desarrollar un sistema de mensajería instantánea de desarrollo propio

1. Cuáles son las formas de implementar un sistema de mensajería instantánea


Primero mire el sistema im del mercado (nada más que estas tres formas):

  1. Use productos de código abierto para desarrollo secundario o uso directo
  2. Utilice un proveedor de servicios en la nube de pago
  3. Autoestudio

1.1 Usar productos de código abierto para desarrollo secundario o uso directo


优点: Puede comenzar rápidamente, use

缺点: Falta de funciones, poca sustentabilidad, sin equipo para mantenimiento y expansión posteriores, ya sea que coincida con la pila tecnológica de su empresa

1.2 Uso de proveedores de servicios en la nube pagos


优点: No es necesario desarrollar un sistema im, ni necesita un servidor de operación y mantenimiento. Los proveedores de servicios a gran escala tienen una tecnología relativamente madura y una alta confiabilidad en la transmisión de mensajes. Según las bibliotecas oficiales sdk y ui de los proveedores de servicios, es es fácil agregar funciones im a sus propios servicios

缺点: Es imposible espiar el código fuente del proveedor de servicios (fuente cerrada), y es difícil satisfacer las necesidades personalizadas. Si la extensión oficial no satisface sus necesidades, básicamente no hay solución. La información y los datos son importantes. activos Las manos de los demás no son muy buenas, el costo del servicio es alto

1.3 Desarrollo propio


优点: Desarrolle en línea con la pila de tecnología de la empresa, no se preocupe por el mantenimiento posterior, personalice sus propias necesidades y la seguridad de los datos está protegida

缺点: Debe ser desarrollado por alguien que esté particularmente familiarizado con el sistema im, y existen ciertos requisitos para el nivel técnico, y el costo de mano de obra aumenta



2. Cómo desarrollar un sistema de mensajería instantánea de desarrollo propio

2.1 Cómo se realizó el primer sistema de mensajería instantánea

inserte la descripción de la imagen aquí

Esta es la arquitectura técnica implementada por el primer servicio al cliente de Jingdong

Esta arquitectura provocará un desperdicio de recursos y el sondeo no se detendrá cuando no haya ningún mensaje para enviar.

2.2 La composición básica de un sistema de mensajería instantánea


inserte la descripción de la imagen aquí

  • 客户端: terminal PC (MAC, WINDOS), terminal móvil (Android, Apple), terminal WEB

  • 服务层

    • 接入层: El portal del sistema im es un 核心módulo comparativo en el sistema im, que mantiene el enlace largo entre el cliente y el servidor.El mensaje se envía desde el cliente a la capa de acceso, y la capa de acceso lo entrega a la lógica capa para el procesamiento;: la primera 接入层主要分为四个功能es Mantener un enlace largo, la segunda es el análisis de protocolo, la tercera es el mantenimiento de nuestra sesión y la cuarta es el envío de mensajes; cuando se completa el procesamiento del mensaje, también se entrega al cliente por el capa de acceso; entre la capa de acceso y el cliente 必须Debe haber un protocolo (protocolo de capa de aplicación: protocolo de texto y protocolo binario - MQTT, XMPP, HTPP y otros protocolos; protocolo privado)
    • 逻辑层: Un módulo tras otro del sistema empresarial: usuarios, cadenas de relación, grupos, mensajes
  • 存储层:MySQL, Redis

2.3 La arquitectura común actual de los sistemas de mensajería instantánea


inserte la descripción de la imagen aquí

  • La conexión larga envía y recibe mensajes inmediatamente, y el mensaje se puede entregar directamente al usuario a través de la conexión larga. En comparación con el sondeo largo, se evitan muchos bucles vacíos (consulte este artículo: Cuatro formas de comunicación web )

  • rpc调用Se puede acceder a las capas de acceso y lógica a través demq解耦

  • Las principales capas de persistencia conectadas por la capa lógica completan el trabajo de persistencia.

2.4 Resumen

接入层: Para mantener la conexión larga de nuestro cliente y el envío y recepción de mensajes, el protocolo puede considerar usar el protocolo TCP (confiable); elegir uno adecuado (MQTT, XMPP, protocolo privado); la capa de acceso también necesita mantener 应用层协议la sesión de usuario y conexión La capa de acceso es diferente del desarrollo web tradicional.La capa de acceso es un servicio con estado, mientras que el http tradicional es un servicio sin estado.

逻辑层: Procese la lógica central del envío y la recepción de mensajes, y coopere con la capa de acceso y la capa de almacenamiento para asegurarse verdaderamente de que los mensajes no se pierdan, se filtren o se mezclen.

存储层:Debe tener un diseño razonable, proporcionar servicios de datos para la capa lógica y poder transportar una gran cantidad de datos de registros de chat.



2. Desarrollo de datos básicos

1. Importar información de usuario, eliminar información de usuario, modificar información de usuario, consultar información de usuario


Aquí creo que es un buen lugar, usando la lógica de importar datos de usuario como demostración:

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

Luego, aquí hay algunas lógicas de agregar, eliminar, modificar y verificar, por lo que no lo escribiré aquí, sabré el significado general después de revisarlo yo mismo, y será lo mismo más adelante.

2. Los datos más valiosos en la mensajería instantánea: análisis comercial del módulo de cadena de relaciones y diseño de base de datos


2.1 Los datos más valiosos - cadena de amistad

       ¿Por qué dices eso? Verá, ¿por qué WeChat y QQ son tan fuertes? Es porque tienen a tus amigos entre ellos. Si cambias a otro software de chat, perderás a todos estos amigos. ¿Crees que esto es de gran valor?

2.2 Amistad

  1. Amistad débil: suscribirse a Weibo
  2. Amistad fuerte: como WeChat (el método utilizado en este sistema)

2.3 Diseño de base de datos

  • Diseño de relaciones amistosas débiles y buenas:
    inserte la descripción de la imagen aquí

  • Fuerte diseño de amistad:

inserte la descripción de la imagen aquí

  • diseño final
    inserte la descripción de la imagen aquí

3. Se realizan las funciones de importar, agregar, actualizar amigos, eliminar amigos, eliminar todos los amigos, extraer amigos específicos y extraer todos los amigos.


Aquí hay un código lógico específico para agregar amigos, otros son similares a esta idea general.

// 添加好友的逻辑
@Transactional
public ResponseVO doAddFriend(RequestBase requestBase, String fromId, FriendDto dto, Integer appId){
    
    
    // A-B
    // Friend表插入 A 和 B 两条记录
    // 查询是否有记录存在,如果存在则判断状态,如果是已经添加,则提示已经添加了,如果是未添加,则修改状态

    // 第一条数据的插入
    LambdaQueryWrapper<ImFriendShipEntity> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ImFriendShipEntity::getAppId, appId);
    lqw.eq(ImFriendShipEntity::getFromId, fromId);
    lqw.eq(ImFriendShipEntity::getToId, dto.getToId());
    ImFriendShipEntity entity = imFriendShipMapper.selectOne(lqw);

    long seq = 0L;
    // 不存在这条消息
    if(entity == null){
    
    
        // 直接添加
        entity = new ImFriendShipEntity();
        seq = redisSeq.doGetSeq(appId + ":" + Constants.SeqConstants.Friendship);
        entity.setAppId(appId);
        entity.setFriendSequence(seq);
        entity.setFromId(fromId);
        BeanUtils.copyProperties(dto, entity);
        entity.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode());
        entity.setCreateTime(System.currentTimeMillis());
        int insert = imFriendShipMapper.insert(entity);
        if(insert != 1){
    
    
            // TODO 添加好友失败
            return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR);
        }
        writeUserSeq.writeUserSeq(appId, fromId, Constants.SeqConstants.Friendship, seq);
    }else{
    
    
        // 存在这条消息,去根据状态做判断
        // 他已经是你的好友了
        if(entity.getStatus() == FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()){
    
    
            // TODO 对方已经是你的好友
            return ResponseVO.errorResponse(FriendShipErrorCode.TO_IS_YOUR_FRIEND);
        }else{
    
    
            ImFriendShipEntity update = new ImFriendShipEntity();
            if(StringUtils.isNotEmpty(dto.getAddSource())){
    
    
                update.setAddSource(dto.getAddSource());
            }
            if(StringUtils.isNotEmpty(dto.getRemark())){
    
    
                update.setRemark(dto.getRemark());
            }
            if(StringUtils.isNotEmpty(dto.getExtra())){
    
    
                update.setExtra(dto.getExtra());
            }
            seq = redisSeq.doGetSeq(appId + ":" + Constants.SeqConstants.Friendship);
            update.setFriendSequence(seq);
            update.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode());

            int res = imFriendShipMapper.update(update, lqw);
            if(res != 1){
    
    
                // TODO 添加好友失败
                return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR);
            }
            writeUserSeq.writeUserSeq(appId, fromId, Constants.SeqConstants.Friendship, seq);
        }
    }

    // 第二条数据的插入
    LambdaQueryWrapper<ImFriendShipEntity> lqw1 = new LambdaQueryWrapper<>();
    lqw1.eq(ImFriendShipEntity::getAppId, appId);
    lqw1.eq(ImFriendShipEntity::getFromId, dto.getToId());
    lqw1.eq(ImFriendShipEntity::getToId, fromId);
    ImFriendShipEntity entity1 = imFriendShipMapper.selectOne(lqw1);

    // 不存在就直接添加
    if(entity1 == null){
    
    
        entity1 = new ImFriendShipEntity();
        entity1.setAppId(appId);
        entity1.setFromId(dto.getToId());
        BeanUtils.copyProperties(dto, entity1);
        entity1.setToId(fromId);
        entity1.setFriendSequence(seq);
        entity1.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode());
        entity1.setCreateTime(System.currentTimeMillis());
        int insert = imFriendShipMapper.insert(entity1);
        if(insert != 1){
    
    
            // TODO 添加好友失败
            return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR);
        }
        writeUserSeq.writeUserSeq(appId, dto.getToId(), Constants.SeqConstants.Friendship, seq);
    }else{
    
    
        // 存在就判断状态
        if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() != entity1.getStatus()){
    
    
            // TODO 对方已经是你的好友
            return ResponseVO.errorResponse(FriendShipErrorCode.TO_IS_YOUR_FRIEND);
        }else{
    
    
            ImFriendShipEntity entity2 = new ImFriendShipEntity();
            entity2.setFriendSequence(seq);
            entity2.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode());
            imFriendShipMapper.update(entity2, lqw1);
            writeUserSeq.writeUserSeq(appId, dto.getToId(), Constants.SeqConstants.Friendship, seq);
        }
    }

    // TODO TCP通知
    // A B 添加好友,要把添加好友的信息,发送给除了A其他的端,还要发送给B的所有端

    // 发送给from
    AddFriendPack addFriendPack = new AddFriendPack();
    BeanUtils.copyProperties(entity, addFriendPack);
    addFriendPack.setSequence(seq);
    if(requestBase != null){
    
    
        messageProducer.sendToUser(fromId, requestBase.getClientType(), requestBase.getImei(),
                FriendshipEventCommand.FRIEND_ADD, addFriendPack, requestBase.getAppId());
    }else{
    
    
        messageProducer.sendToUser(fromId,
                FriendshipEventCommand.FRIEND_ADD, addFriendPack, requestBase.getAppId());
    }

    // 发送给to
    AddFriendPack addFriendToPack = new AddFriendPack();
    BeanUtils.copyProperties(entity1, addFriendToPack);
    messageProducer.sendToUser(entity1.getFromId(), FriendshipEventCommand.FRIEND_ADD, addFriendToPack,
            requestBase.getAppId());

    // 之后回调
    if(appConfig.isDestroyGroupAfterCallback()){
    
    
        AddFriendAfterCallbackDto addFriendAfterCallbackDto = new AddFriendAfterCallbackDto();
        addFriendAfterCallbackDto.setFromId(fromId);
        addFriendAfterCallbackDto.setToItem(dto);
        callbackService.callback(appId, Constants.CallbackCommand.AddFriendAfter,
                JSONObject.toJSONString(addFriendAfterCallbackDto));
    }

    return ResponseVO.successResponse();
}

Puede ignorar la siguiente secuencia, devolución de llamada y notificación de TCP

4. Verificar la amistad es en realidad mucho más complicado de lo que piensas


La verificación de amigos aquí se puede dividir en dos tipos, una es la verificación de amigos unidireccional, la otra es la verificación de amigos bidireccional, el código se publica aquí

// 校验好友关系
@Override
public ResponseVO checkFriendShip(CheckFriendShipReq req) {
    
    
    // 双向校验的修改
    // 1、先是把req中的所有的toIds都转化为key为属性,value为0的map
    Map<String, Integer> result
            = req.getToIds().stream().collect(Collectors.toMap(Function.identity(), s-> 0));

    List<CheckFriendShipResp> resp = new ArrayList<>();

    if(req.getCheckType() == CheckFriendShipTypeEnum.SINGLE.getType()){
    
    
        resp = imFriendShipMapper.checkFriendShip(req);
    }else{
    
    
        resp = imFriendShipMapper.checkFriendShipBoth(req);
    }

    // 2、将复杂sql查询出来的数据转换为map
    Map<String, Integer> collect = resp.stream()
            .collect(Collectors.toMap(CheckFriendShipResp::getToId,
                    CheckFriendShipResp::getStatus));

    // 3、最后比对之前result中和collect是否完全相同,collect中没有的话,就将这个数据封装起来放到resp中去
    for (String toId : result.keySet()){
    
    
        if(!collect.containsKey(toId)){
    
    
            CheckFriendShipResp checkFriendShipResp = new CheckFriendShipResp();
            checkFriendShipResp.setFromId(req.getFromId());
            checkFriendShipResp.setToId(toId);
            checkFriendShipResp.setStatus(result.get(toId));
            resp.add(checkFriendShipResp);
        }
    }

    return ResponseVO.successResponse(resp);
}

这里还要一个点,就是那个result最后和collect 里面的做一下对比,如果我们要校验的用户,不存在于数据库(双向校验在下面出现status=4的情况是,那个用户存在于数据库但是它的status为0),collect就查询不出来,也就要把那个数据也要加到resp中去,此时它的status=0

El punto importante son las dos declaraciones sql en imFriendShipMapper

checkFriendShip (comprobación unidireccional)

@Select("<script>" +
            "select from_id as fromId, to_id as toId, if(status = 1, 1, 0) as status from im_friendship where from_id = #{fromId} and to_id in " +
            "<foreach collection='toIds' index = 'index' item = 'id' separator = ',' close = ')' open = '('>" +
            "#{id}" +
            "</foreach>" +
            "</script>")
    public List<CheckFriendShipResp> checkFriendShip(CheckFriendShipReq req);

inserte la descripción de la imagen aquí

Es decir, siempre que pueda encontrarlo a través de fromId y toId, incluso si la verificación es exitosa, el resultado de la verificación será juzgado por if(status = 1, 1, 0) como estado y finalmente regresado al frente.

checkFriendShipBoth (comprobación bidireccional)

@Select("<script>" +
        "select a.fromId, a.toId, ( " +
        "case " +
        "when a.status = 1 and b.status = 1 then 1 " +
        "when a.status = 1 and b.status != 1 then 2 " +
        "when a.status != 1 and b.status = 1 then 3 " +
        "when a.status != 1 and b.status != 1 then 4 " +
        "end" +
        ")" +
        "as status from " +
        "(select from_id as fromId, to_id as toId, if(status = 1, 1, 0) as status from im_friendship where app_id = #{appId} and from_id = #{fromId} and to_id in " +
        "<foreach collection='toIds' index='index' item='id' separator=',' close=')' open='('>" +
        "#{id}" +
        "</foreach>" +
        ") as a inner join" +
        "(select from_id as fromId, to_id as toId, if(status = 1, 1, 0) as status from im_friendship where app_id = #{appId} and to_id = #{fromId} and from_id in " +
        "<foreach collection='toIds' index='index' item='id' separator=',' close=')' open='('>" +
        "#{id}" +
        "</foreach>" +
        ") as b " +
        "on a.fromId = b.toId and a.toId = b.fromId" +
        "</script>")
public List<CheckFriendShipResp> checkFriendShipBoth(CheckFriendShipReq req);

inserte la descripción de la imagen aquí

5. Agregar, eliminar, verificar la realización de negocios en la lista negra


El negocio de la lista negra de verificación aquí es similar al negocio de amigo de verificación anterior, y el código también se publica aquí

// 校验黑名单
@Override
public ResponseVO checkFriendBlack(CheckFriendShipReq req) {
    
    
    Map<String, Integer> toIdMap
            = req.getToIds().stream().collect(Collectors.toMap(Function.identity(),s -> 0));

    List<CheckFriendShipResp> resp = new ArrayList<>();

    if(req.getCheckType() == CheckFriendShipTypeEnum.SINGLE.getType()){
    
    
        resp = imFriendShipMapper.checkFriendShipBlack(req);
    }else {
    
    
        resp = imFriendShipMapper.checkFriendShipBlackBoth(req);
    }

    Map<String, Integer> collect
            = resp.stream().collect(Collectors.toMap(CheckFriendShipResp::getToId, CheckFriendShipResp::getStatus));

    for (String toId : toIdMap.keySet()) {
    
    
        if(!collect.containsKey(toId)){
    
    
            CheckFriendShipResp checkFriendShipResp = new CheckFriendShipResp();
            checkFriendShipResp.setToId(toId);
            checkFriendShipResp.setFromId(req.getFromId());
            checkFriendShipResp.setStatus(toIdMap.get(toId));
            resp.add(checkFriendShipResp);
        }
    }
    return ResponseVO.successResponse(resp);
}

checkFriendShipBlack (comprobación unidireccional)

@Select("<script>" +
       " select from_id AS fromId, to_id AS toId , if(black = 1,1,0) as status from im_friendship where app_id = #{appId} and from_id = #{fromId} and to_id in " +
        "<foreach collection='toIds' index='index' item='id' separator=',' close=')' open='('>" +
        " #{id} " +
        "</foreach>" +
        "</script>"
)
List<CheckFriendShipResp> checkFriendShipBlack(CheckFriendShipReq req);

checkFriendShipBlackBoth (comprobación bidireccional)

@Select("<script>" +
        " select a.fromId,a.toId , ( \n" +
        " case \n" +
        " when a.black = 1 and b.black = 1 then 1 \n" +
        " when a.black = 1 and b.black != 1 then 2 \n" +
        " when a.black != 1 and b.black = 1 then 3 \n" +
        " when a.black != 1 and b.black != 1 then 4 \n" +
        " end \n" +
        " ) \n " +
        " as status from "+
        " (select from_id AS fromId , to_id AS toId , if(black = 1,1,0) as black from im_friendship where app_id = #{appId} and from_id = #{fromId} AND to_id in " +
        "<foreach collection='toIds' index='index' item='id' separator=',' close=')' open='('>" +
        " #{id} " +
        "</foreach>" +
        " ) as a INNER join" +
        " (select from_id AS fromId, to_id AS toId , if(black = 1,1,0) as black from im_friendship where app_id = #{appId} and to_id = #{fromId} AND from_id in " +
        "<foreach collection='toIds' index='index' item='id' separator=',' close=')' open='('>" +
        " #{id} " +
        "</foreach>" +
        " ) as b " +
        " on a.fromId = b.toId AND b.fromId = a.toId "+
        "</script>"
)
List<CheckFriendShipResp> checkFriendShipBlackBoth(CheckFriendShipReq toId);

6. Extracción de la lista de solicitudes de amigos, solicitud de nuevos amigos, aprobación de la solicitud de amigos, lista de solicitudes de amigos, lectura de la realización del negocio


     La nueva aplicación de amigos aquí se implementa en el negocio de agregar amigos. Según un campo del usuario, si se requiere una aplicación para agregar amigos, el código es el siguiente

inserte la descripción de la imagen aquí

Y el código para aprobar la solicitud.

// 审批好友请求
@Override
@Transactional
public ResponseVO approverFriendRequest(ApproverFriendRequestReq req) {
    
    

    ImFriendShipRequestEntity imFriendShipRequestEntity = imFriendShipRequestMapper.selectById(req.getId());
    if(imFriendShipRequestEntity == null){
    
    
        throw new ApplicationException(FriendShipErrorCode. FRIEND_REQUEST_IS_NOT_EXIST);
    }

    if(!req.getOperater().equals(imFriendShipRequestEntity.getToId())){
    
    
        //只能审批发给自己的好友请求
        throw new ApplicationException(FriendShipErrorCode.NOT_APPROVER_OTHER_MAN_REQUEST);
    }

    long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.FriendshipRequest);

    ImFriendShipRequestEntity update = new ImFriendShipRequestEntity();
    // 这里审批是指同意或者拒绝,所以要写活
    update.setApproveStatus(req.getStatus());
    update.setUpdateTime(System.currentTimeMillis());
    update.setId(req.getId());
    update.setSequence(seq);
    imFriendShipRequestMapper.updateById(update);

    writeUserSeq.writeUserSeq(req.getAppId(),req.getOperater(), Constants.SeqConstants.FriendshipRequest,seq);

    // 如果是统一的话,就可以直接调用添加好友的逻辑了
    if(ApproverFriendRequestStatusEnum.AGREE.getCode() == req.getStatus()){
    
    
        FriendDto dto = new FriendDto();
        dto.setAddSource(imFriendShipRequestEntity.getAddSource());
        dto.setAddWorking(imFriendShipRequestEntity.getAddWording());
        dto.setRemark(imFriendShipRequestEntity.getRemark());
        dto.setToId(imFriendShipRequestEntity.getToId());
        ResponseVO responseVO = imFriendShipService.doAddFriend(req
                , imFriendShipRequestEntity.getFromId(), dto, req.getAppId());
        if(!responseVO.isOk() && responseVO.getCode() != FriendShipErrorCode.TO_IS_YOUR_FRIEND.getCode()){
    
    
            return responseVO;
        }
    }

    // TODO TCP通知
    // 通知审批人的其他端
    ApproverFriendRequestPack approverFriendRequestPack = new ApproverFriendRequestPack();
    approverFriendRequestPack.setStatus(req.getStatus());
    approverFriendRequestPack.setId(req.getId());
    approverFriendRequestPack.setSequence(seq);
    messageProducer.sendToUser(imFriendShipRequestEntity.getToId(), req.getClientType(), req.getImei(),
            FriendshipEventCommand.FRIEND_REQUEST_APPROVER, approverFriendRequestPack, req.getAppId());

    return ResponseVO.successResponse();
}

7. Introducción al negocio del grupo de amigos y diseño de la base de datos.


inserte la descripción de la imagen aquí

El de la izquierda en la imagen de arriba es para WeChat. Un usuario puede estar en varios grupos. El de la derecha es para QQ. Un usuario solo puede estar en un grupo. Este sistema se implementa a la izquierda, por lo que necesitamos para diseñar una base de datos

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

8. Realización de la creación, adquisición, adición, eliminación de miembros y eliminación grupal de grupos de amigos.


      Esta parte se centra en una unión. Por ejemplo, para crear un grupo de amigos, debe agregar miembros, eliminar el grupo de amigos y también debe borrar los miembros del grupo. Al agregar miembros del grupo, también debe obtener el grupo. , que está muy acoplado.

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

9. El módulo más complejo de mensajería instantánea: análisis empresarial del módulo grupal y diseño de base de datos.


Los chats individuales no pueden ser tan animados como los chats grupales, por lo que debemos implementar chats grupales


下面是腾讯云

inserte la descripción de la imagen aquí

Este sistema implementa estos dos tipos de grupos

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

10. Realización de negocios de grupos de importación y miembros del grupo


nada que decir aquí

11. Cree grupos, modifique la información del grupo y obtenga funciones comerciales de información del grupo


Complejo y altamente acoplado

12. Realización de la función comercial de obtención de la lista de grupos a los que se unieron los usuarios


No hay nada aquí, solo consulte este group_member para encontrar el grupo al que se unió el usuario

@Select("select group_id from im_group_member where app_id = #{appId} and member_id = #{memberId}")
List<String> getJoinedGroupId(Integer appId, String memberId);

13. Realización de funciones comerciales de grupos disueltos y grupos transferidos


levemente

14. Se realizan las funciones comerciales de atraer personas al grupo, salir del chat grupal y salir del chat grupal.


levemente

15. Adquirir información de miembros del grupo, modificar información de miembros del grupo realización de funciones comerciales


levemente

16. Realización de funciones comerciales del grupo mudo y miembros del grupo mudo


levemente

3. Introducción a BIO, NIO y Netty

1, BIO, NIÑO


Esto se puede ver en mi otro artículo Modelo de subprocesos IO

2, red


Esta cosa es muy grande, aquí hay una pequeña explicación básica.


官网:Netty es un marco de aplicación de red asíncrono basado en eventos
para el desarrollo rápido de servidores y clientes de protocolo de alto rendimiento que se pueden mantener.

     Netty es un marco de aplicación de red asíncrono basado en eventos. Para el desarrollo rápido de clientes y servidores de protocolo de alto rendimiento que se pueden mantener.


官网:Netty es un marco de servidor de cliente NIO que permite el desarrollo rápido y fácil de aplicaciones de red, como servidores de protocolo y clientes. Simplifica y agiliza en gran medida la programación de redes, como el servidor de socket TCP y UDP.

      Netty es un marco de trabajo cliente-servidor de NIO que permite el desarrollo rápido y sencillo de aplicaciones de red, como servidores de protocolo y clientes. Simplifica y optimiza enormemente la programación de redes, como servidores de socket TCP y UDP.


inserte la descripción de la imagen aquí


¿En qué escenarios de aplicación se utilizará Netty?

  1. Desarrolle cualquier programación de red e implemente su propio marco rpc
  2. Se puede utilizar como componente intermediario de algunos protocolos públicos, como mqtt, http
  3. Muchos marcos de código abierto y comunicación entre grandes campos de datos también usarán netty

4. Habilidades que deben dominarse en el desarrollo empresarial de Netty

1. Usa netty para implementar una sala de chat simple


DiscardServerHandlerDiscardServerHandler

public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    
    

    static Set<Channel> channelList = new HashSet<>();

    // 有客户端连接进来就触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        // 通知其他人我上线了
        channelList.forEach((e)->{
    
    
            e.writeAndFlush("[客户端]" + ctx.channel().remoteAddress() + "上线了");
        });
        channelList.add(ctx.channel());
    }

    // 有读写事件发生的时候触发这个方法
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        String message = (String) msg;

        System.out.println("收到数据: " + message);
//        // 通知分发给聊天室内所有的客户端
//        channelList.forEach((e)->{
    
    
//            if(e == ctx.channel()){
    
    
//                e.writeAndFlush("[自己]: " + message);
//            }else{
    
    
//                e.writeAndFlush("[客户端]:" + ctx.channel().remoteAddress() + "     " + message);
//            }
//        });
    }

    /**
     *  channel 处于不活跃的时候会调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    
    
        // 通知其他客户端 我下线了
        channelList.remove(ctx.channel());
        // 通知其他人我上线了
        channelList.forEach((e)->{
    
    
            e.writeAndFlush("[客户端]" + ctx.channel().remoteAddress() + "下线了");
        });
    }
}

Lo principal es escribir Handler, y la lógica compleja se puede hacer con algunas API.

descartar servidor

public class DiscardServer {
    
    

    private int port;

    public DiscardServer(int port){
    
    
        this.port = port;
    }

    public void run(){
    
    
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
    
     // (4)
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
    
    
                            ch.pipeline().addLast(new DiscardServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            System.out.println("tcp start success");
            ChannelFuture f = b.bind(port).sync(); // (7)


            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

Inicio

public class Starter {
    
    
    public static void main(String[] args) {
    
    
        new DiscardServer(8001).run();
    }
}

2. Códec Netty


Asistente de depuración de red——"sistema operativo—"red—"el sistema operativo de la otra parte——" encuentre el proceso correspondiente (pasado en el pasado no es una cadena)

El asistente de depuración de red se utiliza aquí

La capa inferior de Netty solo reconoce ByteBuf. No podemos enviar cadenas directamente al cliente, por lo que debemos agregar algunos códigos de codificación y decodificación al servidor, y luego no tenemos que decodificarlos nosotros mismos cuando recibimos mensajes. y podemos usarlos directamente.

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

3. El núcleo del flujo de datos subyacente: mecanismo de tubería


inserte la descripción de la imagen aquí

public class DiscardServer {
    
    
    private int port;
    public DiscardServer(int port){
    
    
        this.port = port;
    }

    public void run(){
    
    
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
    
     // (4)
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
    
    
                            Charset gbk = Charset.forName("GBK");
                            ch.pipeline().addLast("decoder", new StringDecoder(gbk));
                            ch.pipeline().addLast("encoder", new StringEncoder(gbk));
                            ch.pipeline().addLast(new DiscardServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            System.out.println("tcp start success");
            ChannelFuture f = b.bind(port).sync(); // (7)


            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

inserte la descripción de la imagen aquí

里面的这些Handler都要注意位置

4. El problema que nos dejó el protocolo de la capa de transporte TCP - Netty resuelve el problema de half-packet y sticky-packet


4.1 Problemas en la transmisión TCP (medio paquete, paquete pegajoso)


Inicie el programa de la sala de chat aquí e inicie un script de python para enviar mensajes al servidor en un bucle

pitón

import socket

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8001))

for i in range(100):
    print(i)
    string = "hello1哈"
    body = bytes(string, 'gbk')
    s.sendall(body)

       Cuando ejecutamos este script, veremos información en la consola del servidor, lo que veremos deberían ser 100 líneas de saludo, lo cual debería ser razonable, pero después de la ejecución, encontraremos que se muestran 100 mensajes en la misma línea. , la segunda vez algunos están en la misma línea, y algunos están en la misma línea

inserte la descripción de la imagen aquí

primero enviar

inserte la descripción de la imagen aquí

segundo envío

      La razón de este fenómeno es que la transmisión TCP se envía en forma de transmisión. A veces se envía un conjunto completo y, a veces, se envía un dato. Cómo resolver este problema

4.2 ¿Cómo resuelve Netty el medio paquete y el paquete pegajoso?


primera solucion

Puede agregar algo a la canalización del servidor para limitar la cantidad de bytes leídos. La desventaja es que es posible que deba considerar el tamaño de los datos.

inserte la descripción de la imagen aquí

segunda solucion

Agregue este símbolo de división. La desventaja de esto es que la cadena dividida no puede aparecer en los datos para ser leídos seriamente.

inserte la descripción de la imagen aquí

5. El problema que nos dejó el protocolo de capa de transporte TCP: usar un protocolo privado para resolver las API subyacentes de medio paquete, sticky-packet y byteBuf


Aquí hay un protocolo privado para resolverlo, es decir, por ejemplo 6123456, los primeros 6 son para leer los siguientes 6 números

inserte la descripción de la imagen aquí

Aquí mencionamos primero la API central de ByteBuf

public class NettyByteBuf {
    
    
    public static void main(String[] args) {
    
    
        // 创建byteBuf对象,该对象内部包含一个字节数组byte[10]
        ByteBuf byteBuf = Unpooled.buffer(10);
        System.out.println("byteBuf=" + byteBuf);

        for (int i = 0; i < 8; i++) {
    
    
            byteBuf.writeByte(i);
        }
        System.out.println("byteBuf=" + byteBuf);

        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(byteBuf.getByte(i));
        }
        System.out.println("byteBuf=" + byteBuf);

        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(byteBuf.readByte());
        }
        System.out.println("byteBuf=" + byteBuf);

        System.out.println(byteBuf.readableBytes());
    }
}

inserte la descripción de la imagen aquí

A partir de los resultados de la consola anterior, no es difícil ver que ridx significa dónde se ha leído, cuánto ancho ha ocupado y cap es la capacidad total.

inserte la descripción de la imagen aquí

Ridx es el índice de lectura, widx es el índice de escritura

API comunes efecto
Unpooled.buffer(10) Crear una matriz de bytes[10]
byteBuf.writeByte(i) Escribe i en byteBuf
byteBuf.getByte(i) Obtenga el i-ésimo byte en btyeBuf, el índice de lectura no cambia
byteBuf.readByte() Lee bytes desde el principio y el índice de lectura retrocede automáticamente
byteBuf.readableBytes() Obtener los bytes que no se han leído en byteBuf
byteBuf.markReaderIndex() Registre la ubicación del índice de lectura
byteBuf.resetReaderIndex() Devuelve la posición del índice de lectura del registro.
// 继承了这个类就可以去 自定义协议了
public class MyDecodecer extends ByteToMessageDecoder {
    
    

    // 数据长度 + 数据
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
    
    

        // 一个int是4字节,可读长度要大于4才可以继续执行
        if(byteBuf.readableBytes() < 4){
    
    
            return;
        }

        // 数据长度
        int i = byteBuf.readInt();
        if(byteBuf.readableBytes() < i){
    
    
            byteBuf.resetReaderIndex();
            return;
        } 

        // 开辟一个byte数组去接收数据
        byte[] data = new byte[i];
        byteBuf.readBytes(data);
        System.out.println(new String(data));
        byteBuf.markReaderIndex();
    }
}

Para que pueda personalizar un protocolo privado para leer datos de acuerdo con sus reglas, ¡recuerde poner esto en proceso!

inserte la descripción de la imagen aquí

De esta manera, se puede resolver el problema de la mitad del paquete y el paquete pegajoso.

6. Explicación detallada del código fuente del mecanismo de latido IdleStateHandler


Primero puede comprender la conexión corta y la conexión larga Conexión larga HTTP y conexión corta

public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
    
    
    int readTimeout = 0;

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
    
//        // IdleStateEven 超时类型
        IdleStateEvent event = (IdleStateEvent) evt;
        // ALL_IDLE : 一段时间内没有数据接收或者发送
        // READER_IDLE : 一段时间内没有数据接收
        // WRITER_IDLE : 一段时间内没有数据发送
        if(event.state() == IdleState.READER_IDLE){
    
    
            readTimeout++;
        }

        if(readTimeout >= 3){
    
    
            System.out.println("超时超过3次,断开连接");
            ctx.close();
        }

        System.out.println("触发了:" + event.state() + "事件");
    }
}

inserte la descripción de la imagen aquí

El efecto de esta implementación es que se activará una detección de latido una vez que el tiempo de espera de lectura sea de 3 segundos, y la lógica es que la conexión se desconectará si excede tres veces

7. Usa Netty para subir y subir archivos

==Descodificador de archivo de carga ==

public class UploadFileDecodecer extends ByteToMessageDecoder {
    
    

    // 数据长度 + 数据
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
    
    

        // 一个int是4字节,可读长度要大于4才可以继续执行
        if(byteBuf.readableBytes() < 8){
    
    
            return;
        }

        // 数据长度
        int command = byteBuf.readInt();

        FileDto fileDto = new FileDto();
        fileDto.setCommand(command);

        // 文件名长度
        int fileNameLen = byteBuf.readInt();
        if(byteBuf.readableBytes() < fileNameLen){
    
    
            byteBuf.resetReaderIndex();
            return;
        }

        // 开辟一个byte数组去接收数据
        byte[] data = new byte[fileNameLen];
        byteBuf.readBytes(data);
        String fileName = new String(data);
        fileDto.setFileName(fileName);

        if(command == 2){
    
    
            int dataLen = byteBuf.readInt();
            if(byteBuf.readableBytes() < dataLen){
    
    
                byteBuf.resetReaderIndex();
                return;
            }
            byte[] fileData = new byte[dataLen];
            byteBuf.readBytes(fileData);
            fileDto.setBytes(fileData);
        }

        byteBuf.markReaderIndex();
        list.add(fileDto);
    }
}

Coloque esta parte en la tubería y colóquela frente a UploadFileHandler, donde el comando y el nombre del archivo, los datos específicos del archivo se analizan a través del protocolo personalizado y luego se encapsulan en FileDto y finalmente se colocan en la tubería. para uso posterior

UploadFileHandler

public class UploadFileHandler extends ChannelInboundHandlerAdapter {
    
    

    // 有客户端连接进来就触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
    }

    // 有读写事件发生的时候触发这个方法
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        if(msg instanceof FileDto){
    
    
            FileDto fileDto = (FileDto) msg;
            if(fileDto.getCommand() == 1){
    
    
                // 创建文件
                File file = new File("E://" + fileDto.getFileName());
                if(!file.exists()){
    
    
                    file.createNewFile();
                }
            }else if(fileDto.getCommand() == 2){
    
    
                // 写入文件
                save2File("E://" + fileDto.getFileName(), fileDto.getBytes());
            }
        }
    }

    public static boolean save2File(String fname, byte[] msg){
    
    
        OutputStream fos = null;
        try{
    
    
            File file = new File(fname);
            File parent = file.getParentFile();
            boolean bool;
            if ((!parent.exists()) &
                    (!parent.mkdirs())) {
    
    
                return false;
            }
            fos = new FileOutputStream(file,true);
            fos.write(msg);
            fos.flush();
            return true;
        }catch (FileNotFoundException e){
    
    
            return false;
        }catch (IOException e){
    
    
            File parent;
            return false;
        }
        finally{
    
    
            if (fos != null) {
    
    
                try{
    
    
                    fos.close();
                }catch (IOException e) {
    
    }
            }
        }
    }
}

Aquí se usa en el FileDto obtenido del lugar de decodificación, créelo si no está disponible y escríbalo si lo está

inserte la descripción de la imagen aquí

Puede usar el siguiente script de python para probar

#-*- coding: UTF-8 -*-
import socket,os,struct
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("127.0.0.1",8001))

filepath = "D://txt.txt"
if os.path.isfile(filepath):
    
    filename = os.path.basename(filepath).encode('utf-8')

    # 请求传输文件
    command = 1
    
    body_len = len(filename)
    fileNameData = bytes(filename)
    i = body_len.to_bytes(4, byteorder='big')
    c = command.to_bytes(4, byteorder='big')

    s.sendall(c + i + fileNameData) 

    fo = open(filepath,'rb')
    while True:
      command = 2;
      c = command.to_bytes(4, byteorder='big')
      filedata = fo.read(1024)
      print(len(filedata))
      b = len(filedata).to_bytes(4, byteorder='big')
      if not filedata:
        break
      s.sendall(c + i + fileNameData + b + filedata)
    fo.close()
    #s.close()
else:
    print(False)

Supongo que te gusta

Origin blog.csdn.net/weixin_52487106/article/details/130538843
Recomendado
Clasificación