Sistema de mensagens instantâneas IM [SpringBoot+Netty] - combinação (1)

Diretório de artigos

código-fonte do projeto

Índice
Sistema de mensagens instantâneas IM [SpringBoot+Netty] - combinação (2)
Sistema de mensagens instantâneas IM [SpringBoot+Netty] - combinação (3)
Sistema de mensagens instantâneas IM [SpringBoot+Netty] - combinação (4)
Sistema de mensagens instantâneas IM [SpringBoot+Netty] - combinação (5)

1. Por que desenvolver um sistema de mensagens instantâneas autodesenvolvido

1. Quais são as formas de implementar um sistema de mensagens instantâneas


Primeiro, olhe para o sistema im do mercado (nada mais do que essas três maneiras):

  1. Use produtos de código aberto para desenvolvimento secundário ou uso direto
  2. Use um provedor de serviços de nuvem pago
  3. Auto estudo

1.1. Use produtos de código aberto para desenvolvimento secundário ou uso direto


优点: Você pode começar rapidamente, use

缺点: Falta de funções, pouca sustentabilidade, nenhuma equipe para manutenção e expansão posteriores, independentemente de corresponder à pilha de tecnologia da sua empresa

1.2. Usando provedores de serviços de nuvem pagos


优点: Não há necessidade de desenvolver um sistema im, nem precisa de um servidor de operação e manutenção. Os provedores de serviços em grande escala têm tecnologia relativamente madura e alta confiabilidade de transmissão de mensagens. De acordo com as bibliotecas sdk e ui oficiais dos provedores de serviços, é é fácil adicionar funções im aos seus próprios serviços

缺点: É impossível espionar o código-fonte do provedor de serviços (código fechado) e é difícil atender às necessidades personalizadas. Se a extensão oficial não atender às suas necessidades, basicamente não há solução. Informações e dados são importantes ativos. As mãos dos outros não são muito boas, o custo do serviço é alto

1.3. Autodesenvolvimento


优点: Desenvolva de acordo com a pilha de tecnologia da empresa, não se preocupe com pós-manutenção, personalize suas próprias necessidades e a segurança dos dados é protegida

缺点: Precisa ser desenvolvido por alguém que esteja familiarizado com o sistema im, e há certos requisitos para o nível técnico, e o custo da mão de obra aumenta



2. Como desenvolver um sistema de mensagens instantâneas autodesenvolvido

2.1. Como o primeiro sistema de mensagens instantâneas foi criado

insira a descrição da imagem aqui

Esta é a arquitetura técnica implementada pelo serviço ao cliente Jingdong no início

Essa arquitetura causará desperdício de recursos e a pesquisa não será interrompida quando não houver mensagem para enviar

2.2. A composição básica de um sistema de mensagens instantâneas


insira a descrição da imagem aqui

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

  • 服务层

    • 接入层: O portal do sistema im é um 核心módulo comparativo no sistema im, que mantém o link longo entre o cliente e o servidor. A mensagem é enviada do cliente para a camada de acesso, e a camada de acesso a entrega à lógica camada para processamento; : a primeira 接入层主要分为四个功能é Manter um link longo, a segunda é a análise do protocolo, a terceira é a manutenção da nossa sessão e a quarta é o push da mensagem; quando o processamento da mensagem é concluído, ele também é entregue ao cliente pelo camada de acesso; entre a camada de acesso e o cliente 必须Deve haver um protocolo (protocolo da camada de aplicação: protocolo de texto e protocolo binário - MQTT, XMPP, HTPP e outros protocolos; protocolo privado)
    • 逻辑层: Um módulo após o outro do sistema de negócios: usuários, cadeias de relacionamento, grupos, mensagens
  • 存储层:MySQL, Redis

2.3. A atual arquitetura comum dos sistemas de mensagens instantâneas


insira a descrição da imagem aqui

  • A conexão longa está enviando e recebendo mensagens imediatamente, e a mensagem pode ser entregue diretamente ao usuário por meio da conexão longa. Em comparação com a votação longa, muitos loops vazios são evitados (consulte este artigo: Quatro formas de comunicação na web )

  • As camadas de acesso e lógica podem ser acessadas por meio rpc调用demq解耦

  • As principais camadas de persistência conectadas pela camada lógica completam o trabalho de persistência

