Idea+maven+springboot项目搭建系列--2 整合Netty完成客户端服务器端消息收发

news2024/11/28 13:31:24

前言:Netty 作为主流的nio 通信模型应用相当广泛,本文在spring-boot 项目中集成Netty,并实现客户端以及服务器端消息的接收和发送;本文是 Spring架构篇–2.7 远程通信基础–使用Netty 的扩展;

1 spring-boot jar包引入:引入的jar 和解释如下:

<!-- springboot-web 用于发送http 请求 -->
 <dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 用于发送生成java 对象的get set 和构造方法 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <!-- lombok 依赖项目不被maven 传递 -->
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- netty jar :https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.51.Final</version>
</dependency>
<!-- Json 格式化 https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.31</version>
</dependency>

2 服务端和客户端:

2.1 服务端:
2.1.1 WebNettyServer 服务端:

import com.example.nettydemo.netty.handler.NettyServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Slf4j
@Component
public class WebNettyServer {
    private final static Logger logger = LoggerFactory.getLogger(WebNettyServer.class);
    private final static int PORT = 8888;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private EventLoopGroup extGroup;

    private ChannelFuture channelFuture;
    @PostConstruct
    public void start() throws InterruptedException {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();
        extGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // 添加解码器
                        pipeline.addLast("解码器",new StringDecoder());
                        // 添加编码器
                        pipeline.addLast("编码器",new StringEncoder());
                        // 添加自己的处理器
                        pipeline.addLast("ext",new NettyServerHandler());
                        // 如果业务调用比较耗时,为了不影响netty 服务端处理器请求的性能,可以使用另外的NioEventLoopGroup 进行handler 处理
                       // pipeline.addLast(extGroup,"ext",new NettyServerHandler());
                    }
                })
                // 连接请求时的等待队列
                .option(ChannelOption.SO_BACKLOG,128)
                // 设置TCP连接是否开启长连接
                .childOption(ChannelOption.SO_KEEPALIVE,true);
        // 绑定端口,开启服务
        channelFuture = bootstrap.bind(PORT).sync();
        if (channelFuture.isSuccess()) {
            logger.debug("Netty server started on port {}", PORT);
        }
    }
    @PreDestroy
    public void stop() {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        extGroup.shutdownGracefully();
    }
}

这里对code做下简要的说明:

  • 通过NioEventLoopGroup 分别定义了负责接收客户端连接的的bossGroup,定义负责客户端数据的读和服务端写回数据到客户端的workerGroup,定义额外的事件处理extGroup用于负责业务handler 比较耗时的操作;
  • NioServerSocketChannel.class 用于服务端使用NioServerSocketChannel 对象,完成对channel 管道事件的处理;
  • StringDecoder 用于对客户端数据的解码,StringEncoder 用于对服务端发送数据进行编码,NettyServerHandler 为业务类处理解码后的数据;
  • ChannelOption.SO_BACKLOG 设置 ServerSocketChannel 的可连接队列大小,用于处理连接请求时的等待队列,即 TCP 握手完成后等待被应用程序处理的连接队列,在系统内核建立连接之前,连接请求会先进入队列中等待被处理。当 ServerSocketChannel 与客户端连接成功后,会创建一个新的 SocketChannel 与该客户端进行通信,如果队列满了,新的连接请求就会被拒绝。 不同操作系统下 backlog 参数的默认值可能不同,通常建议显式设置它的值。在 Windows 操作系统下,它的默认值为 200,而在 Linux 操作系统下,它的默认值为 128;
  • ChannelOption.SO_KEEPALIVE是一个TCP参数,它用于设置TCP连接是否开启长连接。当设置为true时,TCP连接会在一定时间内没有数据传输时发送心跳包,以保持连接状态;当设置为false时,TCP连接会在一定时间内没有数据传输时直接关闭连接。
  • bootstrap.bind(PORT).sync() 绑定端口,启动监听,sync 同步阻塞直到服务端启动成功;
  • @Component 告诉spring 需要扫描改类并进行装配;
  • @PostConstruct 告诉spring 改类被springboot 装备之后需要执行定义好的 start() 方法;
  • @PreDestroy 告诉spring 在容器关闭之前需要执行定义好的stop() 方法,这里stop() 方法里对定义的事件轮询处理组进行优雅的关闭(优雅是指,事件轮询处理组在完成所有任务之后在进行关闭操作);

2.1.1 服务端 NettyServerHandler 业务处理:


import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

