即时通讯增加Redis渠道

news2024/10/22 14:57:10

情况说明

在本地和服务器分别启动im服务,当本地发送消息时,会发现服务器上并没有收到消息

初版im只支持单机版,不支持分布式的情况。此次针对该情况对项目进行优化,文档中贴出的代码非完整代码,可自行查看参考资料[2]

代码结构调整

本次调整需要增加一个redis的渠道,为了方便后续再进行渠道的增加,对现有代码结构进行调整

  • IBaseSendExecutor

渠道扩充接口,后续再增加渠道都可以实现该接口

package com.example.im.infra.executor.send;

/**
 * @author PC
 * 通信处理
 */
public interface IBaseSendExecutor {
    /**
     * 获取通信类型,预置的有默认和redis
     *
     * @return 通讯类型
     */
    String getCommunicationType();

    /**
     * 发送给指定人
     *
     * @param sendUserName 发送人
     * @param message      消息
     */
    void sendToUser(String sendUserName, String message);

    /**
     * 发送给全部人
     *
     * @param sendUserName 发送人
     * @param message      消息
     */
    void sendToAll(String sendUserName, String message);
}
  • AbstractBaseSendExecutor

通信处理抽象类,将一些预定义的渠道所需要的公有方法提取出来

package com.example.im.infra.executor.send;

import com.example.im.config.WebSocketProperties;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author PC
 */
public abstract class AbstractBaseSendExecutor implements IBaseSendExecutor {

    protected WebSocketProperties webSocketProperties;

    @Autowired
    public void setWebSocketProperties(WebSocketProperties webSocketProperties) {
        this.webSocketProperties = webSocketProperties;
    }


    /**
     * 获取接收人信息
     *
     * @param sendUserName 发送人
     * @param message      消息
     * @return 接收人列表
     */
    protected List<String> getReceiverName(String sendUserName, String message) {
        if (!StringUtils.contains(message, webSocketProperties.getReceiverSeparator())) {
            return new ArrayList<>();
        }
        String[] names = StringUtils.split(message, webSocketProperties.getReceiverSeparator());
        return Stream.of(names).skip(1).filter(receiver ->
                        !(webSocketProperties.getReceiverExcludesHimselfFlag() && StringUtils.equals(sendUserName, receiver)))
                .collect(Collectors.toList());
    }

    /**
     * 根据配置处理发送的信息
     *
     * @param message 原消息
     * @return 被处理后的消息
     */
    protected String generatorMessage(String message) {
        return BooleanUtils.isTrue(webSocketProperties.getExcludeReceiverInfoFlag()) ?
                StringUtils.substringBefore(message, webSocketProperties.getReceiverSeparator()) : message;
    }
}
  • DefaultSendExecutor

原有消息发送逻辑

package com.example.im.infra.executor.send;

import com.example.im.endpoint.WebSocketEndpoint;
import com.example.im.infra.constant.ImConstants;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

/**
 * @author PC
 * 默认执行
 */
@Component
public class DefaultSendExecutor extends AbstractBaseSendExecutor {
    private final static Logger logger = LoggerFactory.getLogger(DefaultSendExecutor.class);

    private TaskExecutor taskExecutor;

    @Autowired
    public void setTaskExecutor(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public String getCommunicationType() {
        return ImConstants.CommunicationType.DEFAULT;
    }

    @Override
    public void sendToUser(String sendUserName, String message) {
        List<String> receiverNameList = getReceiverName(sendUserName, message);
        CountDownLatch countDownLatch = new CountDownLatch(receiverNameList.size());
        Set<String> notOnlineReceiverSet = ConcurrentHashMap.newKeySet();
        Set<String> finalNotOnlineReceiverSet = notOnlineReceiverSet;
        receiverNameList.forEach(receiverName -> taskExecutor.execute(() -> {
                            try {
                                if (WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.containsKey(receiverName)) {
                                    WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.get(receiverName).getSession().getBasicRemote()
                                            .sendText(generatorMessage(message));
                                } else {
                                    finalNotOnlineReceiverSet.add(receiverName);
                                }
                            } catch (IOException ioException) {
                                logger.error("send error:" + ioException);
                            } finally {
                                countDownLatch.countDown();
                            }
                        }
                )
        );
        try {
            countDownLatch.await();
        } catch (InterruptedException interruptedException) {
            logger.error("error.countDownLatch.await");
        }
        notOnlineReceiverSet = notOnlineReceiverSet.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toSet());
        if (CollectionUtils.isNotEmpty(notOnlineReceiverSet)) {
            logger.info("not online number is " + notOnlineReceiverSet.size());
            logger.info("The user : {} is not online", String.join(",", notOnlineReceiverSet));
        }
    }

