RocketMQ事务消息在订单创建和库存扣减的使用 - Scotyzh - 博客园

mikel阅读(237)

来源: RocketMQ事务消息在订单创建和库存扣减的使用 – Scotyzh – 博客园

前言

下单的过程包括订单创建,还有库存的扣减,为提高系统的性能,将库存放在redis扣减,则会涉及到MySQL和redis之间的数据同步,其中,这个过程还涉及到,必须是订单创建成功才进行库存的扣减操作。其次,还涉及到库存的同步,需要保证订单创建成功和redis里的库存都扣减成功,再将库存数据同步到Mysql,为了实现上述这里情况,可以借助RocketMQ的事务型消息来实现。

流程图

流程图如下,这里引入了stocklog,即订单流水表,通过判断stocklog的状态来决定是否commite消息去同步mysql,这里stocklog状态为成功的前提是订单入库和redis库存扣减成功。

image-20240116094823532

RocketMQ事务消息

在第五步执行成功返回可能因为网络状况卡住,但是stocklog状态已经得到修改

如果返回成功 MQ事务就会commit这条消息

如果没有返回成功 MQ事务会去轮询stocklog有没有被修改

一直五次轮询发现没有被修改就会回滚这条消息,这个消息相当于被删掉,不会让消费系统消费到

这条消息commit后,就会被MQ的消费者消费,对MySQL的实际库存进行更新

stock_log的意义

这里是为了保证订单的插入和redis库存扣减都成功,才进行后续异步操作MySQL,本身的存在就是为了辅助这个本地事务的成功执行再进行后续的操作,保证一致性。

这里再说一下我之前面试遇到的一个问题:既然先对redis扣减库存再MQ异步是去操作MySQL数据库扣减库存,这样子是为了提高性能,那么这套流程一开始就操作MySQL,性能会有提升吗?答案肯定有的,这里操作数据库是将订单流水入库,并没有涉及到锁,并发下不会因为行锁而影响性能。而针对某个产品的库存扣减,直接操作MySQL进行Update操作,会对这一行加上行锁,其他请求都需要阻塞等待行锁的释放。

需要的SQL表

这里简化一下下单的流程,不涉及用户表,只涉及到库存表,库存流水表,订单表。

order表

CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_num` int(11) DEFAULT NULL COMMENT '产品数量',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stock表

CREATE TABLE `stock` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_name` varchar(255) DEFAULT NULL COMMENT '产品名字',
  `stock_num` int(11) DEFAULT NULL COMMENT '产品库存',
  PRIMARY KEY (`id`),
  UNIQUE KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品Id唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

stock_log表