@Slf4j
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
    private final static Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        logger.debug("Received message: {}", msg,ctx);
        JSONObject json = JSONObject.parseObject(msg);
        String type = json.getString("type");
        Object content = json.get("content");
        JSONObject resultJson = new JSONObject();
        resultJson.put("id", json.getString("id"));
        resultJson.put("type", type);
        switch (type) {
            case "user":
                // 进入 userService 业务处理
                JSONArray jsonArray;
                Map<String, Object> map;
                if (content instanceof JSONArray) {
                    jsonArray = (JSONArray) content;
                    // 处理 jsonArray
                    // ...
                } else if (content instanceof JSONObject) {
                    map = (Map<String, Object>) content;
                    // 处理 map
                    // ...
                }
                resultJson.put("success", true);
                break;
            case "order":
                // 进入 orderService 业务处理
                resultJson.put("success", true);
                break;
            default:
                resultJson.put("success", false);
                resultJson.put("errorMsg", "unsupported type");
        }
        // 回写结果到客户端
        log.debug("回写结果:{}到客户端",resultJson.toJSONString());
        ctx.channel().writeAndFlush(resultJson.toJSONString());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

code 比较简单,在服务端的处理器中,首先将接收到的字符串解析成一个 JSONObject 对象。然后根据 type 字段进行业务分发,可以在不同的分支中获取 content 字段并根据内容进行业务处理。处理完成后,将结果封装成一个新的 JSONObject 对象,并回写到客户端。需要注意的是,Netty 自带的 StringDecoder 和 StringEncoder 可以直接处理字符串,因此不需要手动进行 Json 转换。同时,回写结果时需要使用 ctx.channel().writeAndFlush() 方法,而不是之前的 channelFuture.channel().writeAndFlush()。

2.2 客户端:
2.2.1 客户端WebNettyClient:



import com.example.nettydemo.netty.handler.NettyClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Slf4j
@Component
@DependsOn("webNettyServer")
public class WebNettyClient {
    private final static Logger logger = LoggerFactory.getLogger(WebNettyClient.class);
    private final String host = "127.0.0.1";  // 服务端地址
    private final int port = 8888;  // 服务端端口
    private ChannelFuture channelFuture;  // 连接通道
    private EventLoopGroup eventLoopGroup;
    @PostConstruct
    public void start() throws InterruptedException {
        eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // 添加解码器
                        pipeline.addLast(new StringDecoder());
                        // 添加编码器
                        pipeline.addLast(new StringEncoder());
                        // 添加自己的处理器
                        pipeline.addLast(new NettyClientHandler());
                    }
                });
        // 连接服务端
        channelFuture = bootstrap.connect(host, port).sync();
        if (channelFuture.isSuccess()) {
            logger.debug("连接服务端成功");
        }
    }
    public void sendMessage(String message) {
        channelFuture.channel().writeAndFlush(message);
    }
    @PreDestroy
    public void stop() {
        eventLoopGroup.shutdownGracefully();
    }
}

2.2.1 客户端业务处理:

package com.example.nettydemo.netty.handler;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // 接收到服务端消息时的处理
        log.debug("Received from server:{} ", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端的code 基本和服务端一致,客户端接收了服务端的回写数据并进行了打印,其中@DependsOn(“webNettyServer”) 告诉spring 在装载客户端的bean 时,先要去装载服务端的bean;

2.3 web controller 客户端发送数据测试:
controller:



import com.alibaba.fastjson2.JSONObject;
import com.example.nettydemo.modules.user.dto.UserDto;
import com.example.nettydemo.netty.client.WebNettyClient;
import com.example.nettydemo.netty.data.NettyData;
import com.example.nettydemo.netty.server.WebNettyServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("user/")
public class UserController {
    @Autowired
    private WebNettyClient webNettyClient;

    /**
     * 客户端信息发送
     * @return
     */
    @GetMapping("client/msg")
    public boolean testClientUser(){
        // 处理业务
        NettyData data = dealAndGetClientData();
        webNettyClient.sendMessage(JSONObject.toJSONString(data));

        return true;
    }

    private NettyData dealAndGetClientData() {
        NettyData data = new NettyData();
        data.setId("1").setType("user-client").setContent(new UserDto().setUserId("001").setUserName(" haha"));
        return data;
    }

}

UserDto:



import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@Accessors(chain = true)
public class UserDto implements Serializable {
    private String userId;
    private String userName;
}

NettyData:


import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@Accessors(chain = true)
public class NettyData implements Serializable {
    private String id;
    private String type;
    private Object content;
}

可以看到服务端和客户端的结果接收情况:
在这里插入图片描述

3 总结:

  • Netty 的服务端,通过定义bossGroup对客户端的accept 事件进行处理,然后通过定义的workerGroup 进行具体channel 内读写事件发生的处理;
  • Netty在read数据时,会依次从头到尾调用 ChannelInboundHandlerAdapter 的hadler 进行数据的处理;
  • Netty在write 数据时,如果使用SocketChannel ch进行数据写入,会依次从尾到头调用ChannelOutboundHandlerAdapter 的handler 进行数据处理,如果通过ChannelHandlerContext ctx会从当前处理器,向前找ChannelOutboundHandlerAdapter 的handler 进行数据处理;
  • Netty 对于事件的处理是通过SocketChannel channel 中的pipeline 中每个handler 进行的处理,而pipeline 中维护的是handler 的双向链表;

4 git 项目代码:
https://codeup.aliyun.com/61cd21816112fe9819da8d9c/netty-demo.git

5 扩展:
5.1 使用EmbeddedChannel 来测试chanel 中handler 的处理走向:


import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

/**
 * 测试出入handler
 */
@Slf4j
public class TestEmbeddedChannel {
    public static void main(String[] args) {
        ChannelInboundHandlerAdapter h1 =new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("h1");

                super.channelRead(ctx, msg);
            }
        };
        ChannelInboundHandlerAdapter h2 =new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("h2");
                super.channelRead(ctx, msg);
            }
        };
        ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("h3");
                super.write(ctx, msg, promise);
            }
        };
        ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("h4");
                super.write(ctx, msg, promise);
            }
        };
        EmbeddedChannel channel = new EmbeddedChannel(h1,h2,h3,h4);
        //  测试read 
        channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello read".getBytes(StandardCharsets.UTF_8)));
        //  测试write
        channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello write".getBytes(StandardCharsets.UTF_8)));


    }
}

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

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