    @Override
    public void sendToAll(String sendUserName, String message) {
        for (Map.Entry<String, WebSocketEndpoint> webSocketEndpointEntry : WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.entrySet()) {
            taskExecutor.execute(() -> {
                        if (webSocketProperties.getReceiverExcludesHimselfFlag() && StringUtils.equals(sendUserName, webSocketEndpointEntry.getKey())) {
                            return;
                        }
                        try {
                            webSocketEndpointEntry.getValue().getSession().getBasicRemote()
                                    .sendText(generatorMessage(message));
                        } catch (IOException ioException) {
                            logger.error("send error:" + ioException);
                        }
                    }
            );
        }
    }
}
  • SendExecutorFactory

发送渠道工厂

package com.example.im.infra.executor.send;

import com.example.im.config.WebSocketProperties;
import com.example.im.infra.executor.config.ExecutorConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * @author PC
 * 发送逻辑工厂
 */
@Component
public class SendExecutorFactory {

    private final WebSocketProperties webSocketProperties;

    private ExecutorConfiguration executorConfiguration;

    @Autowired
    public SendExecutorFactory(WebSocketProperties webSocketProperties) {
        this.webSocketProperties = webSocketProperties;
    }

    @Autowired
    public void setExecutorConfiguration(ExecutorConfiguration executorConfiguration) {
        this.executorConfiguration = executorConfiguration;
    }

    public void onMessage(String sendUserName, String message) {
        IBaseSendExecutor iBaseSendExecutor = Optional.ofNullable(executorConfiguration.getBaseSendExecutorMap()
                .get(webSocketProperties.getCommunicationType())).orElse(new DefaultSendExecutor());
        //包含@发给指定人,否则发给全部人
        if (StringUtils.contains(message, webSocketProperties.getReceiverSeparator())) {
            iBaseSendExecutor.sendToUser(sendUserName, message);
        } else {
            iBaseSendExecutor.sendToAll(sendUserName, message);
        }
    }
}
  • ExecutorConfiguration

加载

package com.example.im.infra.executor.config;

import com.example.im.infra.executor.send.IBaseSendExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @author PC
 * Executor配置
 */
@Component
public class ExecutorConfiguration implements ApplicationContextAware {
    private final static Logger logger = LoggerFactory.getLogger(ExecutorConfiguration.class);

    private Map<String, IBaseSendExecutor> baseSendExecutorMap = new HashMap<>(16);

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ExecutorConfiguration.applicationContext = applicationContext;
        //加载IBaseSendExecutor实现类
        this.initBaseSendExecutor(applicationContext);
    }

    /**
     * 加载IBaseSendExecutor实现类
     * 如果一个服务的发送渠道是固定的,可以使用@Bean搭配@ConditionalOnProperty的方式
     * 但是考虑到后续可能会有一个服务不同发送渠道的场景,采用当前加载方式
     *
     * @param applicationContext 上下文
     */
    private void initBaseSendExecutor(ApplicationContext applicationContext) {
        logger.info("Start loading IBaseSendExecutor");
        Map<String, IBaseSendExecutor> baseSendExecutorMap = applicationContext.getBeansOfType(IBaseSendExecutor.class);
        for (Map.Entry<String, IBaseSendExecutor> iBaseSendExecutorEntry : baseSendExecutorMap.entrySet()) {
            String communicationType = iBaseSendExecutorEntry.getValue().getCommunicationType();
            this.baseSendExecutorMap.put(communicationType, iBaseSendExecutorEntry.getValue());
            logger.info("initBaseSendExecutor>>>>>>>communicationType:{},className:{}", communicationType, iBaseSendExecutorEntry.getValue().getClass().getName());
        }
        logger.info("IBaseSendExecutor loading is complete");
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public Map<String, IBaseSendExecutor> getBaseSendExecutorMap() {
        return baseSendExecutorMap;
    }

    public void setBaseSendExecutorMap(Map<String, IBaseSendExecutor> baseSendExecutorMap) {
        this.baseSendExecutorMap = baseSendExecutorMap;
    }
}