CREATE TABLE `stock_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `amount` int(11) DEFAULT NULL COMMENT '库存变化数量',
  `status` int(11) DEFAULT NULL COMMENT '状态0->初始化,1->成功,2->回滚',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键代码

OrderController类

@Controller
@RequestMapping("/order")
@RequiredArgsConstructor
@Slf4j
public class OrderController {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final DecreaseStockProducer decreaseStockProducer;

    private final StockService stockService;

    private final RedisTemplate redisTemplate;

    @PostMapping(value = "/create/{id}")
    public ResponseEntity<Object> create(@PathVariable("id") Integer productId) {
        // 检查redis是否有库存0的标识
        if (redisTemplate.hasKey("product_stock_invalid_" + productId)) {
            return new ResponseEntity<>("库存不足", HttpStatus.OK);
        }

        // 先创建库存流水 这里默认一次只能扣减数量1的库存
        StockLog stockLog = StockLog.builder()
                .amount(1)
                .productId(productId)
                .status(0)
                .build();
        stockLogService.save(stockLog);

        // 发送事务消息
        try {
            DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
                    .productId(productId)
                    .stockLogId(stockLog.getId())
                    .build();
            SendResult sendResult = decreaseStockProducer.sendMessageInTransaction(decreaseStockEvent);
            if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
                log.error("事务消息发送错误,请求参数productId:{}", productId);
            }
        } catch (Exception e) {
            log.error("消息发送错误,请求参数:{}", productId, e);
        }

        return new ResponseEntity<>("created successfully", HttpStatus.OK);
    }

StockStatusCheckerListener类,执行本地事务和检查事务

@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor
public class StockStatusCheckerListener implements RocketMQLocalTransactionListener {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final TransactionTemplate transactionTemplate;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
        log.info("message: {}, args: {}", message, arg);
        TypeReference<MessageWrapper<DecreaseStockEvent>> typeReference = new TypeReference<MessageWrapper<DecreaseStockEvent>>() {};
        MessageWrapper<DecreaseStockEvent> messageWrapper = JSON.parseObject(new String((byte[]) message.getPayload()), typeReference);
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        log.info("decreaseStockEvent info : {}", decreaseStockEvent);
        try {
            orderService.createOrder(decreaseStockEvent.getProductId(), decreaseStockEvent.getStockLogId());
        } catch (Exception e) {
            log.error("插入订单失败, decreaseStockEvent info : {}", decreaseStockEvent, e);
            // 触发回查
            //设置对应的stockLog为回滚状态
            StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
            stockLog.setStatus(2);
            stockLogService.updateById(stockLog);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        log.info("message: {}, args: {}", message);
        MessageWrapper<DecreaseStockEvent> messageWrapper = (MessageWrapper) message.getPayload();
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
        if (stockLog == null) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        // 已经被扣减了库存
        if (stockLog.getStatus().intValue() == 1) {
            return RocketMQLocalTransactionState.COMMIT;
            // 初始化状态
        } else if (stockLog.getStatus().intValue() == 0) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }

}

MQ相关代码,使用模板方法

DecreaseStockProducer,消息生产者,实现了一些指定方法

@Slf4j
@Component
public class DecreaseStockProducer extends AbstractCommonSendProduceTemplate<DecreaseStockEvent> {

    private final ConfigurableEnvironment environment;

    public DecreaseStockProducer(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {
        super(rocketMQTemplate);
        this.environment = environment;
    }

    @Override
    protected BaseSendExtendDTO buildBaseSendExtendParam(DecreaseStockEvent messageSendEvent) {
        return BaseSendExtendDTO.builder()
                .eventName("库存同步到mysql")
                .keys(String.valueOf(messageSendEvent.getProductId()))
                .topic(environment.resolvePlaceholders(StockMQConstant.STOCK_TOPIC_KEY))
                .tag(environment.resolvePlaceholders(StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY))
                .sentTimeout(2000L)
                .build();
    }

    @Override
    protected Message<?> buildMessage(DecreaseStockEvent messageSendEvent, BaseSendExtendDTO requestParam) {
        String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();
        return MessageBuilder
                .withPayload(new MessageWrapper(requestParam.getKeys(), messageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag())
                .build();
    }
}

AbstractCommonSendProduceTemplate,发送消息的类

@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCommonSendProduceTemplate<T> {

    private final RocketMQTemplate rocketMQTemplate;

    /**
     * 构建消息发送事件基础扩充属性实体
     *
     * @param messageSendEvent 消息发送事件
     * @return 扩充属性实体
     */
    protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);

    /**
     * 构建消息基本参数,请求头、Keys...
     *
     * @param messageSendEvent 消息发送事件
     * @param requestParam     扩充属性实体
     * @return 消息基本参数
     */
    protected abstract Message<?> buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);

   

    /**
     * 事务消息事件通用发送
     *
     * @param messageSendEvent 事务消息发送事件
     * @return 消息发送返回结果
     */
    public SendResult sendMessageInTransaction(T messageSendEvent) {
        BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);
        SendResult sendResult;
        try {
            StringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());
            if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {
                destinationBuilder.append(":").append(baseSendExtendDTO.getTag());
            }
            sendResult = rocketMQTemplate.sendMessageInTransaction(
                    destinationBuilder.toString(),
                    buildMessage(messageSendEvent, baseSendExtendDTO),
                    null
            );
            log.info("[{}] 消息发送结果:{},消息ID:{},消息Keys:{}", baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());
        } catch (Throwable ex) {
            log.error("[{}] 消息发送失败,消息体:{}", baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
            throw ex;
        }
        return sendResult;
    }

OrderService的createOrder方法:

@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    private final OrderMapper orderMapper;

    private final StockLogMapper stockLogMapper;

    private final RedisTemplate redisTemplate;

    private final TransactionTemplate transactionTemplate;

    private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";

    @Override
    public void createOrder(Integer productId, Integer stockLogId) {

        // 减少Redis里面的库存
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);


        // 执行Lua脚本
        Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(String.valueOf(productId)));

        if (redisResult < 1L) {
            throw new RuntimeException("库存售罄");
        }

        // 编程式事务
        transactionTemplate.executeWithoutResult(status -> {
            try {
                // 事务性操作
                Order order = Order.builder()
                        .productId(productId)
                        .productNum(1)
                        .build();
                orderMapper.insert(order);

                // 改stockLog
                StockLog stockLog = stockLogMapper.selectOne(new QueryWrapper<StockLog>().eq("id", stockLogId));
                if (stockLog == null) {
                    throw new RuntimeException("该库存流水不存在");
                }
                stockLog.setStatus(1);
                stockLogMapper.updateById(stockLog);
                // 如果操作成功,不抛出异常,事务将提交
            } catch (Exception e) {
                // 如果操作失败,抛出异常,事务将回滚 并且需要补偿redis的库存
                redisTemplate.opsForValue().increment(String.valueOf(productId));
                status.setRollbackOnly();
            }
        });

    }
}

redis的lua脚本代码如下,这里只会在库存大于0的时候进行扣减,先检查库存,再扣减。如果库存为0,在redis里面setIfAbsent该商品售罄的标识,这样子在controller查询到售罄就直接return

local key = KEYS[1]

-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
    -- 键存在,获取值
    local value = redis.call('GET', key)
    if tonumber(value) > 0 then
        -- 如果值大于0,则递减
        redis.call('DECR', key)
        return 1  -- 表示递减成功
    else
        local prefix = "product_stock_invalid_"
        local stock_invalid_tag = prefix .. KEYS[1]
        local exists_tag = redis.call('EXISTS', stock_invalid_tag)
        if exists_tag == 0 then
            -- 键不存在,设置键的值
            redis.call('SET', stock_invalid_tag, "true")
        return 0  -- 表示递减失败,值不大于0
        end
    end
else
    return -1  -- 表示递减失败,键不存在
end

MQ的consumer:

@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
        topic = StockMQConstant.STOCK_TOPIC_KEY,
        selectorExpression = StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY,
        consumerGroup = StockMQConstant.STOCK_DEREASE_STOCK_CG_KEY
)
public class DecreaseStockConsumer implements RocketMQListener<MessageWrapper<DecreaseStockEvent>> {

    private final StockService stockService;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void onMessage(MessageWrapper<DecreaseStockEvent> message) {
        DecreaseStockEvent decreaseStockEvent = message.getMessage();
        Integer productId = decreaseStockEvent.getProductId();
        try {
            stockService.decreaseStock(productId);
        } catch (Exception e) {
            log.error("库存同步到mysql失败,productId:{}", productId, e);
            throw e;
        }
    }
}

stockService.decreaseStock()方法如下

    public int decreaseStock(Integer productId) {
        return stockMapper.decreaseStock(productId);
    }

相关的SQL语句

    <update id="decreaseStock">
        UPDATE stock
        SET stock_num = stock_num - 1
        WHERE id = #{id} AND stock_num >= 1
    </update>

消息重复消费问题

我们知道,MQ可能会存在重复消费的问题,包括我在压测的时候,就存在了重复消费,导致MySQL的库存最终比redis库存要少,重复扣减了MySQL的库存,针对这种情况,应该解决幂等性问题。

在前面我们用MessageWrapper来包装消息体的时候,每次new一个MessageWrapper都会生成新的UUID,我们将这UUID存到Redis里面来保证幂等性

/**
 * 消息体包装器
 */
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 消息发送 Keys
     */
    @NonNull
    private String keys;

    /**
     * 消息体
     */
    @NonNull
    private T message;

    /**
     * 唯一标识,用于客户端幂等验证
     */
    private String uuid = UUID.randomUUID().toString();

    /**
     * 消息发送时间
     */
    private Long timestamp = System.currentTimeMillis();
}

修改后的扣减库存方法,先判断redis里面有没有存在已经扣除了库存的标识,有就直接返回

@Service
@RequiredArgsConstructor
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {

    private final StockMapper stockMapper;

    private final RedisTemplate redisTemplate;

    @Override
    public int decreaseStock(Integer productId, String UUID) {
        if(redisTemplate.hasKey("decrease_mark_" + UUID)) {
            return 0;
        }
        redisTemplate.opsForValue().set("decrease_mark_" + UUID, "true", 24, TimeUnit.HOURS);
        return stockMapper.decreaseStock(productId);
    }
}

下面是上述demo的代码地址,修改数据库和mysql地址即可使用

scottyzh/stock-demo: RocketMQ事务消息在订单生成和扣减库存的应用 (github.com)

Asp .Net Core 系列:集成 Ocelot+Nacos+Swagger+Cors实现网关、服务注册、服务发现 - Code技术分享 - 博客园

mikel阅读(320)

来源: Asp .Net Core 系列:集成 Ocelot+Nacos+Swagger+Cors实现网关、服务注册、服务发现 – Code技术分享 – 博客园

 

简介

什么是 Ocelot ?

Ocelot是一个开源的ASP.NET Core微服务网关,它提供了API网关所需的所有功能,如路由、认证、限流、监控等。

Ocelot是一个简单、灵活且功能强大的API网关,它可以与现有的服务集成,并帮助您保护、监控和扩展您的微服务。

以下是Ocelot的一些主要功能:

  1. 路由管理:Ocelot允许您定义路由规则,将请求路由到正确的微服务。
  2. 认证和授权:Ocelot支持多种认证机制,如JWT、OAuth等,并允许您定义访问控制策略,确保只有授权的用户才能访问特定的API。
  3. 限流和速率限制:Ocelot提供了一些内置的限流和速率限制功能,以确保您的服务不会受到过度的请求压力。
  4. 监控和日志:Ocelot可以收集和显示各种度量指标,帮助您了解您的服务的性能和行为。此外,它还可以将日志记录到各种日志源,以便您进行分析和故障排除。
  5. 集成:Ocelot可以与现有的服务集成,包括Kubernetes、Consul等。
  6. 易于扩展:Ocelot的设计使其易于扩展,您可以编写自己的中间件来处理特定的逻辑,例如修改请求或响应、添加自定义的认证机制等。
  7. 可扩展的配置:Ocelot使用JSON配置文件进行配置,这意味着您可以轻松地根据需要进行配置更改,而无需重新编译代码。

总之,Ocelot是一个功能强大且易于使用的API网关,可以帮助您保护、监控和扩展您的微服务。

官网:https://ocelot.readthedocs.io/en/latest/index.html

什么是 Nacos ?

Nacos是一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是Dynamic Naming and Configuration Service的首字母简称。Nacos提供了一组简单易用的特性集,包括动态服务发现、服务配置、服务元数据及流量管理等功能,帮助用户快速实现微服务的发现、配置和管理。Nacos还支持多种服务注册方式和服务发现方式,如DNS、RPC、原生SDK和OpenAPI等。

此外,Nacos致力于提供更敏捷和容易的微服务平台构建、交付和管理。它是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式)的服务基础设施,能够支持动态DNS服务权重路由和动态DNS服务等特性。

官网:https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

什么是 Swagger ?

Swagger是一种规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。它是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许API始终保持同步。

此外,Swagger还提供了一个文档工具,可自动生成Web服务的API文档,使开发人员能够更轻松地理解和使用API。它还提供了一个测试工具,可以模拟对Web服务的API请求并验证响应。

什么是 Cors ?

CORS(跨来源资源共享,Cross-Origin Resource Sharing)是一种机制,允许Web应用程序在未经服务器明确许可的情况下,通过浏览器向服务器发送跨域请求。CORS是一种W3C规范,旨在解决Web应用程序中的跨域问题,以促进Web应用程序的安全性和可扩展性。

在Web应用程序中,浏览器会遵循同源策略(Same-Origin Policy),即默认只允许来自同一域的页面之间进行通信。然而,随着Web应用程序的发展,越来越多的应用程序需要与不同域的资源进行交互,例如使用第三方API或进行跨域请求。为了解决这个问题,CORS规范允许服务器通过设置适当的HTTP标头来明确地允许跨域请求。

当浏览器向服务器发送跨域请求时,服务器可以在响应头中包含一个Access-Control-Allow-Origin标头,指定允许哪些源进行跨域请求。浏览器会检查这个标头,如果允许的源与请求的源匹配,则允许跨域请求。此外,CORS规范还定义了其他一些相关的标头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers等,以进一步控制跨域请求的行为。

通过使用CORS机制,Web应用程序可以更安全、更有效地进行跨域请求,提高了应用程序的可扩展性和用户体验。

Asp .Net Core 集成 Ocelot

要在ASP.NET Core中集成Ocelot,您可以按照以下步骤进行操作:

  1. 安装Ocelot NuGet包:
    在您的ASP.NET Core项目中,打开终端或NuGet包管理器控制台,并运行以下命令来安装Ocelot的NuGet包:
dotnet add package Ocelot
  1. 添加Ocelot配置文件:
{
  "Routes": [ //这里注意一下版本(旧版本用ReRoutes)
    {
      "DownstreamPathTemplate": "/api/{controller}", //下游路径模板
      "DownstreamScheme": "http", //下游方案
      //"DownstreamHostAndPorts": [
      //  {
      //    "Host": "localhost",
      //    "Port": "5014"
      //  }
      //], //下游主机和端口
      "UpstreamPathTemplate": "/api/product/{controller}", //上游路径模板
      "UpstreamHttpMethod": [], //上游请求方法,可以设置特定的 HTTP 方法列表或设置空列表以允许其中任何方法
      "ServiceName": "api-product-service", //请求服务名称
      "LoadBalancerOptions": {
        "Type": "LeastConnection" //负载均衡算法:目前 Ocelot 有RoundRobin 和LeastConnection算法
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5015", //进行标头查找和替换以及某些管理配置
    "ServiceDiscoveryProvider": {
      "Type": "Nacos"
    }
  },
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ], //服务地址
    "UserName": "nacos",  //用户名
    "Password": "nacos", //密码
    "ServiceName": "api-gateway", //服务名称
    //"Namespace": "",  //命名空间
    //"GroupName": "DEFAULT_GROUP" //组名,
    //"ClusterName": "DEFAULT", // 集群名称
    "ListenInterval": 1000,   //监听
    "RegisterEnabled": true, // 注册是否启动
    "InstanceEnabled": true   //实例是否启动
  },
  "Url": "http://*:5015"
}
  1. 配置Ocelot服务:
builder.Services.AddOcelot();

Configure方法中配置请求管道并添加Ocelot中间件:

app.UseOcelot().Wait();

网关集成 Nacos

要将Naocs集成Ocelot到中,您可以按照以下步骤进行操作:

  1. 下载Ocelot.Provider.Nacos 源码,导入Ocelot.Provider.Nacos 项目

    github:https://github.com/softlgl/Ocelot.Provider.Nacos

    修改Ocelot.Provider.Nacos 源码,在Ocelot 22版本中 IServiceDiscoveryProvider接口中的Get方法变成了GetAsync

    升级各Nuget包

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Ocelot.ServiceDiscovery.Providers;
    using Ocelot.Values;
    using Nacos.V2;
    using Microsoft.Extensions.Options;
    using Ocelot.Provider.Nacos.NacosClient.V2;
    using NacosConstants = Nacos.V2.Common.Constants;
    
    namespace Ocelot.Provider.Nacos
    {
        public class Nacos : IServiceDiscoveryProvider
        {
            private readonly INacosNamingService _client;
            private readonly string _serviceName;
            private readonly string _groupName;
            private readonly List<string> _clusters;
    
            public Nacos(string serviceName, INacosNamingService client, IOptions<NacosAspNetOptions> options)
            {
                _serviceName = serviceName;
                _client = client;
                _groupName = string.IsNullOrWhiteSpace(options.Value.GroupName) ? 
                   NacosConstants.DEFAULT_GROUP : options.Value.GroupName;
                _clusters = (string.IsNullOrWhiteSpace(options.Value.ClusterName) ? NacosConstants.DEFAULT_CLUSTER_NAME : options.Value.ClusterName).Split(",").ToList();
            }
    
            public async  Task<List<Service>> GetAsync()
            {
                var services = new List<Service>();
    
                var instances = await _client.GetAllInstances(_serviceName, _groupName, _clusters);
    
                if (instances != null && instances.Any())
                {
                    services.AddRange(instances.Select(i => new Service(i.InstanceId, new ServiceHostAndPort(i.Ip, i.Port), "", "", new List<string>())));
                }
    
                return await Task.FromResult(services);
            }
        }
    }
    
  2. 配置Ocelot:
    在Ocelot的配置中,您需要指定Nacos作为服务发现和配置的提供者。在Ocelot的配置文件(例如appsettings.json)中,添加以下内容:
{
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5015", //进行标头查找和替换以及某些管理配置
    "ServiceDiscoveryProvider": {
      "Type": "Nacos" //指定Nacos
    }
  },
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ], //服务地址
    "UserName": "nacos",  //用户名
    "Password": "nacos", //密码
    "ServiceName": "api-gateway", //服务名称
    //"Namespace": "",  //命名空间
    //"GroupName": "DEFAULT_GROUP" //组名,
    //"ClusterName": "DEFAULT", // 集群名称
    "ListenInterval": 1000,   //监听
    "RegisterEnabled": true, // 注册是否启动
    "InstanceEnabled": true   //实例是否启动
  }
}
  1. 启动Ocelot:
    在您的ASP.NET Core应用程序中启动Ocelot。您可以在Startup.cs文件中添加以下代码:
builder.Services.AddOcelot().AddNacosDiscovery("Nacos");

下游配置 Nacos

  1. 安装必要的NuGet包:

    在Visual Studio中打开你的项目,通过NuGet包管理器安装Nacos.AspNetCore包。可以通过NuGet包管理器控制台运行以下命令来安装:

Install-Package Nacos.AspNetCore
  1. 配置Nacos客户端:

    appsettings.json文件中添加Nacos服务的配置信息,例如服务器地址、端口、命名空间等信息。示例配置如下:

{
  "Nacos": {
    "ServerAddresses": [ "http://127.0.0.1:8848" ],
    //命名空间GUID,public默认没有
    //"Namesapce": "",
    "UserName": "nacos",
    "Password": "nacos",
    // 配置中心
    //"Listeners": [
    //  {
    //    "Group": "dev",
    //    "DataId": "api-product-service",
    //    "Optional": false
    //  }
    //],
    // 服务发现
    "Ip": "localhost", // Nacos 注册时如果没有指定IP,那么就按照本机的IPv4 Address
    "Port": "5014", //端口
    "ServiceName": "api-product-service" // 服务名称
    //"GroupName": "",
    // 权重
    //"Weight": 100
  }
}
  1. 配置依赖注入:

    如果你需要在你的应用程序中使用Nacos服务,可以在Startup.csConfigureServices方法中注册Nacos服务的依赖注入。示例如下:

builder.Services.AddNacosAspNet(builder.Configuration,"Nacos");

配置跨域(Cors)

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Cors
{
    public static class CorsServiceExtensions
    {
        private readonly static string PolicyName = "MCodeCors";

        /// <summary>
        /// 添加跨域
        /// </summary>
        /// <param name="services">服务集合</param>
        /// <returns></returns>
        public static IServiceCollection AddMCodeCors(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            //origin microsoft.aspnetcore.cors      
            return services.AddCors(options =>
            {
                options.AddPolicy(PolicyName, policy =>
                {
                    policy.SetIsOriginAllowed(_ => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
                });
            });
        }
        /// <summary>
        /// 使用跨域
        /// </summary>
        /// <param name="app">应用程序建造者</param>
        /// <returns></returns>
        public static IApplicationBuilder UseMCodeCors(this IApplicationBuilder app)
        {
            return app.UseCors(PolicyName);
        }
    }
}

网关和微服务中配置Swagger

SwaggerOptions

using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger配置
    /// </summary>
    public class SwaggerOptions
    {
        /// <summary>
        /// 服务名称
        /// </summary>
        public string ServiceName { get; set; }

        /// <summary>
        /// API信息
        /// </summary>
        public OpenApiInfo ApiInfo { get; set; }

        /// <summary>
        /// Xml注释文件
        /// </summary>
        public string[] XmlCommentFiles { get; set; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serviceName">服务名称</param>
        /// <param name="apiInfo">API信息</param>
        /// <param name="xmlCommentFiles">Xml注释文件</param>
        public SwaggerOptions(string serviceName, OpenApiInfo apiInfo, string[] xmlCommentFiles = null)
        {
            ServiceName = !string.IsNullOrWhiteSpace(serviceName) ? serviceName : throw new ArgumentException("serviceName parameter not config.");
            ApiInfo = apiInfo;
            XmlCommentFiles = xmlCommentFiles;
        }
    }
}

SwaggerEndPoint

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger终端
    /// </summary>
    public class SwaggerEndPoint
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 地址
        /// </summary>
        public string Url { get; set; }
    }
}

OcelotSwaggerOptions

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// 网关Swagger配置
    /// </summary>
    public class OcelotSwaggerOptions
    {
        public List<SwaggerEndPoint> SwaggerEndPoints { get; set; }
    }
}

SwaggerServiceExtensions

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

namespace MCode.Common.Extensions.Swagger
{
    /// <summary>
    /// Swagger 服务扩展
    /// </summary>
    public static class SwaggerServiceExtensions
    {
        /// <summary>
        /// 添加 Swagger 服务
        /// </summary>
        /// <param name="services"></param>
        /// <param name="swaggerOptions"></param>
        /// <returns></returns>
        public static IServiceCollection AddMCodeSwagger(this IServiceCollection services, SwaggerOptions swaggerOptions)
        {
            services.AddSingleton(swaggerOptions);

            SwaggerGenServiceCollectionExtensions.AddSwaggerGen(services, c =>
            {
                c.SwaggerDoc(swaggerOptions.ServiceName, swaggerOptions.ApiInfo);

                if (swaggerOptions.XmlCommentFiles != null)
                {
                    foreach (string xmlCommentFile in swaggerOptions.XmlCommentFiles)
                    {
                        string str = Path.Combine(AppContext.BaseDirectory, xmlCommentFile);
                        if (File.Exists(str)) c.IncludeXmlComments(str, true);
                    }
                }

                SwaggerGenOptionsExtensions.CustomSchemaIds(c, x => x.FullName);

                c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer",
                    BearerFormat = "JWT",
                    Description = "请输入 bearer 认证"
                });


                c.AddSecurityRequirement(new OpenApiSecurityRequirement
                                              {
                                                  {
                                                      new OpenApiSecurityScheme
                                                      {
                                                          Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "bearerAuth" }
                                                      },
                                                      new string[] {}
                                                  }
                                              });
            });

            return services;
        }

        /// <summary>
        /// 使用 Swagger UI
        /// </summary>
        /// <param name="app"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseMCodeSwagger(this IApplicationBuilder app)
        {
            string serviceName = app.ApplicationServices.GetRequiredService<SwaggerOptions>().ServiceName;

            SwaggerUIBuilderExtensions.UseSwaggerUI(SwaggerBuilderExtensions.UseSwagger(app), c =>
            {
                c.SwaggerEndpoint("/swagger/" + serviceName + "/swagger.json", serviceName);
            });
            return app;
        }


        public static IServiceCollection AddMCodeOcelotSwagger(this IServiceCollection services, OcelotSwaggerOptions ocelotSwaggerOptions)
        {
            services.AddSingleton(ocelotSwaggerOptions);
            SwaggerGenServiceCollectionExtensions.AddSwaggerGen(services);
            return services;
        }

        public static IApplicationBuilder UseMCodeOcelotSwagger(this IApplicationBuilder app)
        {
            OcelotSwaggerOptions ocelotSwaggerOptions = app.ApplicationServices.GetService<OcelotSwaggerOptions>();

            if (ocelotSwaggerOptions == null || ocelotSwaggerOptions.SwaggerEndPoints == null)
            {
                return app;
            }

            SwaggerUIBuilderExtensions.UseSwaggerUI(SwaggerBuilderExtensions.UseSwagger(app), c =>
            {
                foreach (SwaggerEndPoint swaggerEndPoint in ocelotSwaggerOptions.SwaggerEndPoints)
                {
                    c.SwaggerEndpoint(swaggerEndPoint.Url, swaggerEndPoint.Name);
                }
            });
            return app;
        }
    }
}

效果

image

image

其他文章:Asp .Net Core 系列:集成 Ocelot+Consul+Swagger+Cors实现网关、服务注册、服务发现

.NET使用QuestPDF高效地生成PDF文档 - 追逐时光者 - 博客园

mikel阅读(265)

来源: .NET使用QuestPDF高效地生成PDF文档 – 追逐时光者 – 博客园

前言

在.NET平台中操作生成PDF的类库有很多如常见的有iTextSharp、PDFsharp、Aspose.PDF等,今天我们分享一个用于生成PDF文档的现代开源.NET库:QuestPDF,本文将介绍QuestPDF并使用它快速实现发票PDF文档生成功能。

QuestPDF介绍

QuestPDF 是一个用于生成 PDF 文档的现代开源 .NET 库。QuestPDF 由简洁易用的 C# Fluent API 提供全面的布局引擎。轻松生成 PDF 报告、发票、导出等。QuestPDF它提供了一个布局引擎,在设计时考虑了完整的分页支持。与其他库不同,它不依赖于 HTML 到 PDF 的转换,这在许多情况下是不可靠的。相反,它实现了自己的布局引擎,该引擎经过优化,可以满足所有与分页相关的要求。

QuestPDF License

分为社区版、专业版、和企业版。

项目源代码

创建一个控制台应用

创建一个名为QuestPDFTest的控制台应用。

安装QuestPDF Nuget包

搜索:QuestPDF包进行安装。

 

快速实现发票PDF文档生成

创建InvoiceModel

namespace QuestPDFTest
{
    public class InvoiceModel
    {

        /// <summary>
        /// 发票号码
        /// </summary>
        public int InvoiceNumber { getset; }

        /// <summary>
        /// 发票开具日期
        /// </summary>
        public DateTime IssueDate { getset; }

        /// <summary>
        /// 发票到期日期
        /// </summary>
        public DateTime DueDate { getset; }

        /// <summary>
        /// 卖方公司名称
        /// </summary>
        public string SellerCompanyName { getset; }

        /// <summary>
        /// 买方公司名称
        /// </summary>
        public string CustomerCompanyName { getset; }

        /// <summary>
        /// 订单消费列表
        /// </summary>
        public List<OrderItem> OrderItems { getset; }

        /// <summary>
        /// 备注
        /// </summary>
        public string Comments { getset; }
    }

    public class OrderItem
    {
        /// <summary>
        /// 消费类型
        /// </summary>
        public string Name { getset; }

        /// <summary>
        /// 消费金额
        /// </summary>
        public decimal Price { getset; }

        /// <summary>
        /// 消费数量
        /// </summary>
        public int Quantity { getset; }
    }
}

CreateInvoiceDetails

namespace QuestPDFTest
{
    public class CreateInvoiceDetails
    {
        private static readonly Random _random = new Random();

        public enum InvoiceType
        {
            餐饮费,
            交通费,
            住宿费,
            日用品,
            娱乐费,
            医疗费,
            通讯费,
            教育费,
            装修费,
            旅游费
        }

        /// <summary>
        /// 获取发票详情数据
        /// </summary>
        /// <returns></returns>
        public static InvoiceModel GetInvoiceDetails()
        {
            return new InvoiceModel
            {
                InvoiceNumber = _random.Next(1_000, 10_000),
                IssueDate = DateTime.Now,
                DueDate = DateTime.Now + TimeSpan.FromDays(14),
                SellerCompanyName = "追逐时光者",
                CustomerCompanyName = "DotNetGuide技术社区",
                OrderItems = Enumerable
                .Range(120)
                .Select(_ => GenerateRandomOrderItemInfo())
                .ToList(),
                Comments = "DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。"
            };
        }

        /// <summary>
        /// 订单信息生成
        /// </summary>
        /// <returns></returns>
        private static OrderItem GenerateRandomOrderItemInfo()
        {
            var types = (InvoiceType[])Enum.GetValues(typeof(InvoiceType));
            return new OrderItem
            {
                Name = types[_random.Next(types.Length)].ToString(),
                Price = (decimal)Math.Round(_random.NextDouble() * 1002),
                Quantity = _random.Next(110)
            };
        }
    }
}

CreateInvoiceDocument

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    public class CreateInvoiceDocument : IDocument
    {
        /// <summary>
        /// 获取Logo的的Image对象
        /// </summary>
        public static Image LogoImage { get; } = Image.FromFile("dotnetguide.png");

        public InvoiceModel Model { get; }

        public CreateInvoiceDocument(InvoiceModel model)
        {
            Model = model;
        }

        public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

        public void Compose(IDocumentContainer container)
        {
            container
                .Page(page =>
                {
                    //设置页面的边距
                    page.Margin(50);

                    //字体默认大小18号字体
                    page.DefaultTextStyle(x => x.FontSize(18));

                    //页眉部分
                    page.Header().Element(BuildHeaderInfo);

                    //内容部分
                    page.Content().Element(BuildContentInfo);

                    //页脚部分
                    page.Footer().AlignCenter().Text(text =>
                    {
                        text.CurrentPageNumber();
                        text.Span(" / ");
                        text.TotalPages();
                    });
                });
        }

        #region 构建页眉部分
        void BuildHeaderInfo(IContainer container)
        {
            container.Row(row =>
            {
                row.RelativeItem().Column(column =>
                {
                    column.Item().Text($"发票编号 #{Model.InvoiceNumber}").FontFamily("fangsong").FontSize(20).SemiBold().FontColor(Colors.Blue.Medium);

                    column.Item().Text(text =>
                    {
                        text.Span("发行日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.IssueDate:d}");
                    });

                    column.Item().Text(text =>
                    {
                        text.Span("终止日期: ").FontFamily("fangsong").FontSize(13).SemiBold();
                        text.Span($"{Model.DueDate:d}");
                    });
                });

                //在当前行的常量项中插入一个图像
                row.ConstantItem(130).Image(LogoImage);
            });
        }

        #endregion

        #region 构建内容部分

        void BuildContentInfo(IContainer container)
        {
            container.PaddingVertical(40).Column(column =>
            {
                column.Spacing(20);

                column.Item().Row(row =>
                {
                    row.RelativeItem().Component(new AddressComponent("卖方公司名称", Model.SellerCompanyName));
                    row.ConstantItem(50);
                    row.RelativeItem().Component(new AddressComponent("客户公司名称", Model.CustomerCompanyName));
                });

                column.Item().Element(CreateTable);

                var totalPrice = Model.OrderItems.Sum(x => x.Price * x.Quantity);
                column.Item().PaddingRight(5).AlignRight().Text($"总计: {totalPrice}").FontFamily("fangsong").SemiBold();

                if (!string.IsNullOrWhiteSpace(Model.Comments))
                    column.Item().PaddingTop(25).Element(BuildComments);
            });
        }

        /// <summary>
        /// 创建表格
        /// </summary>
        /// <param name="container">container</param>
        void CreateTable(IContainer container)
        {
            var headerStyle = TextStyle.Default.SemiBold();

            container.Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.ConstantColumn(25);
                    columns.RelativeColumn(3);
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                    columns.RelativeColumn();
                });

                table.Header(header =>
                {
                    header.Cell().Text("#").FontFamily("fangsong");
                    header.Cell().Text("消费类型").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("花费金额").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("数量").Style(headerStyle).FontFamily("fangsong");
                    header.Cell().AlignRight().Text("总金额").Style(headerStyle).FontFamily("fangsong");
                    //设置了表头单元格的属性
                    header.Cell().ColumnSpan(5).PaddingTop(5).BorderBottom(1).BorderColor(Colors.Black);
                });

                foreach (var item in Model.OrderItems)
                {
                    var index = Model.OrderItems.IndexOf(item) + 1;

                    table.Cell().Element(CellStyle).Text($"{index}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).Text(item.Name).FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity}").FontFamily("fangsong");
                    table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}").FontFamily("fangsong");
                    static IContainer CellStyle(IContainer container) => container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5);
                }
            });
        }

        #endregion

        #region 构建页脚部分

        void BuildComments(IContainer container)
        {
            container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Column(column =>
            {
                column.Spacing(5);
                column.Item().Text("DotNetGuide技术社区介绍").FontSize(14).FontFamily("fangsong").SemiBold();
                column.Item().Text(Model.Comments).FontFamily("fangsong");
            });
        }

        #endregion
    }

    public class AddressComponent : IComponent
    {
        private string Title { get; }
        private string CompanyName { get; }

        public AddressComponent(string title, string companyName)
        {
            Title = title;
            CompanyName = companyName;
        }

        public void Compose(IContainer container)
        {
            container.ShowEntire().Column(column =>
            {
                column.Spacing(2);

                column.Item().Text(Title).FontFamily("fangsong").SemiBold();
                column.Item().PaddingBottom(5).LineHorizontal(1);
                column.Item().Text(CompanyName).FontFamily("fangsong");
            });
        }
    }
}

Program

using QuestPDF;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;

namespace QuestPDFTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 1、请确保您有资格使用社区许可证,不设置的话会报异常。
            Settings.License = LicenseType.Community;

            // 2、禁用QuestPDF库中文本字符可用性的检查
            Settings.CheckIfAllTextGlyphsAreAvailable = false;

            // 3、PDF Document 创建
            var invoiceSourceData = CreateInvoiceDetails.GetInvoiceDetails();
            var document = new CreateInvoiceDocument(invoiceSourceData);

            // 4、生成 PDF 文件并在默认的查看器中显示
            document.GeneratePdfAndShow();
        }
    }
}

完整示例源代码

https://github.com/YSGStudyHards/QuestPDFTest

示例运行效果图

注意问题

中文报异常

QuestPDF.Drawing.Exceptions.DocumentDrawingException:“Could not find an appropriate font fallback for glyph: U-53D1 '发'. Font families available on current environment that contain this glyph: Microsoft JhengHei, Microsoft JhengHei UI, Microsoft YaHei, Microsoft YaHei UI, SimSun, NSimSun, DengXian, FangSong, KaiTi, SimHei, FZCuHeiSongS-B-GB. Possible solutions: 1) Use one of the listed fonts as the primary font in your document. 2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. However, this may result with text glyphs being incorrectly rendered without any warning.”

加上这段代码:

// 2、禁用QuestPDF库中文本字符可用性的检查
Settings.CheckIfAllTextGlyphsAreAvailable = false;

原因:

默认情况下,使用 QuestPDF 生成 PDF 文档时,它会检查所使用的字体是否支持文本中的所有字符,并在发现不能显示的字符时输出一条警告消息。这个选项可以确保文本中的所有字符都能正确地显示在生成的 PDF 文件中。

中文乱码问题

解决方案:

假如Text(“”)中为汉字一定要在后面加上FontFamily(“fangsong”)[仿宋字体]或FontFamily(“simhei”)[黑体字体],否则中文无法正常显示。

项目源码地址

更多项目实用功能和特性欢迎前往项目开源地址查看👀,别忘了给项目一个Star支持💖。

GitHub地址:https://github.com/QuestPDF/QuestPDF

文档地址:https://www.questpdf.com/api-reference/

优秀项目和框架精选

该项目已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没🤞)。

https://github.com/YSGStudyHards/DotNetGuide/blob/main/docs/DotNet/DotNetProjectPicks.md

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。

5分钟教会你如何在生产环境debug代码 - 欧阳码农 - 博客园

mikel阅读(421)

来源: 5分钟教会你如何在生产环境debug代码 – 欧阳码农 – 博客园

前言

有时出现的线上bug在测试环境死活都不能复现,靠review代码猜测bug出现的原因,然后盲改代码直接在线上测试明显不靠谱。这时我们就需要在生产环境中Debug代码,快速找到bug的原因,然后将锅丢出去。

生产环境的代码一般都是关闭source map和经过混淆的,那么如何进行Debug代码呢?我一般都是使用这两种方式debug线上代码:“通过console找到源代码打断点”和“通过network面板的Initiator找到源代码打断点”。

通过console找到源代码打断点

打开浏览器控制台的console面板,在上面找到由bug导致抛出的报错信息或者在代码里面通过console.log打的日志。然后点击最右边的文件名称跳转到具体的源码位置,直接在代码中打上断点就可以debug代码了。

如果点击右边的文件名后出现这种404报错的情况。
could-not-load-content-for-webpack://***-(fetch-through-target-failed:-unsupported-url-scheme;-fallback:-http-error:-status-code-404,-net:: ERR_UNKNOWN_URL_SCHEME)

只需要点击控制台右边倒数第三个图标setting(设置),将preferences(偏好设置)中的Enable JavaScript source maps(启用 JavaScript 源代码映射)取消勾选后再重新点console最右边的文件名称即可。

这种方式很简单就可以找到源代码,但是有的bug是没有报错信息的,而且我们也不可能到处都给代码加上console.log,所以这种方式有一定的局限性。

通过network面板的Initiator找到源代码打断点

将鼠标放到请求的Initiator(启动器)后,就会显示当前请求完整的调用链中的方法和函数。假如请求是由A函数中发起的,B函数调用了A函数,C函数又调用了B函数。那么这种情况中Initiator就会按照顺序依次将A、B、C函数都列出来。

了解了Initiator的作用思路就清晰了,我们只需要找到离bug最近的一个接口请求,然后从调用链中找到我们需要的方法或者函数就可以了。

这时有的小伙伴又会说了,线上的代码都是经过混淆的,原本代码中的函数和变量经过混淆后已经都不是原本的名字了,那么我们怎么知道调用栈中哪个是我们想要找的函数呢?

确实函数和变量名称经过混淆后已经变得面目全非了,但是对象中的方法和属性名称是不会被修改的,还是会保留原本的名字。比如我们有一个对象名字叫user,user中有个名叫dance的方法。经过混淆后user对象的名字可能已经变成了U,但是dance方法还是叫原本的名字,不会被修改。利用这一点我们可以在调用栈中找到我们熟悉的对象方法名称就可以很快的定位到源代码。

举个例子,我们当前有个service/common.js文件

import axios from "axios";

const urls = {
  messageList: "http://127.0.0.1:3000/api/getMessageList",
};

const methods = {
  getMessageList() {
    return axios({
      method: "get",
      url: urls.messageList,
    });
  },
};

export default {
  urls,
  methods,
};

业务组件中这样调用

import CommonService from "@/service/common.js";

async function initData() {
  const res = await CommonService.methods.getMessageList();
  const formatData: Array<Message> = handleFormatData(res.data.list);
  messageList.value = formatData;
}

Initiator调用栈中就可以很容易的找到getMessageList方法,并且我们知道getMessageList方法是我们的initData调用的。那么在调用栈中getMessageList的上一个就是我们想要找的源代码位置,点击文件名称就可以跳转到目标源代码具体的位置。

如果跳转到源代码后代码是被压缩的状态,点左下角的花括号将代码格式化。找到具体的定位后,经过比对其实混淆后的代码和源代码其实差别不是特别大,debug代码还是很容易的。

这时有的小伙伴又会问了,假如我们出现bug的地方没有接口请求怎么办呢?

这种情况也可以利用Initiator调用栈找到对应的源代码js文件,然后搜索你知道的属性和方法名字,因为属性和方法名称在混淆的过程中是不会被重写的。这样也可以找到源代码的位置。

总结

这篇文章主要介绍了两种在线上debug源码的方法。第一种方法是在控制台找到console输出,点击console右边的文件名称跳转到源码进行debug。第二种方式通过请求的Initiator调用栈,找到源代码中对应的方法,点击文件名称也可以跳转到源代码具体的位置。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

这才是你应该了解的Redis数据结构! - lyxlucky - 博客园

mikel阅读(234)

来源: 这才是你应该了解的Redis数据结构! – lyxlucky – 博客园

Redis,作为一种高性能的内存数据库,支持多种数据结构,从简单的字符串到复杂的哈希表。在这篇博文中,我们将深入探讨Redis的一些主要数据结构,并通过详细的例子展示它们的使用。

1. 字符串 (String)

1.1 存储和获取

Redis中的字符串是二进制安全的,可以存储任何数据。让我们通过一个简单的例子来演示:

# 存储字符串
SET my_key "Hello, Redis!"

# 获取字符串
GET my_key

在这个例子中,我们使用SET命令将字符串”Hello, Redis!”存储在my_key中,并通过GET命令获取它。

1.2 字符串操作

Redis提供了丰富的字符串操作,比如拼接、截取等。让我们看一个例子:

# 拼接字符串
APPEND my_key ", How are you?"

# 获取更新后的字符串
GET my_key

在这里,我们使用APPEND命令将”, How are you?”拼接到之前的字符串后面。

2. 列表 (List)

2.1 添加和获取元素

列表是一个有序的字符串元素集合。我们可以使用LPUSHLRANGE来添加和获取元素:

# 添加元素到列表的头部
LPUSH my_list "Apple"
LPUSH my_list "Banana"
LPUSH my_list "Orange"

# 获取列表的元素
LRANGE my_list 0 -1

在这个例子中,我们通过LPUSH命令将”Apple”、”Banana”和”Orange”添加到my_list的头部,并通过LRANGE命令获取整个列表。

2.2 列表操作

Redis提供了许多列表操作,比如裁剪、弹出等。让我们看一个例子:

# 裁剪列表,保留前两个元素
LTRIM my_list 0 1

# 弹出列表的最后一个元素
RPOP my_list

# 获取更新后的列表
LRANGE my_list 0 -1

在这里,我们使用LTRIM命令裁剪列表,保留前两个元素,然后使用RPOP命令弹出最后一个元素。

3. 集合 (Set)

3.1 添加和获取元素

集合是一个无序、唯一元素的集合。我们可以使用SADDSMEMBERS来添加和获取元素:

# 添加元素到集合
SADD my_set "Red"
SADD my_set "Green"
SADD my_set "Blue"

# 获取集合的所有元素
SMEMBERS my_set

在这个例子中,我们通过SADD命令将”Red”、”Green”和”Blue”添加到my_set,并通过SMEMBERS获取所有元素。

3.2 集合操作

Redis支持多种集合操作,比如交集、并集等。让我们看一个例子:

# 添加另一个集合
SADD my_set_2 "Green"
SADD my_set_2 "Yellow"

# 计算集合的交集
SINTER my_set my_set_2

在这里,我们通过SINTER命令计算my_setmy_set_2的交集。

4. 有序集合 (Sorted Set)

4.1 添加和获取元素

有序集合是一种集合,其中的每个元素都关联了一个分数,这使得我们可以按照分数排序元素。下面是一个示例:

# 向有序集合添加元素
ZADD my_zset 1 "Apple"
ZADD my_zset 2 "Banana"
ZADD my_zset 3 "Orange"

# 获取有序集合的所有元素
ZRANGE my_zset 0 -1 WITHSCORES

在这个例子中,我们使用ZADD命令向my_zset添加了三个元素,并通过ZRANGE命令获取所有元素及其分数。

4.2 有序集合操作

我们可以执行许多操作,例如查找特定排名范围的元素,或根据分数范围来查询元素。例如:

# 根据分数范围获取元素
ZRANGEBYSCORE my_zset 1 2

# 获取特定元素的排名
ZRANK my_zset "Banana"

5. 哈希 (Hash)

5.1 添加和获取元素

哈希是一种键值对集合,非常适合存储对象。以下是一个示例:

# 向哈希添加数据
HSET my_hash name "Alice"
HSET my_hash age "30"
HSET my_hash city "New York"

# 获取哈希中的所有键值对
HGETALL my_hash

在这个例子中,我们使用HSET命令向my_hash中添加了三个键值对,并用HGETALL获取了所有键值对。

5.2 哈希操作

哈希结构提供了丰富的操作,比如只获取所有的键或值,或者删除特定的键。例如:

# 获取所有键
HKEYS my_hash

# 获取所有值
HVALS my_hash

# 删除一个键
HDEL my_hash name

6. HyperLogLog

6.1 添加元素

HyperLogLog 是用于估计基数(集合中不重复元素的数量)的数据结构。下面是一个示例:

# 添加元素到 HyperLogLog
PFADD my_hyperloglog "Apple"
PFADD my_hyperloglog "Banana"
PFADD my_hyperloglog "Orange"

在这个例子中,我们使用 PFADD 命令向 my_hyperloglog 添加了三个元素。

6.2 估算基数

HyperLogLog 提供了估算基数的功能:

# 估算基数
PFCOUNT my_hyperloglog

这个命令返回 HyperLogLog 中不同元素的估算数量。

HyperLogLog 在处理大型数据集时非常有用,因为它能够以固定的内存消耗来估算基数,而不需要存储所有元素。

7. Bitmaps

7.1 设置和获取位

Bitmaps 是一种位图数据结构,可以用于存储和处理位信息。下面是一个简单的示例:

# 设置位
SETBIT my_bitmap 0 1
SETBIT my_bitmap 2 1

# 获取位的值
GETBIT my_bitmap 0
GETBIT my_bitmap 1

在这个例子中,我们使用 SETBIT 命令设置了位,然后使用 GETBIT 命令获取了相应位的值。

7.2 位操作

Bitmaps 还支持位操作,例如按位与、按位或、按位异或等:

# 按位与
BITOP AND result_bitmap my_bitmap1 my_bitmap2

# 按位或
BITOP OR result_bitmap my_bitmap1 my_bitmap2

# 按位异或
BITOP XOR result_bitmap my_bitmap1 my_bitmap2

这些位操作可以用于处理多个位图之间的关系。

Bitmaps 在一些场景下非常有用,例如统计用户的在线状态、记录用户的行为等。使用 Bitmaps 可以在占用较少内存的情况下高效地处理大量位信息。

8. Streams

8.1 添加消息

Streams 是一种日志数据结构,允许你按时间顺序添加、读取和消费消息。以下是一个简单的示例:

# 添加消息到 Stream
XADD mystream * name John age 30

# 添加另一条消息
XADD mystream * name Jane age 25

在这个例子中,我们使用 XADD 命令向名为 mystream 的 Stream 添加了两条消息。

8.2 读取消息

可以使用 XRANGE 命令按范围读取消息:

# 读取所有消息
XRANGE mystream - +

这将返回 mystream 中的所有消息。

Streams 在处理事件日志、消息队列等场景中非常有用,因为它允许按时间顺序组织和检索消息。

9. Geospatial 数据结构

9.1 添加地理位置

Geospatial 数据结构可以用来存储地理位置的信息,比如经度和纬度。以下是一个简单的示例:

# 添加地理位置信息
GEOADD locations 13.361389 38.115556 "Palermo"
GEOADD locations 15.087269 37.502669 "Catania"

在这个例子中,我们使用 GEOADD 命令添加了两个地理位置信息,分别是 “Palermo” 和 “Catania”。

9.2 查询附近的位置

可以使用 GEODIST 命令计算两个位置之间的距离,或者使用 GEORADIUS 命令查找附近的位置:

# 计算两个位置之间的距离
GEODIST locations "Palermo" "Catania" km

# 查找附近的位置
GEORADIUS locations 15 37 100 km

这些命令使得在地理信息系统中进行位置相关的操作变得非常方便。

结语

通过这些详细的例子,我们深入了解了Redis的数据结构。当我们在实际项目中选择合适的数据结构时,这些例子将为我们提供有力的指导。希望这篇博文对你加深对Redis数据结构的理解有所帮助。如果你有其他关于Redis的问题,欢迎留言讨论!

全流程机器视觉工程开发(一)环境准备,paddledetection和labelme-CSDN博客

mikel阅读(281)

来源: 全流程机器视觉工程开发(一)环境准备,paddledetection和labelme-CSDN博客

前言
我现在在准备做一个全流程的机器视觉的工程,之前做了很多理论相关的工作。大概理解了机器视觉的原理,然后大概了解了一下,我发现现在的库其实已经很发展了,完全不需要用到非常多的理论,只需要知道开发过程就可以了,甚至paddlex已经直接有了傻瓜式模型训练的软件,所以我现在准备来做一个全流程机器视觉工程开发,不涉及过多理论。

准备
现在准备一下机器视觉工程的前情提要。

我准备使用paddledetection来做机器视觉。什么是paddleDetection?你可以理解为paddlepaddle对于目前主流的机器学习模型做了一些整合,只需要使用paddleDetection库就可以做一个很方便的训练、预测等工作。

准备好paddledetection之后,也就是我们的模型工具之后,还需要对现有图片做一些简单的划分工作,这里就需要用到labelme工具来进行.

 

环境安装
我这个教程和别的教程不太一样。因为年代久远,paddledetection库的原始安装方式已经不太适用了,所以我这里重新写一个paddledetection安装方式。

主要流程大概如下:

安装anaconda
安装paddle库
安装CUDA库
去github上下载paddledetection仓库
给自己安装pycocotools和lap库
直接安装paddledetection的依赖包requirements.txt
安装paddledetection
流程
安装anaconda
这步略,不知道的可以浏览:Anaconda安装教程(超详细版)

安装paddle库
这步略,参考paddle官网,不行就自己在csdn上搜,或者看我往期
这里给出官网链接:开始使用

安装CUDA库
这步略,参考本人往期文章:简易机器学习笔记(十)Windows下 PaddlePaddle配置CUDA加速环境

去github上下载paddledetection仓库
github链接:PaddleDetection
你要做的就是直接把这个仓库clone到本地,拉下来的项目大概是这样的

里面是这一大堆东西,暂时先不管是干嘛的,只需要先放在这里就可以了。

pycocotools和lap库
到一般的教程了,这里会告诉你直接去安装requirements.txt,但是很多人现在可能会直接报错numpy的问题,这个可能是因为库实在是年久失修了,主要出问题的库实际上就那么两个,一个是pycocotools,一个是lap
首先可以尝试一下能不能直接安装这两个库,也就是直接尝试以下两条命令

pip install pycocotools
pip install lap

一般情况下这个pycocotools是没问题的,出问题的是这个lap库,我这里主要演示lap库怎么手动安装,pycocotools也是同理

首先我们找到两个库的github地址:

pycocotools
lap

把这两个库clone到本地,大概是这样

在cmd中使用python尝试安装这个setup.py文件,指令大概是:

#path/to/setup.py指代setup.py的路径
python path/to/setup.py install

注意这条指令需要使用setuptools,怎么安装这个库不过多赘述了

一般这样手动安装就可以正常安装成功了,pycocotools和lap库都是这样安装的。

直接安装paddledetection的依赖包requirements.txt
lap库和pycocotools安装完毕后,基本上问题就不大了。现在只需要使用以下指令来对paddledetection包中的requirements进行安装就行了

#path/to/requirements.txt 指代paddledetection库下的requirements.txt的路径
pip install -r path/to/requirements.txt

我们可以打开requirements.txt来看一看,里面也只有一些库的名字而已

 

安装paddledetection
到这里基本上就快安装完了,只需要最后一步,就是安装paddledetection,流程和安装lap库差不多
#path/to/setup.py 指代paddledetection库下的setup.py的路径
python path/to/setup.py install

数据标注工具labelme
刚刚我们安装完了paddledetection库,也就是准备好模型,接下来要准备的就是数据标注工具labelme

这个比较简单,直接参考博客:添加链接描述
————————————————
版权声明:本文为CSDN博主「Leventure_轩先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Andius/article/details/135675515

浅谈6种流行的API架构风格 - 追逐时光者 - 博客园

mikel阅读(284)

来源: 浅谈6种流行的API架构风格 – 追逐时光者 – 博客园

浅谈6种流行的API架构风格

前言

API在现代软件开发中扮演着重要的角色,它们是不同应用程序之间的桥梁。编写业务API是日常开发工作中最常见的一部分,选择合适的API框架对项目的成功起到了至关重要的作用。本篇文章将浅谈一下当前6种流行的API架构风格的优点、缺点以及适用场景。

6种流行的API架构风格图

SOAP

SOAP全拼:Simple Object Access Protocol

  • 优点:SOAP 是一种基于 XML 的通信协议,具有良好的跨平台和跨语言支持。它提供了丰富的安全性和事务管理功能,并支持复杂的消息交换模式。
  • 缺点:SOAP 在处理大量数据时可能效率较低,因为它使用了冗长的 XML 格式,并且需要较多的带宽和处理能力。
  • 适用场景:SOAP 适用于需要高安全性和复杂数据交换的企业级应用程序和 Web 服务场景,尤其是需要实现事务处理和消息传递机制的场景。

RESTful

RESTful全拼:Representational State Transfer

  • 优点:RESTful 一种基于现有 Web标准和 HTTP协议的设计和构建网络应用程序的架构风格,旨在提供一种简洁、可扩展、可靠和可互操作的方式来进行网络通信。它具有良好的可伸缩性、可缓存性和可见性,并支持多种数据格式(如:JSON、XML等)。
  • 缺点:缺乏标准化、安全性问题、粒度问题、难以处理复杂逻辑、复杂性问题和版本管理问题。
  • 适用场景:RESTful 适用于构建 Web 应用程序和移动应用程序的 API,特别是那些需要简单和易于使用的场景。

GraphQL

  • 优点:GraphQL 是一种由 Facebook 开发的查询语言和运行时执行环境。它允许客户端精确地指定所需的数据,并减少了网络传输的数据量。GraphQL 还提供了强大的类型系统和自动文档生成。
  • 缺点:GraphQL 在处理大型查询和复杂数据模型时可能存在性能问题,因为它需要在运行时解析查询,并执行多个数据源之间的数据获取操作。
  • 适用场景:GraphQL 适用于需要灵活数据获取和精确控制的应用程序,特别是面向移动设备的应用程序和需要聚合多个数据源的场景。

gRPC

gRPC全拼:Google Remote Procedure Call

  • 优点:gRPC 是一种高性能、开源的远程过程调用框架,基于 Protocol Buffers(protobuf)序列化协议。它提供了强大的类型系统、双向流和流式数据传输的支持。
  • 缺点:gRPC 对网络稳定性有较高的要求,不太适合部署在不可靠的网络环境中。
  • 适用场景:gRPC 适用于构建分布式系统和微服务架构,特别是那些需要高性能和强类型约束的场景。

WebSocket

  • 优点:WebSocket 提供了全双工通信的能力,允许服务器主动向客户端推送数据。它具有低延迟、高吞吐量和实时性的特点。
  • 缺点:WebSocket 对于服务器和客户端都需要保持长时间的连接,这可能增加服务器的负载,并且需要较高的网络稳定性。
  • 适用场景:WebSocket 适用于实时通信和实时数据更新的应用程序,特别是聊天应用、协作工具和实时游戏等场景。

Webhook

  • 优点:Webhook 是一种通过 HTTP 请求将事件通知发送给预定义 URL 的机制。它能够实时推送数据并触发自定义的后续操作。
  • 缺点:Webhook 需要事先配置目标 URL,并且对于每个事件都需要建立一个独立的 Webhook。此外,Webhook 不支持请求-响应模式。
  • 适用场景:Webhook 适用于需要实时事件通知和与其他应用程序集成的场景,特别是信息发布、应用程序集成和自动化工作流等场景。

总结

这些 API 架构风格都各有优点和适用场景,您可以根据具体需求选择适合的架构风格来构建和设计 API。

 

 

解决WordPress”无需升级 您的WordPress数据库已经是最新的了!” 关闭插件后无法登陆后台-腾讯云开发者社区-腾讯云

mikel阅读(296)

来源: 解决WordPress”无需升级 您的WordPress数据库已经是最新的了!” 关闭插件后无法登陆后台-腾讯云开发者社区-腾讯云

捣鼓另一个站点的时候关闭了全部插件,结果前台无法访问,后台提示”无需升级 您的WordPress数据库已经是最新的了!”,如下图。因为这个站点是有使用缓存的,可能是关闭了缓存导致的。

解决办法,前往你的WordPress安装目录下的  wp-content  下。找到object-cache.php,把它的名字改一下(改成任意都可以。)

然后就可以访问了,恢复正常。

宝塔中nginx配置websocket的wss协议_宝塔安装websocket启动-CSDN博客

mikel阅读(341)

来源: 宝塔中nginx配置websocket的wss协议_宝塔安装websocket启动-CSDN博客

配置ws也就是不安全的协议,比较简单,可以参考我之前的文章
在配置wss之前要准备好ssl证书,
配置证书也很简单,只要上传两个证书,就好了,我这里就不再赘述了,我这里证书已经配置好了,配置wss的代码如下
server
{
    listen 80;
listen 443 ssl http2;
    server_name kf.xxx.com;
    index index.php index.html index.htm default.php default.htm default.html;
    root /www/wwwroot/kf.xxx.com/public;
    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;
    #HTTP_TO_HTTPS_START
    if ($server_port !~ 443){
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END
    ssl_certificate    /www/server/panel/vhost/cert/kf.xxx.com/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/kf.xxx.com/privkey.pem;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    error_page 497  https://$host$request_uri;
    #SSL-END
    #ERROR-PAGE-START  错误页配置,可以注释、删除或修改
    #error_page 404 /404.html;
    #error_page 502 /502.html;
    #ERROR-PAGE-END
    #PHP-INFO-START  PHP引用配置,可以注释或修改
    include enable-php-56.conf;
    #PHP-INFO-END
    #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
    include /www/server/panel/vhost/rewrite/kf.xxx.com.conf;
    #REWRITE-END
    #禁止访问的文件或目录
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
    {
        return 404;
    }
    #一键申请SSL证书验证目录相关设置
    location ~ \.well-known{
        allow all;
    }
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
    {
        expires      30d;
        error_log off;
        access_log /dev/null;
    }
    location ~ .*\.(js|css)?$
    {
        expires      12h;
        error_log off;
        access_log /dev/null;
    }
    location / {
      if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php?s=$1 last;
        break;
      }
    }
     location /wss {
            proxy_pass http://127.0.0.1:7272;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection “upgrade”;
            rewrite /wss/(.*) /$1 break;
            proxy_redirect off;
  }
    access_log  /www/wwwlogs/kf.xxx.com.log;
    error_log  /www/wwwlogs/kf.xxx.com.error.log;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
其中最重要的就是这一部分
 location /wss {
            proxy_pass http://127.0.0.1:7272;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection “upgrade”;
            rewrite /wss/(.*) /$1 break;
            proxy_redirect off;
  }
1
2
3
4
5
6
7
8
9
10
11
因为我的服务器上已经开了一个7272端口,这个端口运行的是websocket服务,当路径包含/wss就走到这个服务,
所有在调用的时候也得包含/wss,调用的代码如下,我截取了一部分
一定要跟一个/wss不然找不到。
————————————————
版权声明:本文为CSDN博主「reg183」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/chendongpu/article/details/123354674

宝塔nginx环境如何配置 wss WebSocket 连接_宝塔如何建立websocket协议的链接-CSDN博客

mikel阅读(331)

来源: 宝塔nginx环境如何配置 wss WebSocket 连接_宝塔如何建立websocket协议的链接-CSDN博客

1、需要一个备案域名,在宝塔可申请免费ssl证书,ssl 证书这里有个大坑,为何宝塔申请成功总是访问https失败,我发现关闭防火墙就行了。

2、开放端口,宝塔环境新开放一个端口

nginx 配置

 

location /wss {
proxy_pass http://127.0.0.1:2022;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
rewrite /wss/(.*) /$1 break;
proxy_redirect off;
}

3、宝塔终端启动:php server.php start -d

4、测试:

<!DOCTYPE html>

<html>

<head>

<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />

<title>Title</title>

</head>

<body>

<script>

var ws = new WebSocket(“wss://www.xxx.com/wss”);

ws.onopen = function() {

alert(“连接成功”);

ws.send(‘tom’);

alert(“给服务端发送一个字符串:tom”);

};

ws.onmessage = function(e) {

alert(“收到服务端的消息:” + e.data);

};

</script>

</body>

</html>
————————————————
版权声明:本文为CSDN博主「神夜大侠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012015434/article/details/124890321