分布式集群下WebSocket Session共享解决方案

接上一篇 SpringBoot集成WebSocket进行消息主动推送
分布式集群下WebSocket Session共享解决方案

在实现中需要解决的类变量有两个

private static AtomicInteger online = new AtomicInteger();

private static Map<String, Session> sessionPools = new ConcurrentHashMap<>();

其中online可以用Redis实现存储

Session无法采用Redis进行存储, 因为不能对Session进行序列化

由于session无法实现序列化,不能存储到redis这些中间存储里面,因此这里我们只能把session存储在本地的内存中,那么如果是集群的话,我们如何实现session准确的发送消息呢,其实就是session共享。在websocket中,其实是无法做到session共享的

目前通用的解决方案都是通过消息中间件,实现消息的发布与订阅

向前端推送消息时, 向消息队列中发布消息, 所有的后端服务器订阅该消息, 所有的后端服务器收到消息消费消息时, 都执行推送消息, 本地有Session的即可推送成功(前端只可能跟某一个后端建立Session)

具体实现

1. 引入Redis

此处忽略Redis 的配置及依赖, 直接上封装的服务实现类

IRedisDao

import java.util.List;
import java.util.Map;

/**
 * @Author:
 * @Date:2023/7/3 11:24
 * @Des: IRedisDao
 */
public interface IRedisDao {

    /**
     * 设置增量
     *
     * @param key   键名
     * @param value 增量值
     * @return 增加后的值
     */
    long incryBy(String key, long value);

    /**
     * 添加字符串并设置过期时间
     *
     * @param key        键名
     * @param value      字符串值
     * @param expireTime 过期时间,单位:分钟
     */
    void setString(String key, String value, int expireTime);

    /**
     * 添加字符串
     *
     * @param key   键名
     * @param value 字符串值
     */
    void setString(String key, String value);

    /**
     * 获取字符串值
     *
     * @param key 键名
     * @return 字符串值
     */
    String getString(String key);

    /**
     * 删除字符串
     *
     * @param key 键名
     */
    void delString(String key);

    /**
     * 加锁
     *
     * @param key            键名
     * @param expiredSeconds 过期时间,单位:秒
     * @param lockFlag       锁标志
     *                       锁标志的作用主要有两个方面:
     *                       唯一性检查:在使用connection.set()方法设置锁时,通过指定SET_IF_ABSENT选项,只有当该键在Redis中不存在时才会设置成功。这样可以确保只有一个实体或线程能够成功获取到锁,其他的尝试会被拒绝。
     *                       释放锁时的验证:在释放锁时,可以通过比对锁标志的值来验证是否是持有锁的实体或线程进行释放操作。只有当锁标志匹配时才执行释放操作,以防止其他实体或线程错误释放锁。
     *                       通过使用锁标志,可以实现简单的分布式锁机制,用于控制并发访问共享资源的情况,确保同一时间只有一个实体或线程能够访问该资源。
     * @return true:加锁成功,false:已经加锁
     */
    boolean addLock(String key, int expiredSeconds, String lockFlag);

    /**
     * 释放锁
     *
     * @param key      键名
     * @param lockFlag 锁标志
     * @return true:释放成功,false:释放失败
     */
    boolean releaseLock(String key, String lockFlag);

    /**
     * 向Set集合中添加元素
     *
     * @param key    键名
     * @param member 元素值
     * @return 添加成功的数量
     */
    long sAdd(String key, String member);

    /**
     * 从Set集合中移除元素
     *
     * @param key    键名
     * @param member 元素值
     */
    void sRemove(String key, String member);

    /**
     * 批量从Set集合中移除元素
     *
     * @param key    键名
     * @param member 元素值
     */
    void sRemoveBatch(String key, Object... member);

    /**
     * 判断元素是否存在于Set集合中
     *
     * @param key    键名
     * @param member 元素值
     * @return true:存在,false:不存在
     */
    boolean sIsMember(String key, String member);

    /**
     * 设置键的过期时间
     *
     * @param key            键名
     * @param expiredSeconds 过期时间,单位:秒
     */
    void expire(String key, int expiredSeconds);

    /**
     * 将Map中的键值对保存到Redis的Hash结构中
     *
     * @param key  Hash结构的键名
     * @param data Map类型的数据
     * @return true:保存成功,false:保存失败
     */
    void hmset(String key, Map<String, String> data);