添加redis通信渠道

  • pom.xml
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • application.yml
server:
  port: 18080
cus:
  ws:
    exclude-receiver-info-flag: true
    receiver-excludes-himself-flag: true
    communication-type: redis
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    username: root
    password: root
    database: ${SPRING_REDIS_DATABASE:1}
    # Redis连接超时时间
    connect-timeout: ${SPRING_REDIS_CONNECT_TIMEOUT:2000}
    # Redis读取超时时间
    timeout: ${SPRING_REDIS_READ_TIMEOUT:5000}
    lettuce:
      pool:
        # 资源池中最大连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整
        max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
        # 资源池运行最大空闲的连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销
        max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
        # 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
        # 默认 -1 表示永不超时,设置5秒
        max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
  • RedisSendExecutor

redis发送

package com.example.im.infra.executor.send.redis;

import com.example.im.infra.constant.ImConstants;
import com.example.im.infra.executor.send.AbstractBaseSendExecutor;
import com.example.im.infra.executor.send.dto.MessageInfo;
import com.example.im.infra.executor.send.dto.ScopeOfSendingEnum;
import com.example.im.infra.util.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * @author PC
 * redis执行
 */
@Component
public class RedisSendExecutor extends AbstractBaseSendExecutor {

    private final static Logger logger = LoggerFactory.getLogger(RedisSendExecutor.class);

    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public String getCommunicationType() {
        return ImConstants.CommunicationType.REDIS;
    }

    @Override
    public void sendToUser(String sendUserName, String message) {
        MessageInfo messageInfo = new MessageInfo();
        messageInfo.setSendUserName(sendUserName);
        messageInfo.setMessage(message);
        messageInfo.setScopeOfSending(ScopeOfSendingEnum.USER);
        logger.debug("send to user redis websocket, channel is " + "redis-websocket");
        redisTemplate.convertAndSend("redis-websocket-user", JsonUtils.toJson(messageInfo));
    }

    @Override
    public void sendToAll(String sendUserName, String message) {
        MessageInfo messageInfo = new MessageInfo();
        messageInfo.setSendUserName(sendUserName);
        messageInfo.setMessage(message);
        messageInfo.setScopeOfSending(ScopeOfSendingEnum.ALL);
        logger.debug("send to all redis websocket, channel is " + "redis-websocket");
        redisTemplate.convertAndSend("redis-websocket-all", JsonUtils.toJson(messageInfo));
    }
}
  • RedisMessageListener

redis监听

package com.example.im.infra.executor.send.redis;

import com.example.im.infra.executor.send.DefaultSendExecutor;
import com.example.im.infra.executor.send.dto.MessageInfo;
import com.example.im.infra.util.JsonUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * @author PC
 * redis监听
 */
@Component
public class RedisMessageListener implements MessageListener {

    private final static Logger logger = LoggerFactory.getLogger(RedisMessageListener.class);

    private DefaultSendExecutor defaultSendExecutor;

    @Autowired
    public void setDefaultSendExecutor(DefaultSendExecutor defaultSendExecutor) {
        this.defaultSendExecutor = defaultSendExecutor;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        //消息内容
        String messageJson = new String(message.getBody(), StandardCharsets.UTF_8);
        MessageInfo messageInfo = JsonUtils.toObjectByTypeReference(messageJson, new TypeReference<MessageInfo>() {
        });
        switch (messageInfo.getScopeOfSending()) {
            case USER:
                defaultSendExecutor.sendToUser(messageInfo.getSendUserName(), messageInfo.getMessage());
                break;
            case ALL:
                defaultSendExecutor.sendToAll(messageInfo.getSendUserName(), messageInfo.getMessage());
                break;
            default:
                //一般来说不会出现该情况,除非用户覆盖了ScopeOfSending,后续可以开个扩展发送范围的口子
                logger.warn("invalid sending range:" + messageInfo.getScopeOfSending().getScopeCode());
                break;
        }
    }
}