2.4. Resumo

接入层: Para manter a conexão longa do nosso cliente e o envio e recebimento de mensagens, o protocolo pode considerar o uso do protocolo TCP (confiável); escolha um adequado (MQTT, XMPP, protocolo privado); a 应用层协议camada de acesso também precisa manter o sessão do usuário e conectar A camada de acesso é diferente do desenvolvimento tradicional da Web. A camada de acesso é um serviço com estado, enquanto o http tradicional é um serviço sem estado.

逻辑层: processe a lógica central do envio e recebimento de mensagens e coopere com a camada de acesso e a camada de armazenamento para garantir que as mensagens não sejam perdidas, vazadas ou agrupadas

存储层:Deve ter um design razoável, fornecer serviços de dados para a camada lógica e ser capaz de transportar uma grande quantidade de dados de registro de bate-papo



2. Desenvolvimento de dados básicos

1. Importar informações do usuário, excluir informações do usuário, modificar informações do usuário, consultar informações do usuário


Aqui acho um bom lugar, usando a lógica de importar dados do usuário como demonstração:

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Então aqui estão algumas lógicas de adição, exclusão, modificação e verificação, então não vou escrever aqui, saberei o significado geral depois de passar por isso sozinho, e será o mesmo mais tarde

2. Os dados mais valiosos em mensagens instantâneas—análise de negócios e design de banco de dados do módulo da cadeia de relacionamento


2.1. Os dados mais valiosos - cadeia de amizade

       Por que você diz isso? Veja, por que o WeChat e o QQ são tão fortes? É porque eles têm seus amigos entre eles. Se você mudar para outro software de bate-papo, perderá todos esses amigos. Você acha que isso é de alto valor?

2.2. Amizade

  1. Amizade Fraca: Inscrevendo-se no Weibo
  2. Amizade forte: como WeChat (o método usado neste sistema)

2.3. Projeto de banco de dados

  • Design de relacionamento amigável fraco e bom:
    insira a descrição da imagem aqui

  • Projeto de amizade forte:

insira a descrição da imagem aqui

  • design final
    insira a descrição da imagem aqui

3. As funções de importar, adicionar, atualizar amigos, deletar amigos, deletar todos os amigos, puxar amigos especificados e puxar todos os amigos são realizadas


Aqui está um código lógico específico para adicionar amigos.Outros são semelhantes a essa ideia geral.

// 添加好友的逻辑
@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();
}

Você pode ignorar a seguinte notificação de sequência, retorno de chamada e TCP

4. Verificar a amizade é realmente muito mais complicado do que você pensa


A verificação de amigo aqui pode ser dividida em dois tipos, uma é verificação de amigo unidirecional, a outra é verificação de amigo bidirecional, o código é postado aqui

// 校验好友关系
@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

O ponto importante são as duas instruções sql em imFriendShipMapper

checkFriendShip (cheque unidirecional)

@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);

insira a descrição da imagem aqui

Ou seja, desde que eu consiga encontrá-lo por meio de fromId e toId, mesmo que a verificação seja bem-sucedida, o resultado da verificação será julgado por if(status = 1, 1, 0) como status e, finalmente, retornado à frente

checkFriendShipBoth (cheque bidirecional)

@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);

insira a descrição da imagem aqui

5. Adicione, exclua, verifique a realização de negócios da lista negra


A empresa de lista negra de verificação aqui é semelhante à empresa de verificação de amigos acima, e o código também é postado aqui

// 校验黑名单
@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 (cheque unidirecional)

@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 (verificação bidirecional)

@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. Retirada da lista de aplicativos de amigos, novos aplicativos de amigos, aprovação de aplicativos de amigos, lista de aplicativos de amigos, leitura da realização de negócios


     O novo aplicativo de amigo aqui é implementado no negócio de adicionar amigos. De acordo com um campo do usuário, se um aplicativo é necessário para adicionar amigos, o código é o seguinte

insira a descrição da imagem aqui

E o código para aprovação do aplicativo

// 审批好友请求
@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. Apresentação do negócio do grupo de amigos e design do banco de dados


insira a descrição da imagem aqui