    /**
     * 存hash结构中的键值对
     *
     * @param redisKey Hash结构的键名
     * @param hashKey
     * @param value
     */
    void hput(String redisKey, String hashKey, String value);

    /**
     * hash结构删除键值对
     * @param redisKey
     * @param hashKey
     */
    void hdel(String redisKey, String hashKey);


    String hget(String redisKey, String hashKey);

    /**
     * 判断键是否存在
     *
     * @param key 键名
     * @return true:存在,false:不存在
     */
    boolean hasKey(String key);

    /**
     * 将指定的field和value添加到Redis哈希结构中的key中,仅当该field在哈希结构中不存在时才执行添加操作。
     *
     * @param key   键名
     * @param field 字段名
     * @param value 字段值
     * @return true:添加成功,false:字段已存在,添加失败
     */
    public boolean putIfAbsentDB(String key, String field, String value);

    /**
     * 将值从左侧压入列表
     *
     * @param key   键名
     * @param value 值
     * @return 列表的长度
     */
    public Long lPushDB(String key, String value);

    /**
     * 返回列表中指定范围的元素
     *
     * @param key   键名
     * @param start 起始索引
     * @param end   结束索引
     * @return 指定范围内的元素列表
     */
    public List<String> rRangeDB(String key, long start, long end);

    /**
     * 修剪列表,只保留指定范围内的元素
     *
     * @param key   键名
     * @param start 起始索引
     * @param end   结束索引
     */
    public void lTrimDB(String key, long start, long end);

    /**
     * 获取列表的长度
     *
     * @param key 键名
     * @return 列表的长度
     */
    public Long lSizeDB(String key);
}
RedisDaoImpl

import com.sinotrans.gtp.exception.AppCodeMsg;
import com.sinotrans.gtp.exception.AppException;
import io.lettuce.core.api.sync.RedisCommands;
import lombok.extern.slf4j.Slf4j;
import org.osgi.framework.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Repository;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Author:
 * @Date:2023/7/3 11:25
 * @Des: RedisDaoImpl Redis操作封装类
 */
@Repository("redisDao")
@Slf4j
public class RedisDaoImpl implements IRedisDao {

    private static final int SECOND = 60;

    private static final int MINUTE = SECOND * 60;

    private static final int HOUR = MINUTE * 60;

    private static final int DAY = HOUR * 24;

    private static final int WEEK = DAY * 7;


    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public long incryBy(String key, long increment) {
        Long value = redisTemplate.opsForValue().increment(key, increment);
        return value == null ? 0L : value.longValue();
    }

    @Override
    public void setString(String key, String value, int expireTime) {
        redisTemplate.opsForValue()
                .set(key, value, expireTime, TimeUnit.MINUTES);
    }

    @Override
    public void setString(String key, String value) {
        redisTemplate.opsForValue()
                .set(key, value);
    }

    @Override
    public String getString(String key) {
        String value = redisTemplate.opsForValue().get(key);
        return value;
    }

    @Override
    public void delString(String key) {
        redisTemplate.delete(key);
    }