测试

本地服务发送消息

服务器接收到了消息

常见问题

打包报错

执行mvn clean packages打包时出现以下错误

[ERROR] contextLoads  Time elapsed: 0.001 s  <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpoint' defined in class path resource [c
om/example/im/config/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available

查看ServerContainer接口,发现其有两个接口实现类,其中有一个是test包的

将其排除后即可正常打包

jar包启动时no main manifest attribute问题

需将pom的plugin标签中的skip标签删除或设置为false

参考资料

[1].初版im文档

[2].im项目地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2220868.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C Primer Plus 第9章——第一篇

你该逆袭了 文章目录 一、复习函数1、定义带形式参数的函数2、声明带形式参数函数的原型3、使用 return 从函数中返回值&#xff08;1&#xff09;、返回值不仅可以赋给变量&#xff0c;也可以被用作表达式的一部分。&#xff08;2&#xff09;、返回值不一定是变量的值&#x…

springboot redisTemplate hash 序列化探讨

前提提要&#xff1a;这个是个人小白总结&#xff0c;写完博客后开始厌蠢。 redisTemplate 有两种插入hash的方式 redisTemplate.opsForHash().putAll(key, map);redisTemplate.opsForHash().put(key, field, value);在使用的过程中&#xff0c;难免会疑问为什么 key field v…

Windows下部署autMan

一、安装autMan 下载autMan压缩包 https://github.com/hdbjlizhe/fanli/releases 解压安装包 二、运行&#xff08;注意&#xff0c;无论是交互运行还是静默运行&#xff0c;终端均不可关闭&#xff09; 基本运行 双击autMan.exe运行。 高级运行 在autMan文件夹&#xff0…

Sigrity Power SI Model Extraction模式如何提取电源网络的S参数和阻抗操作指导(一)

Sigrity Power SI Model Extraction模式如何提取电源网络的S参数和阻抗操作指导(一) Sigrity PowerSI是频域电磁场仿真工具,以下图为例介绍如果用它观测电源的网络的S参数以及阻抗的频域曲线. 观测IC端电源网络的自阻抗 1. 用powerSi.exe打开该SPD文件

工业相机详解及选型

工业相机相对于传统的民用相机而言&#xff0c;具有搞图像稳定性,传输能力和高抗干扰能力等&#xff0c;目前市面上的工业相机大多数是基于CCD&#xff08;Charge Coupled Device)或CMOS(Complementary Metal Oxide Semiconductor)芯片的相机。 一&#xff0c;工业相机的分类 …

sentinel原理源码分析系列(六)-统计指标

调用链和统计节点构建完成&#xff0c;进入统计指标插槽&#xff0c;统计指标在最后执行的&#xff0c;等后面的插槽执行完&#xff0c;资源调用完成了&#xff0c;根据资源调用情况累计。指标统计是最重要的插槽&#xff0c;所有的功能都依靠指标数据&#xff0c;指标的正确与…

你知道什么叫数控加工中心吗?

加工中心是一种高度机电一体化的数控机床&#xff0c;具有刀库&#xff0c;自动换刀功能&#xff0c;对工件一次装夹后进行多工序加工的数控机床。通过计算的控制系统和稳定的机械结构&#xff0c;加工中心能够实现高精度的加工&#xff0c;确保工件的尺寸精度和表面质量。通过…

实用好助手

在现代职场中&#xff0c;拥有高效且适用的工具能够显著提升我们的工作效率与质量。除了常见的办公软件&#xff0c;还有许多小众但非常实用的工具可以大幅度优化工作流程。以下是另外五个推荐的工作软件&#xff0c;它们各自具备独特的功能与优势&#xff0c;值得一试。 1 …

【Docker系列】在 Docker 容器中打印和配置环境变量

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

双十一有哪些值得买的东西?2024年最全双十一好物推荐榜单来了!