相关文章

C++进阶 —— (C++11新特性)

三&#xff0c;范围for循环 四&#xff0c;final与override 五&#xff0c;智能指针 六&#xff0c;静态数组array、forward_list、unordered系列(新增容器) 七&#xff0c;默认成员函数的控制 在C中&#xff0c;对于空类编译器会生成一些默认成员函数(如构造函数、拷贝构造函数…

嵌入式Linux驱动开发 03:平台(platform)总线驱动模型

文章目录 目的基础说明开发准备在驱动中获取资源单驱动使用多个资源总结 目的 前面文章 《嵌入式Linux驱动开发 01&#xff1a;基础开发与使用》 和 《嵌入式Linux驱动开发 02&#xff1a;将驱动程序添加到内核中》 介绍了驱动开发最基础的内容&#xff0c;这篇文章将在前面基…

Vue3 小兔鲜4:Layout-静态模版结构搭建

Vue3 小兔鲜4&#xff1a;Layout-静态模版结构搭建 Date: May 31, 2023 目标效果&#xff1a; 分成Nav、Heade、二级路由出口、Footer区域 组件结构快速搭建 Nav <script setup></script><template><nav class"app-topnav"><div clas…

如何用VS2019创建并调用动态库

如何用VS2019创建并调用动态库 创建动态库调用动态库 创建动态库 网上查了相关资料&#xff0c;创建动态库主要有两种方式&#xff0c;一种是通过空项目创建动态库&#xff0c;一种是直接创建动态链接库&#xff0c;本文所总结的就是第二种&#xff0c;直接创建动态链接库。 …

B树(C语言描述)

一.概念 B树是一种多路平衡查找树&#xff0c;不同于二叉平衡树&#xff0c;他不只是有两个分支&#xff0c;而是有多个分支&#xff0c;一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树&#xff0c;B树用于磁盘寻址&#xff0c;它是一种高效的查找算法。 二.性质…

【Kubernetes 入门实战课】Day03——容器的本质

系列文章目录 【Kubernetes 入门实战课】Day01——虚拟机创建及安装 【Kubernetes 入门实战课】Day02——初识容器 文章目录 系列文章目录前言一、容器到底是什么&#xff1f;二、为什么要隔离三、与虚拟机的区别是什么四、隔离是怎么实现的 前言 上一节中我们完成了在Linux虚…

Anaconda下载安装及使用方法汇总

软件说明: Anaconda是Red Hat Linux和Fedora的安装管理程式。它以Python及C语言写成&#xff0c;以图形的PyGTK和文字的python-newt介面写成。它可以用来自动安装配置&#xff0c;使用户能够以最小的监督运行。Anaconda安装管理程式应用在RHEL&#xff0c;Fedora和其他一些项目…

IMX6ULL裸机篇之I2C实验-

一. I2C 实验简介 I2C实验&#xff0c;我们就来学习如何使用 I.MX6U 的 I2C 接口来驱动 AP3216C&#xff0c;读取 AP3216C 的传感器数据。 AP3216C是一个三合一的环境光传感器&#xff0c;ALSPSIRLED&#xff0c;ALS是环境光&#xff0c;PS是接近传感器&#xff0c;IR是红外L…

MANTOO车联网RSU终端助您畅享智慧出行!