O da esquerda na imagem acima é para WeChat. Um usuário pode estar em vários grupos. O da direita é para QQ. Um usuário só pode estar em um grupo. Este sistema é implementado à esquerda, então precisamos para projetar um banco de dados

insira a descrição da imagem aqui

insira a descrição da imagem aqui

8. Realização da criação, aquisição, adição, exclusão de membros e exclusão de grupo de grupos de amigos


      Esta parte se concentra em uma união. Por exemplo, para criar um grupo de amigos, você precisa adicionar membros, excluir o grupo de amigos e também limpar os membros do grupo. Ao adicionar membros do grupo, você também precisa obter o grupo , que é muito acoplado.

insira a descrição da imagem aqui
insira a descrição da imagem aqui

9. O módulo mais complexo de mensagens instantâneas - análise de negócios do módulo de grupo e design de banco de dados


Os bate-papos individuais não podem ser tão animados quanto os bate-papos em grupo, por isso precisamos implementar os bate-papos em grupo


下面是腾讯云

insira a descrição da imagem aqui

Este sistema implementa esses dois tipos de grupos

insira a descrição da imagem aqui
insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui
insira a descrição da imagem aqui

10. Grupo de importação e realização de negócios de membros do grupo


nada a dizer aqui

11. Crie grupos, modifique informações de grupo e obtenha funções de negócios de informações de grupo


Complexo e altamente acoplado

12. Realização da função de negócio de obtenção da lista de grupos ingressados ​​por usuários


Não há nada aqui, apenas consulte este group_member para encontrar o grupo ao qual o usuário ingressou

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

13. Realização de funções de negócios de grupos de dissolução e transferência de grupos


um pouco

14. As funções de negócios de atrair pessoas para o grupo, sair do bate-papo em grupo e sair do bate-papo em grupo são realizadas


um pouco

15. Adquirir informações do membro do grupo, modificar a realização da função de negócios das informações do membro do grupo


um pouco

16. Realização de funções de negócios do grupo mudo e membros do grupo mudo


um pouco

3. Introdução ao BIO, NIO e Netty

1、BIO、NIO


Isso pode ser visto em meu outro artigo IO threading model

2、netty


Essa coisa é muito grande, aqui está uma pequena explicação básica


官网:Netty é uma estrutura de aplicativo de rede assíncrona orientada a eventos
para desenvolvimento rápido de servidores e clientes de protocolo de alto desempenho sustentáveis.

     Netty é uma estrutura de aplicativo de rede assíncrona orientada a eventos. Para desenvolvimento rápido de servidores e clientes de protocolo de alto desempenho sustentáveis.


官网:Netty é uma estrutura de servidor cliente NIO que permite o desenvolvimento rápido e fácil de aplicativos de rede, como servidores de protocolo e clientes. Simplifica e agiliza muito a programação de rede, como servidor de soquete TCP e UDP.

      Netty é uma estrutura cliente-servidor NIO que permite o desenvolvimento rápido e fácil de aplicativos de rede, como servidores de protocolo e clientes. Simplifica e otimiza muito a programação de rede, como servidores de soquete TCP e UDP.


insira a descrição da imagem aqui


Em quais cenários de aplicação o Netty será usado?

  1. Desenvolva qualquer programação de rede e implemente sua própria estrutura rpc
  2. Ele pode ser usado como um componente intermediário de alguns protocolos públicos, como mqtt, http
  3. Muitas estruturas de código aberto e comunicação entre campos de big data também usarão netty

4. Habilidades que devem ser dominadas no desenvolvimento empresarial Netty

1. Use o netty para implementar uma sala de bate-papo simples


DiscardServerHandler

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() + "下线了");
        });
    }
}

O principal é escrever o Handler, e a lógica complexa pode ser feita com algumas APIs

DiscardServer

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

Iniciante

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

2. Codec Netty


Assistente de depuração de rede——"sistema operacional—"rede—"sistema operacional da outra parte——" localize o processo correspondente (passado no passado não é uma string)

O assistente de depuração de rede é usado aqui

A camada inferior do Netty reconhece apenas o ByteBuf. Não podemos enviar strings diretamente para o cliente, portanto, precisamos adicionar alguns códigos de codificação e decodificação ao servidor e não precisamos decodificá-los quando recebermos mensagens, e podemos usá-los diretamente.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