    @Override
    public boolean addLock(String key, int expiredSeconds, String lockFlag) {

        return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
            Boolean set = connection.set(key.getBytes(StandardCharsets.UTF_8), lockFlag.getBytes(StandardCharsets.UTF_8), Expiration.seconds(expiredSeconds), RedisStringCommands.SetOption.SET_IF_ABSENT);
            if (set == null) {
                return false;
            }
            return set;
        });

    }

    @Override
    public boolean releaseLock(String key, String lockFlag) {
        DefaultRedisScript<Boolean> releaseScript = new DefaultRedisScript<>(
                "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
                        "   return redis.call('DEL', KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end",
                Boolean.class
        );
        List<String> keys = Collections.singletonList(key);
        Boolean release = redisTemplate.execute(releaseScript, keys, lockFlag);
        return release != null && release;
    }


    @Override
    public long sAdd(String key, String member) {
        Long rs = redisTemplate.opsForSet().add(key, member);
        long endTime = System.currentTimeMillis();
        return rs == null ? 0L : rs.longValue();
    }

    @Override
    public void sRemove(String key, String member) {
        redisTemplate.opsForSet().remove(key, member);
        long endTime = System.currentTimeMillis();
    }

    @Override
    public void sRemoveBatch(String key, Object... member) {
        redisTemplate.opsForSet().remove(key, member);
        long endTime = System.currentTimeMillis();
    }

    @Override
    public boolean sIsMember(String key, String member) {
        boolean isMember = redisTemplate.opsForSet().isMember(key, member);
        return isMember;
    }

    @Override
    public void expire(String key, int expiredSeconds) {
        redisTemplate.expire(key, expiredSeconds, TimeUnit.SECONDS);
    }

    @Override
    public void hmset(String redisKey, Map<String, String> data) {
        redisTemplate.opsForHash().putAll(redisKey, data);
//            redisTemplate.expire(redisKey, 52 * WEEK, TimeUnit.SECONDS);
    }

    @Override
    public void hput(String redisKey, String hashKey, String value) {
        redisTemplate.opsForHash().put(redisKey, hashKey, value);
    }

    @Override
    public void hdel(String redisKey, String hashKey) {
        redisTemplate.opsForHash().delete(redisKey, hashKey);
    }

    public String hget(String redisKey, String hashKey) {
        return (String) redisTemplate.opsForHash().get(redisKey, hashKey);
    }

    @Override
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }


    /**
     * hash putIfAbsent
     *
     * @param key
     * @param field
     * @param value
     * @return
     */
    public boolean putIfAbsentDB(String key, String field, String value) {
        Boolean result = redisTemplate.opsForHash().putIfAbsent(key, field, value);
        return result;
    }

    public Long lPushDB(String key, String value) {
        Long result = redisTemplate.opsForList().leftPush(key, value);
        return result;
    }

    public List<String> rRangeDB(String key, long start, long end) {
        List<String> list = redisTemplate.opsForList().range(key, start, end);
        return list;
    }

    public void lTrimDB(String key, long start, long end) {
        redisTemplate.opsForList().trim(key, start, end);
        long endTime = System.currentTimeMillis();
        return;
    }

    public Long lSizeDB(String key) {
        Long size = redisTemplate.opsForList().size(key);
        return size;
    }


}

2. 改写在线人数的实现

该处人数统计并不是最终解决方案

此处的解决是, 将建立的客户端标识存储至Redis的String数据结构中, 用固定的前缀拼接

设置过期时间1天, 最终保持一致性

通过key通配符的查询方式获取人数

最终解决方案需要心跳机制(此处暂未实现)

通过后端定时任务去推送一段文本随意一段即可,

存储至Redis 的前端客户端标识中带有当前后端的host+port, 并设置10分钟超时时间

后端定时任务(5分钟一跑)业务逻辑中从Redis拿到属于当前后端的前端客户端标识, 去一一发送心跳, 判断是否发送成功, 成功则继续将该标识续命为10分钟超时时间, 如果服务运转正常, 将会成功发送, 如果发送失败删除标识即可, 如果服务挂掉, 标识10分钟将自动过期无法续命


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.Set;


/**
 * @Author:
 * @Date:2023/7/3 14:41
 * @Des: WebSocketRedisDao WebSocketRedis存储类
 */
@Repository
public class WebSocketRedisDao {

    
    private static final String ONLINE_CLIENT_KEY_PRE = "ONLINE_#_CLIENT_ID_";

    @Autowired
    private IRedisDao redisDao;

    /*人数相关操作---弃用 : 会有一致性问题, 通过下面存客户端标识实现获取人数*/
    /*
    public void addOnlineCount() {
        redisDao.incryBy(ONLINE_COUNT_KEY, 1);
    }

    public void subOnlineCount() {
        redisDao.incryBy(ONLINE_COUNT_KEY, -1);
    }

    public int getOnlineCount() {
        Integer count = Integer.valueOf(redisDao.getString(ONLINE_COUNT_KEY));
        return count != null ? count : 0;
    }*/


    /*前端客户端标识相关操作*/
    public void addClientId(String clientId) {
        // 设置过期时间1天(在线人数 假如SpringBoot程序被不正常打断, 会导致Redis没有删除活跃的客户端标识, 最终通过过期删除, 最终保持一致性)
        redisDao.setString(ONLINE_CLIENT_KEY_PRE + clientId, "0", 60 * 24);
    }

    public void removeClientId(String clientId) {
        redisDao.delString(ONLINE_CLIENT_KEY_PRE + clientId);
    }

    /**
     * 存储的clientId 非用户名
     *
     * @return
     */
    public Set<String> getClientsSet() {
        return redisDao.getKeysByPattern(ONLINE_CLIENT_KEY_PRE + "*");
    }


}

