一个接口触达多平台(包括微信公众号、企业微信、钉钉、邮箱等任何想的到平台,都能一个接口一次推送;极简的代码调用,极大减少业务方消息推送的代码量);
同时提供基于 netty 和 websocket 的即时通讯实现,实现单聊、群聊等功能。开箱即用,采用 SpringCloud 微服务架构,扩展简单且没有单点问题。致力于包揽所有和消息推送有关的技术开发工作,节省开发资源。
- 一个接口触达多平台,支持一个接口多平台同时发送
- 消息平台逻辑与业务逻辑的解耦,业务方不需要关心各个平台的对接实现,只需要关心:要用哪些平台发、要发给对应平台的哪些人、要发什么内容
- 极强的扩展性,要新增一个消息平台的支持,理论只需要新增几个类就能完成,且不需要写任何前端代码即可获得该平台对应的 ui 交互(包括:配置交互、接收人维护、web 手动消息发送交互等)。
- 当然支持 web 端手动发送消息
- 当然也支持定时任务
- 消息方案预设置
- 提供即时通讯实现,且支持服务器横向扩展
- 接收人导入
- 接收人按分组划分
- 消息日志
- ...
在线体验
http://159.75.121.163/
admin admin目前支持的消息类型
- 邮箱
- 企业微信-应用消息
- 文本消息
- 图片消息
- 视频消息
- 文本消息
- 文本卡片消息
- 图文消息
- Markdown 消息
- 企业微信-群机器人
- 文本消息
- 图片消息
- 图文消息
- Markdown 消息
- 微信公众号
- 文本消息
- 图文消息
- 模板消息
- 钉钉-工作通知
- 文本
- Markdown
- 链接消息
- 卡片消息
- OA 消息
- 钉钉-群机器人
- 文本
- Markdown
- 链接消息
- 卡片消息
- FeedCard
Rpush 的架构决定了扩展一个消息平台的消息类型会非常简单,所以如果要扩展一个消息平台,大部分时间都会花在查找该平台的对接文档上。后续会在工作之余加上其它的平台或消息类型。当然,欢迎参与扩展(扩展一个消息平台的消息类型,只需要几个 java 类即可,不需要写任何前端代码,即可获得包括 ui 交互内的所有功能)。
效果展示
单个消息类型发送示例
web 端多平台发送示例
postman 多平台发送示例
用代码发消息
秉持”业务服务只负责发消息“的解耦原则,业务服务在需要发消息的时候,代码应该越简单越好。所以,Rpush 的发消息的 sdk,一种消息只需要一行代码,有几种消息就有几行代码。比如这样:
/** * @author shuangmulin * @since 2021/6/8/008 11:37 **/ public class RpushSenderTest { /** * 要发送的内容 */ public static final String content = "您的会议室已经预定 \n" + ">**事项详情** \n" + ">事 项:开会\n" + ">组织者:@miglioguan \n" + ">参与者:@miglioguan、@kunliu、@jamdeezhou、@kanexiong、@kisonwang \n" + "> \n" + ">会议室:广州TIT 1楼 301\n" + ">日 期:2021年5月18日\n" + ">时 间:上午9:00-11:00\n" + "> \n" + ">请准时参加会议。 \n" + "> \n" + ">如需修改会议信息,请点击:[修改会议信息](https://work.weixin.qq.com)"; public static void main(String[] args) { // 企业微信-markdown消息 MarkdownMessageDTO markdown = RpushMessage.WECHAT_WORK_AGENT_MARKDOWN().content(content).receiverIds(Collections.singletonList("ZhongBaoLin")).build(); // 企业微信-群机器人消息 TextMessageDTO text = RpushMessage.WECHAT_WORK_ROBOT_TEXT().content(content).receiverIds(Collections.singletonList("ZhongBaoLin")).build(); // 邮箱 EmailMessageDTO email = RpushMessage.EMAIL().title("会议通知").content(content).build(); RpushService.instance("baolin", "666666").sendMessage(markdown, text, email); // 填上账号密码,运行即可 } }
以上代码,一次发送了三种不同平台的不同消息类型,全部代码加起来也只需要四五行代码而已。要获得以上效果,只需要 maven 引用 rpush 的 sdk 模块即可:
<project> <!-- 设置 jitpack.io 仓库 --> <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> <dependencies> <!-- 添加rpush-sdk依赖 --> <dependency> <groupId>com.github.shuangmulin.rpush</groupId> <artifactId>rpush-sdk</artifactId> <version>v1.0.2</version> </dependency> </dependencies> </project>
即时通讯
Rpush 对即时通讯的实现方式比较
包容
,即对具体的连接实现做了解耦,不局限于某一种连接方式,可以 netty,可以 websocket,可以 comet,当然也可以用原始的 bio 来做。这里展示 websocke 的网页端和 netty 实现的命令行客户端之间互相单聊和群聊的效果(该示例的相关代码:客户端示例代码地址):
一些比较核心的扩展点
1. 可自由扩展的消息平台和消息类型
在 Rpush 的设计里,消息被归类为“消息平台”和“消息类型”,分别对应如下两个枚举:
/** * 消息平台枚举 **/ public enum MessagePlatformEnum { EMAIL(EmailConfig.class, "邮箱", "", "^[_a-z0-9-]+(\\.[_a-z0-9-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,})$", true), WECHAT_WORK_AGENT(WechatWorkAgentConfig.class, "企业微信-应用消息", "", "", true), WECHAT_WORK_ROBOT(WechatWorkRobotConfig.class, "企业微信-群机器人", "", "", true), WECHAT_OFFICIAL_ACCOUNT(WechatOfficialAccountConfig.class, "微信公众号", "", "", true), DING_TALK_CORP(DingTalkCorpConfig.class, "钉钉-工作通知", "", "", true), RPUSH_SERVER(EmptyConfig.class, "rpush服务", "", "", true); } /** * 消息类型枚举 **/ public enum MessageType { EMAIL("普通邮件 ", MessagePlatformEnum.EMAIL), RPUSH_SERVER("文本", MessagePlatformEnum.RPUSH_SERVER), // ================================企业微信-应用==================================== WECHAT_WORK_AGENT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_IMAGE("图片", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_VIDEO("视频", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_FILE("文件", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_TEXTCARD("文本卡片", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_NEWS("图文消息", MessagePlatformEnum.WECHAT_WORK_AGENT), WECHAT_WORK_AGENT_MARKDOWN("Markdown", MessagePlatformEnum.WECHAT_WORK_AGENT), // ================================企业微信-群机器人==================================== WECHAT_WORK_ROBOT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_ROBOT), WECHAT_WORK_ROBOT_IMAGE("图片", MessagePlatformEnum.WECHAT_WORK_ROBOT), WECHAT_WORK_ROBOT_NEWS("图文消息", MessagePlatformEnum.WECHAT_WORK_ROBOT), WECHAT_WORK_ROBOT_MARKDOWN("Markdown", MessagePlatformEnum.WECHAT_WORK_ROBOT), // ================================微信公众号==================================== WECHAT_OFFICIAL_ACCOUNT_TEXT("文本", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT), WECHAT_OFFICIAL_ACCOUNT_NEWS("图文消息", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT), WECHAT_OFFICIAL_ACCOUNT_TEMPLATE("模板消息", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT), // ================================钉钉-工作通知==================================== DING_TALK_COPR_TEXT("文本", MessagePlatformEnum.DING_TALK_CORP), DING_TALK_COPR_MARKDOWN("Markdown", MessagePlatformEnum.DING_TALK_CORP), DING_TALK_COPR_LINK("链接消息", MessagePlatformEnum.DING_TALK_CORP), DING_TALK_COPR_ACTION_CARD_SINGLE("卡片-单按钮", MessagePlatformEnum.DING_TALK_CORP), DING_TALK_COPR_ACTION_CARD_MULTI("卡片-多按钮", MessagePlatformEnum.DING_TALK_CORP), DING_TALK_COPR_OA("OA消息", MessagePlatformEnum.DING_TALK_CORP), ; }
这里拿“企业微信-应用的文本类型”的消息举例。假设现在要在 Rpush 实现这个类型的消息,步骤如下:
- 定义企业微信的配置类,如下:
/** * 企业微信配置 **/ @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor @Builder public class WechatWorkAgentConfig extends Config { private static final long serialVersionUID = -9206902816158196669L; @ConfigValue(value = "企业ID", description = "在此页面查看:https://work.weixin.qq.com/wework_admin/frame#profile") private String corpId; @ConfigValue(value = "应用Secret") private String secret; @ConfigValue(value = "应用agentId") private Integer agentId; }
里面的字段就按对应平台需要的字段去定义就行,比如这里的企业微信就只有三个字段需要配置。而每个字段上的
@ConfigValue
注解,是用来自动生成页面的,也就是说,只需要打上这个注解,就可以自动在页面上生成对应的增删改查的界面和交互(无需写一行前端代码)。
- 在
MessagePlatformEnum
和MessageType
里加上对应的枚举,即WECHAT_WORK_AGENT(WechatWorkAgentConfig.class, "企业微信-应用消息", "", "", true)
和WECHAT_WORK_AGENT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_AGENT),
。这里要注意下平台枚举的第一个参数就是第一步定义的配置类的 Class。- 定义企业微信-应用-文本消息的参数,如下:
/** * 企业微信消息发送DTO **/ @EqualsAndHashCode(callSuper = true) @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class TextMessageDTO extends BaseMessage { private static final long serialVersionUID = -3289428483627765265L; /** * 接收人分组列表 */ @SchemeValue(type = SchemeValueType.RECEIVER_GROUP) private List<Long> receiverGroupIds; /** * 接收人列表 */ @SchemeValue(type = SchemeValueType.RECEIVER) private List<String> receiverIds; @SchemeValue(description = "PartyID列表,非必填,多个接受者用‘|’分隔。当touser为@all时忽略本参数") private String toParty; @SchemeValue(description = "TagID列表,非必填,多个接受者用‘|’分隔。当touser为@all时忽略本参数") private String toTag; @SchemeValue(type = SchemeValueType.TEXTAREA, description = "请输入内容...") private String content; }
同样的,里面的字段根据该消息类型需要的字段去定义就行。比如企业微信-应用-文本消息就只需要一个
content
内容字段以及接收人相关的字段。这里涉及到的@SchemeValue
注解,同样也是用来自动生成页面交互的,即只需要打上这个注解,就能自动在发消息页面生成对应的 ui 和交互。同时可以使用com.regent.rpush.route.utils.sdk.SdkGenerator
类自动生成 sdk 代码。
- 实现
com.regent.rpush.route.handler.MessageHandler
接口,正式写发消息的代码。/** * 企业微信文本消息handler **/ @Component public class AgentTextMessageHandler extends MessageHandler<TextMessageDTO> { @Override public MessageType messageType() { return MessageType.WECHAT_WORK_AGENT_TEXT; } @Override public void handle(TextMessageDTO param) { // 具体的发消息代码 } }
这里有以下需要关心的点:
- 接口上的泛型填第 3 步定义的类
- 实现
messageType
方法,返回当前类要处理的消息类型- 实现
handle
方法,写发消息的代码,里面的参数是自动解析到这个方法的,直接使用即可到这里,就不需要多做任何其它的事了。也就是说,做完以上四个步骤,就已经完成了一个消息类型的扩展。事做的少,获得的功能并不少:
- 自动获得对应平台配置的增删改交互和 ui
- 自动获得对应平台接收人和接收人分组的增删改交互和 ui(包括导入功能)
- 自动获得该消息类型手动发送消息的交互和 ui
- 执行
com.regent.rpush.route.utils.sdk.SdkGenerator
,自动完成该消息类型的 sdk 代码- 自动获得该消息类型的定时任务的增删改交互和 ui而且增加一个消息类型,不会对业务服务之前正在使用的消息类型有任何影响,是纯粹的叠加”能力“。
2. 可自由扩展的即时通讯实现
在 rpush 的架构里,投递一个消息的流程大致可以概括为:调用统一的接口向路由服务投递消息-》路由服务查出消息目标所在的服务器地址-》路由服务向对应的服务器传递消息-》对应的服务找到对应的会话发送消息。
这里的扩展点在最后一步,即用户和服务器的会话维护。要实现服务端向客户端推送消息,会有比较多的解决方案,比如用 netty 起一个 nio 服务器,客户端去连 netty 服务器或者服务端用 socketio 提供 websocket 实现,客户端按 websocket 的方式连服务器或者用 comet 实现长连接让客户端连等等。这里做成扩展点的一个比较重要的考虑点就是要实现不同端之间的消息通信,比如上面例子里的命令行和网页之间的聊天,或者实现移动端和网页之间的聊天。
RpushClient
不管是什么技术实现的服务端推送,都会有一个“客户端”性质的类,比如 netty 会有
Channel
,socketio 提供的 websocket 会有SocketIOClient
。而对于 rpush 来说,只关心它们的一个共有的能力:消息投递。即RpushClient
接口:/** * 客户端 **/ public interface RpushClient { /** * 推送消息 */ void pushMessage(NormalMessageDTO message); void close(); }
只要实现了这个接口的类,不管是什么技术的实现,都被认为是 rpush 的客户端。也就是说,netty 也好,websocket 也好,只要提供给 rpush 这个接口的能力即可,从而达到解耦具体实现的目的。
目前 rpush 已经做了 netty 和 socketio 两个实现,分别对应com.regent.rpush.server.socket.nio.NioSocketChannelClient
和com.regent.rpush.server.socket.websocket.WebSocketClient
两个类。netty 客户端 sdk
rpush 提供了 netty 对应的客户端的 sdk,项目依赖
rpush-client
即可,使用也非常简单,只需要几行代码即可。public class Main { public static void main(String[] args) { RpushClient rpushClient = new RpushClient(servicePath, registrationId); // 填上rpush服务地址和id rpushClient.addMsgProcessor(new PingIgnoreMsgProcessor()); // 忽略心跳消息 rpushClient.start(); // 向服务端发起连接 rpushClient.addMsgProcessor(msg -> { // 处理接收到的消息 return false; }); } }
关于架构
rpush 目前主要提供两大功能,一个是消息分发,另一个是即时通讯功能。消息分发由路由服务
rpush-route
提供,即时通讯的长连接维护由 socket 服务rpush-server
服务提供。1. 可自由集群的路由服务
为了保证消息投递的统一性以及解耦消息分发和即时通讯之间的关系,路由服务只做一件事,即
负责将消息分发到各个平台
,也就是说 rpush 提供的即时通讯功能,对路由服务来说和其他第三方平台没什么区别,都被视为一个平台
。
所以在架构层面,如果只需要用到消息分发的功能,就不需要部署rpush-server
服务,只需要部署 eureka、zuul 和路由服务即可。zuul 作为系统对外的入口,隔离掉了路由服务器和用户端,同时路由服务又是无状态的,这样就使得路由服务可以根据实际业务情况自由集群,即想加一台路由服务就加,想减一台就减。2. 可自由集群的 socket 服务
rpush-server
作为 socket 服务,主要功能就是维护客户端的长连接。这个服务的承载能力直接决定了即时通讯功能一次可以在线多少用户,所以这个服务毫无疑问必须要是可集群部署的。
假设部署 5 台 socket 服务,都正常配置 eureka 为注册中心。 为了实现 socket 服务的集群,一个客户端连接 rpush 服务的流程为:
- 客户端问路由服务要一个可用的 socket 服务器 ip 和端口
- 路由服务通过合适的负载均衡算法得到一个可用的 socket 服务器 ip 和端口并返回给客户端
- 客户端向拿到的 socket 服务发起长连接
- 连接成功后,对应的 socket 服务器维护服务级别的 session 信息,然后向路由服务汇报该客户端,路由服务保存该客户端和 socket 服务的对应关系
- 客户端与对应的 socket 服务保持一定频率的心跳,并在心跳失败判定连接断开后重新发起以上流程,直到再次连接成功
客户端基于以上步骤上线之后,其他客户端向该客户端投递消息的流程为:
- 客户端请求路由服务提供的消息投递接口(这个就是前面说到的消息投递接口,因为 socket 服务维护的长连接对路由服务来说和其他第三方平台没什么区别,所以消息投递的方式也是一样的)
- 路由服务实现的 socket 服务消息处理器(
com.regent.rpush.route.handler.RpushMessageHandler
),根据消息上的目标客户端 id 找到对应的 socket 服务,并向该服务投递消息- socket 服务从自己维护的 session 里找到目标客户端,最终完成消息投递
实现以上流程之后,socket 服务就可以做到自由集群了。
上面说的流程偏理论化,有几个技术实现点这里做一下详细说明:
- 路由服务如何通过合适的负载均衡算法得到一个可用的 socket 服务器 ip 和端口?
实现的手段其实非常的简单暴力。首先由 socket 服务提供一个查询本机 ip 和端口的接口,路由服务直接通过 ribbon 去请求这个接口,然后自定义一个负载均衡规则类,来实现 socket 服务的选择:
/** * 路由->Socket服务端请求的实例选择 */ public class ServerBalancer extends ZoneAvoidanceRule { @Override public Server choose(Object o) { // ... // 用默认的负载均衡算法选出一个可用的socket服务(这里的算法可以根据实际业务更改) return super.choose(o); // ... } }
在配置文件里配置这个“规则类”:
rpush-server: ribbon: NFLoadBalancerRuleClassName: com.regent.rpush.route.loadbalancer.ServerBalancer
路由服务在向 socket 服务请求的时候会”经过“这个”规则类“,然后由这个“规则类”来选出一个可用的 socket 服务。最终 socket 服务的端口和 ip 信息,也是由选中的 socket 服务通过这次请求返回给路由服务的。
当然这个规则类不是只做这一件事,还有一个问题也需要这个类来完成。
- 消息投递的时候,路由服务如何根据消息上的目标客户端 id 找到对应的 socket 服务?
首先,在客户端与某一个 socket 服务连接成之后,客户端与 socket 服务之间的关系需要保存起来(mysql 或 redis)。然后新增一个 feign 的请求拦截器(
com.regent.rpush.route.loadbalancer.MessageRequestInterceptor
):@Component public class MessageRequestInterceptor implements RequestInterceptor { /** * 存放本次消息投递的目标socket服务id */ static final ThreadLocal<String> SERVER_ID = new ThreadLocal<>(); @Autowired private IRpushServerOnlineService rpushServerOnlineService; @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") @Override public void apply(RequestTemplate requestTemplate) { String url = requestTemplate.url(); String method = requestTemplate.method(); if (!"/push".equals(url) || !"POST".equals(method)) { // 只处理消息投递接口 return; } // 如果是消息推送,需要给接收端连接的服务端投放消息,在服务端集群的情况下,要找到对应的服务端 String body = new String(requestTemplate.body()); JSONObject jsonObject = new JSONObject(body); String sendTo = jsonObject.getStr("sendTo"); // 拿到目标客户端的id String serverId = ""; // 从redis或mysql查到该客户端对应的socket服务id SERVER_ID.set(serverId); // 添加到当前线程里 } }
这个“拦截类”配合上面的”规则类“,就能在路由服务向 socket 服务传递消息时准确的找到对应的 socket 服务。 完整的”规则类“:
/** * 路由->Socket服务端请求的实例选择 */ public class ServerBalancer extends ZoneAvoidanceRule { @Override public Server choose(Object o) { try { // 从拦截类里看有没有指定服务端实例 String serverId = MessageRequestInterceptor.SERVER_ID.get(); if (StringUtils.isEmpty(serverId)) { // 如果没有指定服务端实例,用默认的负载均衡算法 return super.choose(o); } // 如果指定了服务端实例,说明是消息传递,用指定好的实例向socket服务发请求 List<Server> servers = getLoadBalancer().getAllServers(); for (Server server : servers) { if (StringUtils.equals(server.getId(), serverId)) { return server; } } throw new IllegalArgumentException("没有可用的RPUSH_SERVER实例"); } finally { MessageRequestInterceptor.SERVER_ID.remove(); } } }
而且有了这两个类,路由服务向 socket 服务传递消息的代码也会非常的”干净“:
@Component public class RpushMessageHandler extends MessageHandler<RpushMessageDTO> { // ... @Override public void handle(RpushMessageDTO param) { List<String> sendTos = param.getReceiverIds(); for (String sendTo : sendTos) { // ... messagePushService.push(build); // 路由服务直接调用接口请求即可,”规则类“和”拦截类“屏蔽掉了其它逻辑,所以这里不需要关心会不会发给错误socket服务 } } }
3. 其它
- 队列。路由服务内部用
Disruptor
环形队列做了异步处理,尽可能地让消息推送接口更快地返回。如果是并发量较高的情况,可以加入 kafka,路由服务直接监听 kafka 的消息,以此来提升服务整体性能。- 缓存。客户端的上线信息可根据情况做多级缓存。即路由服务内部缓存 +redis 缓存,当然加的缓存越多,缓存一致性的问题就越复杂,需要考虑的情况也会更多。redis 也是需要根据实际情况来决定是否要集群部署。
- 监控。可使用 Spring Boot Admin 做服务状态监控。
用 docker-compose 快速部署一个 Rpush 服务
version: '2' services: nginx: image: nginx container_name: nginx ports: - 80:80 volumes: - /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf - /data/nginx/log:/var/log/nginx - /data/nginx/html:/usr/share/nginx/html rpush-eureka: image: shuangmulin/rpush-eureka container_name: rpush-eureka ports: - 8761:8761 rpush-zuul: image: shuangmulin/rpush-zuul environment: - eureka-service-ip=172.16.0.11 - eureka-service-port=8761 container_name: rpush-zuul ports: - 8124:8124 rpush-route: image: shuangmulin/rpush-route environment: - eureka-service-ip=localhost - eureka-service-port=8761 - jdbc.url=jdbc:mysql://localhost:3306/rpush?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 - jdbc.username=root - jdbc.password=123456 - super-admin.username=superadmin - super-admin.password=superadmin - jwtSigningKey=fjksadjfklds container_name: rpush-route ports: - 8121:8121 rpush-server: image: shuangmulin/rpush-server environment: - eureka-service-ip=localhost - eureka-service-port=8761 container_name: rpush-server ports: - 8122:8122 rpush-scheduler: image: shuangmulin/rpush-scheduler environment: - eureka-service-ip=localhost - eureka-service-port=8761 - jdbc.url=jdbc:mysql://localhost:3306/rpush?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 - jdbc.username=root - jdbc.password=123456 - super-admin.username=superadmin - super-admin.password=superadmin - jwtSigningKey=fasdferear container_name: rpush-scheduler ports: - 8123:8123
运行 docker-compose up -d 之后,直接访问 8124 端口即可
近期热议
推荐标签 标签
-
Firefox
8 引用 • 30 回帖 • 410 关注
Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。
-
Log4j
20 引用 • 18 回帖 • 29 关注
Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。
-
互联网
98 引用 • 344 回帖
互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。
-
HHKB
5 引用 • 74 回帖 • 478 关注
HHKB 是富士通的 Happy Hacking 系列电容键盘。电容键盘即无接点静电电容式键盘(Capacitive Keyboard)。
-
强迫症
15 引用 • 161 回帖 • 3 关注
强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。
-
思源笔记
23020 引用 • 92599 回帖
思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。
融合块、大纲和双向链接,重构你的思维。
-
微软
8 引用 • 44 回帖
微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。
-
Openfire
6 引用 • 7 回帖 • 101 关注
Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。
-
danl
• 146 关注
-
电影
121 引用 • 604 回帖 • 1 关注
这是一个不能说的秘密。
-
Java
3190 引用 • 8214 回帖 • 1 关注
Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。
-
Vim
29 引用 • 66 回帖 • 2 关注
Vim 是类 UNIX 系统文本编辑器 Vi 的加强版本,加入了更多特性来帮助编辑源代码。Vim 的部分增强功能包括文件比较(vimdiff)、语法高亮、全面的帮助系统、本地脚本(Vimscript)和便于选择的可视化模式。
-
Latke
71 引用 • 535 回帖 • 789 关注
Latke 是一款以 JSON 为主的 Java Web 框架。
-
Shell
123 引用 • 74 回帖 • 2 关注
Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。
-
书籍
78 引用 • 391 回帖
宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”
-
OAuth
36 引用 • 103 回帖 • 17 关注
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。
-
Swagger
26 引用 • 35 回帖 • 5 关注
Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。
-
Kafka
36 引用 • 35 回帖
Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。
-
TensorFlow
20 引用 • 19 回帖 • 1 关注
TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。
-
golang
497 引用 • 1388 回帖 • 278 关注
Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。
-
宕机
13 引用 • 82 回帖 • 60 关注
宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。
-
Docker
492 引用 • 926 回帖
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。
-
Hibernate
39 引用 • 103 回帖 • 715 关注
Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。
-
SMTP
4 引用 • 18 回帖 • 623 关注
SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。
-
Pipe
132 引用 • 1114 回帖 • 125 关注
Pipe 是一款小而美的开源博客平台。Pipe 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。
这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!
-
VirtualBox
10 引用 • 2 回帖 • 1 关注
VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。
-
RabbitMQ
49 引用 • 60 回帖 • 361 关注
RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于