双十一能够入手的好东西那肯定是非常多的&#xff0c;不过要想买到性价比高、实用性强的好物&#xff0c;就必须得做些功课了。作为一个智能家居和数码领域的博主&#xff0c;自然知道每年双十一买什么是最划算的。如果有朋友正在为双十一不知道买什么而发愁&#xff0c;那就快…

python+大数据+基于热门视频的数据分析研究【内含源码+文档+部署教程】

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

登录后端笔记(一):注册、登录;基于MD5加密

一、注册 一、参数&#xff1a;lombok pom.xml里引入依赖&#xff1b; 二、响应数据&#xff1a;Result 原视频 两个注解对应有参无参生成构造方法&#xff1b; data类型是泛型T&#xff0c;即data在使用时可对应object可对应string字符串可对应bean对象可对应map等&#x…

微信碰一碰支付系统有哪些好的?教程详解抢先看!

支付宝“碰一碰支付”的风刚刚刮起来&#xff0c;它的老对手微信便紧随其后&#xff0c;推出了自己的碰一碰支付设备&#xff0c;再次印证了这个项目市场前景广阔的同时&#xff0c;也让与碰一碰支付系统相关问题的热度又上了一层楼&#xff0c;尤其是微信碰一碰支付系统有哪些…

炒股VS炒游戏装备,哪个更好做

这个项目&#xff0c;赚个10%都是要被嫌弃的 虽然天天都在抒发自己对股市的看法&#xff0c;但自己自始至终也没有买进任何一支股票。之所以对这个话题感兴趣&#xff0c;着实是因为手上的游戏搬砖项目也是国际性买卖&#xff0c;跟国际形势&#xff0c;国际汇率挂钩&#xff0…

RAG总结及前沿之Meta-Chunking切分思路及VisRAG多模态实现机制解读

今天我们来看两个工作&#xff0c;一个是关于RAG的切分策略&#xff0c;Meta-Chunking&#xff0c;里面基于数学常识提到的边际采样分块&#xff08;Margin Sampling Chunking&#xff09;通过LLMs对连续句子是否需要分割进行二元分类&#xff0c;基于边际采样得到的概率差异来…

基于SSM+微信小程序的房屋租赁管理系统(房屋2)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM微信小程序的房屋租赁管理系统实现了有管理员、中介和用户。 1、管理员功能有&#xff0c;个人中心&#xff0c;用户管理&#xff0c;中介管理&#xff0c;房屋信息管理&#xff…

Nest.js 实战 (十五):前后端分离项目部署的最佳实践

☘️ 前言 本项目是一个采用现代前端框架 Vue3 与后端 Node.js 框架 Nest.js 实现的前后端分离架构的应用。Vue3 提供了高性能的前端组件化解决方案&#xff0c;而 Nest.js 则利用 TypeScript 带来的类型安全和模块化优势构建了一个健壮的服务端应用。通过这种技术栈组合&…

信雅纳Chimera 100G网络损伤仪助力Parallel Wireless开展5G RAN无线前传网络的损伤模拟

背景介绍 Parallel Wireless 为移动运营商提供唯一全覆盖的(5G/4G/3G/2G&#xff09;软件支持的本地 OpenRAN (ORAN) 解决方案。该公司与全球 50 多家领先运营商合作&#xff0c;并被 Telefonica 和 Vodafone 评为表现最佳的供应商。Parallel Wireless 在多技术、开放式虚拟化…

【多视图聚类】【ICCV 2023】基于交叉视图拓扑一致互补信息的深度多视图聚类

0.论文摘要 多视图聚类旨在从不同的来源或视角提取有价值的信息。多年来&#xff0c;深度神经网络在多视图聚类中展示了其优越的表示学习能力&#xff0c;并取得了令人印象深刻的性能。然而&#xff0c;大多数现有的深度聚类方法致力于合并和探索跨多个视图的一致潜在表示&…

Java网络编程的基础:计算机网络

在学习 Java 网络编程之前&#xff0c;我们先来了解什么是计算机网络。 计算机网络是指两台或更多的计算机组成的网络&#xff0c;在同一个网络中&#xff0c;任意两台计算机都可以直接通信&#xff0c;因为所有计算机都需要遵循同一种网络协议。 下面是一张简化的网络拓扑图…