websocket无法通过注解方式注入bean的解决办法
引入ApplicationContextUtils类

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @Author:
 * @Date:2023/7/4 10:57
 * @Des: ApplicationContextUtils 用来获取SpringBoot创建好的工厂
 */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    // 保留下来工厂
    private static ApplicationContext applicationContext;

    // 将创建好的工厂以参数的形式传递给这个类
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 提供在工厂中获取对象的方法 // RedisTemplate redisTemplate
    public static Object getBeanByName(String beanName) {
        return applicationContext.getBean(beanName);
    }

    public static <T> T getBeanByClazz(Class<T> clazz) {

        return applicationContext.getBean(clazz);
    }

}

注入方式
private WebSocketRedisDao webSocketRedisDao = ApplicationContextUtils.getBeanByClazz(WebSocketRedisDao.class);
3. 使用Redis实现发布订阅模型

参考另一篇博客
SpringBoot 基于Redis的消息队列(基于发布订阅模型)

4. session共享

此时调用发送消息时, 由之前的直接调用WebSocket的消息发送方法, 改为往消息队列中发布消息

在消息队列的订阅方法中, 再进行调用WebSocket的消息发送方法即可

5. 其他
5.1. OnClose方法中存在问题

当SpringBoot程序关闭时, 主动触发OnClose注解所在方法执行人数扣减操作

此处需要手动在方法里面获取webSocketRedisDao, 防止已经结束生命周期无法操作

/**
     * 关闭连接时调用
     *
     * @param userName 关闭连接的客户端的姓名
     */
@OnClose
public void onClose(@PathParam(value = "name") String userName) {
    WebSocketRedisDao webSocketRedisDao = ApplicationContextUtils.getBeanByClazz(WebSocketRedisDao.class);
    sessionPools.remove(userName);
    webSocketRedisDao.removeClientId(userName);
    subOnlineCount();
    log.info(userName + "断开webSocket连接!当前SpringBoot实例活跃前端客户端数为" + currentSpringBootOnline.get());
    log.info(userName + "断开webSocket连接!当前整个服务集群总活跃前端客户端数为" + webSocketRedisDao.getClientsSet().size());
}
5.2. 暂未实现判断是否在线
6. WebSocketServer

升级后的WebSocketServer


import com.wd.gtp.component.ApplicationContextUtils;
import com.wd.gtp.dao.redis.WebSocketRedisDao;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author:
 * @Date:2023/6/26 10:11
 * @Des: WebSocketServer WebSocket服务端代码,包含接收消息,推送消息等接口
 */
@Component
@Slf4j
@ServerEndpoint(value = "/socket/{name}")
public class WebSocketServer {

    private WebSocketRedisDao webSocketRedisDao = ApplicationContextUtils.getBeanByClazz(WebSocketRedisDao.class);

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger currentSpringBootOnline = new AtomicInteger(); // 当前实例链接人数

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static Map<String, Session> sessionPools = new ConcurrentHashMap<>();

    /**
     * 发送消息方法
     *
     * @param session 客户端与socket建立的会话
     * @param message 消息
     * @throws IOException
     */
    public void sendMessage(Session session, String message) throws IOException {
        if (session != null) {
            log.info("Session获取非空, 消息推送成功");
            session.getBasicRemote().sendText(message);
        } else {
            log.info("Session获取为空, 消息未推送");
        }
    }

    /**
     * 连接建立成功调用
     *
     * @param session  客户端与socket建立的会话
     * @param userName 客户端的userName
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "name") String userName) {
        sessionPools.put(userName, session);
        webSocketRedisDao.addClientId(userName);
        addOnlineCount();
        log.info(userName + "打开webSocket连接!当前SpringBoot实例活跃前端客户端数为" + currentSpringBootOnline.get());
        log.info(userName + "打开webSocket连接!当前整个服务集群总活跃前端客户端数为" + webSocketRedisDao.getClientsSet().size());
        try {
            sendMessage(session, "欢迎" + userName + "加入连接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭连接时调用
     *
     * @param userName 关闭连接的客户端的姓名
     */
    @OnClose
    public void onClose(@PathParam(value = "name") String userName) {
        WebSocketRedisDao webSocketRedisDao = ApplicationContextUtils.getBeanByClazz(WebSocketRedisDao.class);
        sessionPools.remove(userName);
        webSocketRedisDao.removeClientId(userName);
        subOnlineCount();
        log.info(userName + "断开webSocket连接!当前SpringBoot实例活跃前端客户端数为" + currentSpringBootOnline.get());
        log.info(userName + "断开webSocket连接!当前整个服务集群总活跃前端客户端数为" + webSocketRedisDao.getClientsSet().size());
    }