3. O núcleo do fluxo de dados subjacente - mecanismo de pipeline


insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

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

4. O problema deixado para nós pelo protocolo da camada de transporte TCP - Netty resolve o problema de meio-pacote e sticky-packet


4.1. Problemas na transmissão TCP (meio pacote, sticky packet)


Inicie o programa da sala de bate-papo aqui e inicie um script python para enviar mensagens ao servidor em um loop

Pitão

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)

       Ao executarmos este script, veremos informações no console do servidor, o que vemos deveriam ser 100 linhas de hello, o que deveria ser razoável, mas após a execução, veremos que 100 mensagens são todas exibidas na mesma linha. , na segunda vez, alguns estão na mesma linha e alguns estão na mesma linha

insira a descrição da imagem aqui

primeiro envio

insira a descrição da imagem aqui

segundo envio

      A razão para este fenômeno é que a transmissão TCP é enviada de forma contínua. Às vezes, um conjunto completo é enviado e, às vezes, um pedaço de dados é enviado. Como resolver este problema

4.2 Como o Netty resolve o meio pacote e o pacote adesivo


primeira solução

Você pode adicionar algo ao pipeline do servidor para limitar o número de bytes lidos. A desvantagem é que você pode ter que considerar o tamanho dos dados

insira a descrição da imagem aqui

segunda solução

Adicione este símbolo de divisão. A desvantagem disso é que a string de divisão não pode aparecer nos dados a serem lidos seriamente.

insira a descrição da imagem aqui

5. O problema deixado para nós pelo protocolo da camada de transporte TCP - usando um protocolo privado para resolver APIs subjacentes de meio pacote, pacote fixo e byteBuf


Aqui está um protocolo privado para resolvê-lo, ou seja, por exemplo 6123456, os 6 primeiros são para ler os próximos 6 números

insira a descrição da imagem aqui

Aqui mencionamos primeiro a API principal do 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());
    }
}

insira a descrição da imagem aqui

A partir dos resultados do console acima, não é difícil ver que ridx significa onde foi lido, quanto widx ocupou e cap é a capacidade total

insira a descrição da imagem aqui

Ridx é o índice de leitura, widx é o índice de gravação

APIs comuns efeito
Unpooled.buffer(10) Crie uma matriz de bytes[10]
byteBuf.writeByte(i) Escreva i para byteBuf
byteBuf.getByte(i) Obtenha o i-ésimo byte em btyeBuf, o índice de leitura não muda
byteBuf.readByte() Leia os bytes desde o início e o índice de leitura retrocederá automaticamente
byteBuf.readableBytes() Pega os bytes que não foram lidos em byteBuf
byteBuf.markReaderIndex() Registre a localização do índice de leitura
byteBuf.resetReaderIndex() Retorna a posição do índice lido do 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();
    }
}

Assim, você pode personalizar um protocolo privado para ler os dados de acordo com suas regras, lembre-se de colocar isso no pipeline!

insira a descrição da imagem aqui

Desta forma, o problema de meio pacote e pacote pegajoso pode ser resolvido

6. Explicação detalhada do código-fonte do mecanismo de heartbeat IdleStateHandler


Você pode primeiro entender a conexão curta e a conexão longa HTTP, a conexão longa e a conexão curta

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() + "事件");
    }
}

insira a descrição da imagem aqui

O efeito dessa implementação é que uma detecção de pulsação será acionada assim que o tempo limite de leitura for de 3 segundos, e a lógica é que a conexão será desconectada se exceder três vezes

7. Use o Netty para fazer upload e upload de arquivos

==UploadFileDecodecer ==

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 no pipeline e coloque-a na frente de UploadFileHandler, onde o comando e o nome do arquivo, os dados específicos do arquivo são analisados ​​por meio do protocolo personalizado e, em seguida, encapsulados em FileDto e, finalmente, colocados no pipeline 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) {
    
    }
            }
        }
    }
}

Aqui é usado no FileDto obtido do local de decodificação, crie-o se não estiver disponível e escreva-o se estiver

insira a descrição da imagem aqui

Você pode usar o seguinte script python para testar

#-*- 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)

Acho que você gosta

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