一、案例背景 随着社会经济的飞速发展&#xff0c;汽车逐渐走进了千家万户&#xff0c;目前我国家庭乘用汽车保有量在2.6亿辆&#xff0c;平均每6个人就拥有一辆汽车。汽车保有量的上涨同时也给道路交通安全带来了极大的挑战&#xff0c;为了降低交通事故发生&#xff0c;保障…

牛客网项目—开发社区首页

视频连接&#xff1a;开发社区首页_哔哩哔哩_bilibili 代码地址&#xff1a;Community: msf begin 仿牛客论坛项目 (gitee.com) 本文是对仿牛客论坛项目的学习&#xff0c;学习本文之前需要了解Java开发的常用框架&#xff0c;例如SpringBoot、Mybatis等等。如果你也在学习牛…

遗传算法讲解

遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09; 是模拟生物在自然环境中的遗传和进化的过程而形成的自适应全局优化搜索算法。它借用了生物遗传学的观点&#xff0c;通过自然选择、遗传和变异等作用机制&#xff0c;实现各个个体适应性的提高。 基因型 (G…

文件阅览功能的实现(适用于word、pdf、Excel、ppt、png...)

需求描述&#xff1a; 需要一个组件&#xff0c;同时能预览多种类型文件&#xff0c;一种类型文件可有多个的文件。 看过各种博主的方案&#xff0c;其中最简单的是利用第三方地址进行预览解析&#xff08;无需任何插件&#xff09;&#xff1b; 这里推荐三个地址&#xff1a…

EasyExcel实现excel区域三级联动(模版下载)

序号 前言需求不通过excel,实现省市区级联实战pom.xml配置controller配置service类业务处理类测试 前言 首先&#xff0c;我们先来了解一下java实现模板下载的几种方式 1、使用poi实现2、使用阿里的easyexcel实现 今天社长就给大家说一下easyexcel的实现模板下载的之旅。在这里…

phpword使用整理

目录 介绍 安装 创建文档 设置默认字体和字号 设置文本样式 编号标题 换行符 分页符 超链接 创建表格 添加图片 文件保护 加载word文件 内容转化为html 保存 模板替换 格式 加载模板 替换字符串 替换图片 替换表格 总结 参考 介绍 PHPWord 是一个用纯 …

Vue3 过渡动画效果

文章目录 Vue3 过渡动画效果概述<Transition>组件简单使用为过渡效果命名自定义过渡classJavaScript动画效果元素间过渡 <transition-group>组件列表动画状态动画 Vue3 过渡动画效果 概述 Vue 提供了两个内置组件&#xff0c;可以帮助你制作基于状态变化的过渡和…

中服云设备全生命周期管理系统4.0全新升级,震撼登场!

6月2日&#xff0c;中服云设备全生命周期管理系统4.0将在中服云官方视频号线上直播震撼发布。在此次线上直播发布会上&#xff0c;中服云将详细地介绍设备全生命周期管理系统4.0版本的全新特性和创新功能。同时邀请了业内权威售前顾问、设备管理工程师和合作伙伴&#xff0c;共…

降低FTP服务器速度的解决方案(Filezilla等)

我最近发现&#xff0c;尽管有70Mbps&#xff08;8.75MB / s&#xff09;的互联网连接和1Gbps&#xff08;125MB / s&#xff09;的专用服务器可以从中下载&#xff0c;但我似乎只能从FTP服务器上以大约16.8Mbps&#xff08;2.1MB / s&#xff09;的速度下载。在一个线程上。但…

深入篇【Linux】学习必备:理解文件权限

深入篇【Linux】学习必备&#xff1a;理解文件权限 Ⅰ.Linux权限的概念Ⅱ.Linux权限管理①.文件访问者的分类(访问者的身份)②.文件类型和访问权限(文件本身的事物属性)1.文件类型&#xff1a;2.基本权限: ③.文件权限值的表示方法1.字符表示方法2.八进制数值表示法 ④.文件访问…

【活动回顾】Databend 数据库表达式框架设计与实现 @GOTC

5月28日&#xff0c;“全球开源技术峰会 GOTC 2023 ”圆满落幕。在本次会上&#xff0c;Databend 数据库的 优化器 研发工程师 骆迪安作为嘉宾中的一员&#xff0c;在 rust 专题专区分会场进行了一次主题为《 Rust 实现的先进 SQL Parser 与高效表达式执行框架 — Databend 数…

多语言跨境商城源码,出海跨境商城软件开发模式平台

一、多语言跨境商城模式 多商家模式&#xff1a;容纳不同的商家 多用户模式&#xff1a;用户之社区&#xff0c;用户交互&#xff0c;分享和推广 支持扩展&#xff1a;使用现代化的技术架构和设计&#xff0c;包括支持并发访问、分布式数据存储等功能。 采用常用技术&#x…