    /**
     * 发生错误时候
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        log.error("异常", throwable);
    }

    /**
     * 给指定前端客户端发送消息
     *
     * @param clientId 前端客户端标识
     * @param message  消息
     */
    public void sendInfoClient(String clientId, String message) {
        Session session = sessionPools.get(clientId);
        try {
            sendMessage(session, message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 给指定用户发送消息
     * 该情况下考虑到了 同一用户多次登录, 导致只会向一个发送
     * map中存的key:  (用户名:浏览器标识)  [GTM.ADMIN]:[浏览器标识]  其中浏览器标识最好客户端唯一 每次发起请求都是一样的每个客户端都是唯一的
     *
     * @param id      用户名
     * @param message 消息
     */
    public void sendInfoUser(String id, String message) {
        Set<String> keySet = sessionPools.keySet();
        try {
            for (String key : keySet) {
                if (prefix(key).equals(id)) {
                    sendMessage(sessionPools.get(key), message);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 收到客户端消息时触发(群发)
     *
     * @param message
     * @throws IOException
     */
    @OnMessage
    public void onMessage(String message) {
        for (Session session : sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    private String prefix(String key) {
        return key.substring(0, key.indexOf(":"));
    }

    // 判断用户是否在线
    public boolean isOnline(String userId) {
        Set<String> keySet = webSocketRedisDao.getClientsSet();
        for (String key : keySet) {
            if (prefix(key).equals(userId)) {
                return true;
            }
        }
        return false;
    }


    public static void addOnlineCount() {
        currentSpringBootOnline.incrementAndGet();
    }

    public static void subOnlineCount() {
        currentSpringBootOnline.decrementAndGet();
    }

}


  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring4GWT GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet.Applet 简单实现!~ 网页表格组件 GWT Advanced Table GWT Advanced Table 是一个基于 GWT 框架的网页表格组件,可实现分页数据显示、数据排序和过滤等功能! Google Tag Library 该标记库和 Google 有关。使用该标记库,利用 Google 为你的网站提供网站查询,并且可以直接在你的网页里面显示搜查的结果。 github-java-api github-java-api 是 Github 网站 API 的 Java 语言版本。 java缓存工具 SimpleCache SimpleCache 是一个简单易用的java缓存工具,用来简化缓存代码的编写,让你摆脱单调乏味的重复工作!1. 完全透明的缓存支持,对业务代码零侵入 2. 支持使用Redis和Memcached作为后端缓存。3. 支持缓存数据分区规则的定义 4. 使用redis作缓存时,支持list类型的高级数据结构,更适合论坛帖子列表这种类型的数据 5. 支持混合使用redis缓存和memcached缓存。可以将列表数据缓存到redis中,其他kv结构数据继续缓存到memcached 6. 支持redis的主从集群,可以做读写分离。缓存读取自redis的slave节点,写入到redis的master节点。 Java对象的SQL接口 JoSQL JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于【自动提示】的需要(如:Google搜索), 而开发的架构无关的公共控件, 以满足该类需求可以通过快速配置来开发。AutoTips基于搜索引擎Apache Lucene实现。AutoTips提供统一UI。 WAP浏览器 j2wap j2wap 是一个基于Java的WAP浏览器,目前处于BETA测试阶段。它支持WAP 1.2规范,除了WTLS 和WBMP。 Java注册表操作类 jared jared是一个用来操作Windows注册表的 Java 类库,你可以用来对注册表信息进行读写。 GIF动画制作工具 GiftedMotion GiftedMotion是一个很小的,免费而且易于使用图像互换格式动画是能够设计一个有趣的动画了一系列的数字图像。使用简便和直截了当,用户只需要加载的图片和调整帧您想要的,如位置,时间显示和处理方法前帧。 Java的PList类库 Blister Blister是一个用于操作苹果二进制PList文件格式的Java开源类库(可用于发送数据给iOS应用程序)。 重复文件检查工具 FindDup.tar FindDup 是一个简单易用的工具,用来检查计算机上重复的文件。 OpenID的Java客户端 JOpenID JOpenID是一个轻量级的OpenID 2.0 Java客户端,仅50KB+(含源代码),允许任何Web网站通过OpenID支持用户直接登录而无需注册,例如Google Account或Yahoo Account。 JActor的文件持久化组件 JFile JFile 是 JActor 的文件持久化组件,以及一个高吞吐量的可靠事务日志组件。 Google地图JSP标签库 利用Google:maps JSP标签库就能够在你的Web站点上实现GoogleMaps的所有功能而且不需要javascript或AJAX编程。它还能够与JSTL相结合生成数据库驱动的动态Maps。 OAuth 实现框架 Agorava Agorava 是一个实现了 OAuth 1.0a 和 OAuth 2.0 的框架,提供了简单的方式通过社交媒体进行身份认证的功能。 Eclipse的JavaScript插件 JSEditor JSEditor 是 Eclipse 下编辑 JavaScript 源码的插件,提供语法高亮以及一些通用的面向对象方法。 Java数据库连接池 BoneCP BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K
Hyperf v2.1.4更新日志修复 #3165 修复方法 HyperfDatabaseSchemaMySqlBuilder::getColumnListing 在 MySQL 8.0 版本中无法正常使用的问题。 #3174 修复 hyperf/database 组件中 where 语句因为不严谨的代码编写,导致被绑定参数会被恶意替换的问题。 #3179 修复 json-rpc 客户端因对端服务重启,导致接收数据一直异常的问题。 #3189 修复 kafka 在集群模式下无法正常使用的问题。 #3191 修复 json-rpc 客户端因对端服务重启,导致连接池中的连接全部失效,新的请求进来时,首次使用皆会报错的问题。 新增 #3170 为 hyperf/watcher 组件新增了更加友好的驱动器 FindNewerDriver,支持 Mac Linux 和 Docker。 #3195 为 JsonRpcPoolTransporter 新增了重试机制, 当连接、发包、收包失败时,默认重试 2 次,收包超时不进行重试。 优化 #3169 优化了 ErrorExceptionHandler 中与 set_error_handler 相关的入参代码, 解决静态检测因入参不匹配导致报错的问题。 #3191 优化了 hyperf/json-rpc 组件, 当连接中断后,会先尝试重连。 变更 #3174 严格检查 hyperf/database 组件中 where 语句绑定参数。 新组件孵化 DAG 轻量级有向无环图任务编排库。 RPN 逆波兰表示法。Hyperf简介Hyperf 是基于 Swoole 4.4+ 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 PHP-FPM 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 PSR 标准 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 可替换 与 可复用 的。框架组件库除了常见的协程版的 MySQL 客户端、Redis 客户端,还为您准备了协程版的 Eloquent ORM、WebSocket 服务端及客户端、JSON RPC 服务端及客户端、GRPC 服务端及客户端、OpenTracing(Zipkin, Jaeger) 客户端、Guzzle HTTP 客户端、Elasticsearch 客户端、Consul 客户端、ETCD 客户端、AMQP 组件、Nats 组件、Apollo、ETCD、Zookeeper 和阿里云 ACM 的配置中心、基于令牌桶算法的限流器、通用连接池、熔断器、Swagger 文档生成、Swoole Tracker、Blade、Smarty、Twig、Plates 和 ThinkTemplate 视图引擎、Snowflake 全局ID生成器、Prometheus 监控 等组件,省去了自己实现对应协程版本的麻烦。Hyperf 还提供了 基于 PSR-11 的依赖注入容器、注解、AOP 面向切面编程、基于 PSR-15 的中间件、自定义进程、基于 PSR-14 的事件管理器、Redis/RabbitMQ 消息队列、自动模型缓存、基于 PSR-16 的缓存、Crontab 秒级定时任务、Session、i18n 国际化、Validation 表单验证 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。Hyperf 功能特点框架初衷 尽管现在基于 PHP 语言开发的框架处于一个百花争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。设计理念 Hyperspeed + Flexibility = Hyperf,从名字上我们就将 超高速 和 灵活性 作为 Hyperf 的基因。对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 PSR 标准 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。 基于以上的特点,Hyperf 将存在丰富的可能性,如实现 单体 Web 服务,API 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。文档齐全 我们投入了大量的时间用于文档的建设以提供高质量的文档体验,以解决各种因为文档缺失所带来的问题,文档上也提供了大量的示例,对新手同样友好。 Hyperf 官方开发文档生产可用 我们为组

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值