RedKale 功能

      RedKale虽然只有1.xM大小,但是麻雀虽小五脏俱全。既可作为服务器使用,也可当工具包使用。作为独立的工具包提供以下功能:
            1、convert包提供JSON的序列化和反解析功能,类似Gson、Jackson。
            2、convert包提供Java对象二进制的序列化和反解析功能,类似Protobuf。
            3、source包提供很简便的数据库操作功能,类似JPA、Hibernate。
            4、net包提供TCP/UDP服务功能, 类似Mina。
            5、net.http提供HTTP服务, 类似Tomcat、Netty。
            6、ResourceFactory提供轻量级的依赖注入功能, 类似Google Guice。

RedKale 服务器

      RedKale作为服务器的目录如下:
            bin   : 存放启动关闭脚本(start.sh、shutdown.sh、start.bat、shutdown.bat)
            conf : 存放服务器所需配置文件:
                             application.xml: 服务配置文件 (必需);
                             logging.properties:日志配置文件 (可选);
                             persistence.xml:数据库配置文件 (可选);
            lib    : 存放服务所依赖的第三方包,redkale.jar 放在此处。
            logs : logging.properties 配置中默认的日志存放目录。
            root : application.xml 配置中HTTP服务所需页面的默认根目录。

      RedKale启动的流程如下:
            1、加载 application.xml 并解析。
            2、初始化 <resources> 节点中的资源。
            3、解析所有的 <server> 节点。
            4、初始化并启动所有<server> 节点的Server服务 (优先加载SNCP协议的Server)。
            5、初始化单个Server:
                        5.1、扫描classpath加载所有可用Service实现类并实例化,然后相互依赖注入。
                        5.2、Service实例在依赖注入过程中加载所需的DataSource、CacheSource资源。
                        5.3、调用所有本地模式Service的init方法。
                        5.4、扫描classpath加载所有可用Servlet实现类并实例化 (优先实例化WebSocketServlet)。
                        5.5、给所有Servlet依赖注入所需的Service。
                        5.6、调用所有Servlet的init方法。
                        5.7、启动Server的服务监听。
            6、启动进程本身的监听服务。

RedKale 集中式与分布式

      通常一个系统会分为三层:接入层、业务层、数据层。对应到RedKale的组件是: Servlet、Service、Source。大部分系统提供的是HTTP服务,为了方便演示RedKale从集中式到分布式的变化,以一个简单的HTTP服务作为范例。
      开发一个极简单的小论坛系统。包含三个模块:
            用户模块    UserSerivice:     提供用户注册、登录、更新资料等功能, UserServlet作为接入层。
            帖子模块 ForumSerivice:     提供看帖、发帖、删帖等功能, ForumServlet作为接入层。
            通知模块  NotifySerivice:     提供用户操作、回帖等消息通知功能, NotifyWebSocket是WebSocket的Servlet, 且name为 ws_notify,作为接入层。
      其中数据源有:
               DataSource:    在persistence.xml里配置的数据库Source的name为demodb ,三个模块都需要使用demodb。
            CacheSource:    仅供UserSerivice用于存放session的缓存Service,name为 usersessions, 且session只存放用户ID( int 类型)。


      1、单点部署

      在早期用户量很少或者开发、调试环境中只需部署一个进程就可满足需求。

      如上图,所有模块的HttpServlet、Service与Source数据库操作全部署在一起。 application.xml作简单的配置即可:

<application port="5050">       
    <server protocol="HTTP" port="6060" root="root"> 
        <services autoload="true" />  
        <servlets autoload="true"/>
    </server>     	
</application>

      2、多点部署

      在生产环境需要避免单点问题,一个服务一般会部署多套。在此做个简单的容灾部署,最前端部署一个nginx作反向代理和负载均衡服务器,后面部署两套系统。

      如上图,两个进程间的Serivce都是本地模式,两者会通过SNCP服务保持数据同步,若DataSource开启了数据缓存也会自动同步。两套的配置文件相同,配置如下:

<application port="5050">   
    <resources>	
        <group name="ALL">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.120" port="7070"/>
        </group>
    </resources>

    <!-- HTTP 监听 Server -->
    <server protocol="HTTP" port="6060" root="root"> 
        <!-- 前端配置了nginx,需要配置才能获取客户端真实的IP地址 -->
        <request>
            <remoteaddr value="request.headers.X-RemoteAddress"/>
        </request>
        <services autoload="true" groups="ALL"/>	
        <servlets autoload="true" />
    </server> 

    <!-- SNCP 监听 Server -->
    <server protocol="SNCP" port="7070"> 
        <services autoload="true"  groups="ALL">
            <!-- 有WebSocketServlet的服务必须配置WebSocketNodeService,且RedKale同时会自动创建一个同名(ws_notify)的 CacheSource -->
            <service name="ws_notify" value="org.redkale.service.WebSocketNodeService"/>
            <!-- 存在DataSource必须配置DataSourceService -->
            <service name="demodb" value="org.redkale.service.DataSourceService"/>
            <!-- 存放用户HTTP session信息的CacheSource -->
            <service name="usersessions" value="org.redkale.service.CacheSourceService">
                <property name="key-type" value="java.lang.String"/>
                <property name="value-type" value="java.lang.Integer"/>
            </service>
        </services>
    </server>	
</application>

      3、分层部署

      随着业务的复杂度增加,接入层与业务层混在一起会越来越难部署和维护,因此需要进行分层部署。

      如上图,对HttpServlet与Service进行了分离。每个接入层的Service都是远程模式,业务层只需提供SNCP供远程调用。

      接入层中每个进程的配置相同,配置如下:

<application port="5050">  
    <resources>	
        <group name="ALL">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.120" port="7070"/>
        </group>
    </resources>

    <!-- HTTP 监听 Server -->
    <server protocol="HTTP" port="6060" root="root"> 
        <!-- 前端配置了nginx,需要配置才能获取客户端真实的IP地址 -->
        <request>
            <remoteaddr value="request.headers.X-RemoteAddress"/>
        </request>
        <services autoload="true" groups="ALL">
            <!-- 有WebSocketServlet的服务必须配置WebSocketNodeService,且RedKale同时会自动创建一个同名(ws_notify)的 CacheSource -->
            <service name="ws_notify" value="org.redkale.service.WebSocketNodeService"/>
        </services>
        <servlets autoload="true" />
    </server> 	
</application>

      业务层中每个进程的配置相同,配置如下:

<application port="5050">  
    <resources>	
        <group name="ALL">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.120" port="7070"/>
        </group>
    </resources>

    <!-- SNCP 监听 Server -->
    <server protocol="SNCP" port="7070"> 
        <services autoload="true" groups="ALL">
            <!-- 有WebSocketServlet的服务必须配置WebSocketNodeService,且RedKale同时会自动创建一个同名(ws_notify)的 CacheSource -->
            <service name="ws_notify" value="org.redkale.service.WebSocketNodeService"/>
            <!-- 存在DataSource必须配置DataSourceService -->
            <service name="demodb" value="org.redkale.service.DataSourceService"/>
            <!-- 存放用户HTTP session信息的CacheSource -->
            <service name="usersessions" value="org.redkale.service.CacheSourceService">
                <property name="key-type" value="java.lang.String"/>
                <property name="value-type" value="java.lang.Integer"/>
            </service>
        </services>
    </server>	
</application>

      4、微服务部署

      当用户量和发帖量增加到上百万的时候,明显地将所有模块的服务部署到一个进程里是不行的。 因此需要将Service服务都独立部署形成微服务架构。

      如上图,将Serivice都独立部署并进行容灾部署,当然如果有需要,Servlet之间、Source都可以各自分离独立部署。不同类型的Service之间都是远程模式调用。

      接入层中每个进程的配置相同,配置如下:

<application port="5050">   
    <resources>	
        <group name="USER_SERVICE">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.111" port="7070"/>
        </group>
        <group name="NOTIFY_SERVICE">
            <node addr="192.168.50.120" port="7070"/>
            <node addr="192.168.50.121" port="7070"/>
        </group>
        <group name="FORUM_SERVICE">
            <node addr="192.168.50.130" port="7070"/>
            <node addr="192.168.50.131" port="7070"/>
        </group>
    </resources>

    <!-- HTTP 监听 Server -->
    <server protocol="HTTP" port="6060" root="root"> 
        <!-- 前端配置了nginx,需要配置才能获取客户端真实的IP地址 -->
        <request>
            <remoteaddr value="request.headers.X-RemoteAddress"/>
        </request>
        <services autoload="true">	
            <service value="org.redkale.demo.UserService" groups="USER_SERVICE"/>
            <service value="org.redkale.demo.NotifyService" groups="NOTIFY_SERVICE"/>
            <service value="org.redkale.demo.ForumService" groups="FORUM_SERVICE"/>
            <!-- 有WebSocketServlet的服务必须配置WebSocketNodeService,且RedKale同时会自动创建一个同名(ws_notify)的 CacheSource -->
            <service name="ws_notify" value="org.redkale.service.WebSocketNodeService" groups="NOTIFY_SERVICE"/>
        </services>
        <servlets autoload="true" />
    </server> 	
</application>

      用户模块UserService服务群中各个进程的配置相同,配置如下:

<application port="5050">  
    <resources>	
        <group name="USER_SERVICE">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.111" port="7070"/>
        </group>
        <group name="NOTIFY_SERVICE">
            <node addr="192.168.50.120" port="7070"/>
            <node addr="192.168.50.121" port="7070"/>
        </group>
        <group name="FORUM_SERVICE">
            <node addr="192.168.50.130" port="7070"/>
            <node addr="192.168.50.131" port="7070"/>
        </group>
    </resources>

    <!-- SNCP 监听 Server -->
    <server protocol="SNCP" port="7070"> 
        <services autoload="true">	
            <service value="org.redkale.demo.NotifyService" groups="NOTIFY_SERVICE"/>
            <service value="org.redkale.demo.ForumService" groups="FORUM_SERVICE"/>
            <!-- 存在DataSource必须配置DataSourceService -->
            <service name="demodb" value="org.redkale.service.DataSourceService" groups="USER_SERVICE"/>
            <!-- 存放用户HTTP session信息的CacheSource -->
            <service name="usersessions" value="org.redkale.service.CacheSourceService" groups="USER_SERVICE">
                <property name="key-type" value="java.lang.String"/>
                <property name="value-type" value="java.lang.Integer"/>
            </service>
        </services>
        <servlets autoload="true" />
    </server> 	
</application>

      通知模块NotifyService服务群中各个进程的配置相同,配置如下:

<application port="5050">  
    <resources>	
        <group name="USER_SERVICE">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.111" port="7070"/>
        </group>
        <group name="NOTIFY_SERVICE">
            <node addr="192.168.50.120" port="7070"/>
            <node addr="192.168.50.121" port="7070"/>
        </group>
        <group name="FORUM_SERVICE">
            <node addr="192.168.50.130" port="7070"/>
            <node addr="192.168.50.131" port="7070"/>
        </group>
    </resources>

    <!-- SNCP 监听 Server -->
    <server protocol="SNCP" port="7070"> 
        <services autoload="true">	
            <service value="org.redkale.demo.UserService" groups="USER_SERVICE"/>
            <service value="org.redkale.demo.ForumService" groups="FORUM_SERVICE"/>
            <!-- 有WebSocketServlet的服务必须配置WebSocketNodeService,且RedKale同时会自动创建一个同名(ws_notify)的 CacheSource -->
            <service name="ws_notify" value="org.redkale.service.WebSocketNodeService" groups="NOTIFY_SERVICE"/>
            <!-- 存在DataSource必须配置DataSourceService -->
            <service name="demodb" value="org.redkale.service.DataSourceService" groups="NOTIFY_SERVICE"/>
        </services>
        <servlets autoload="true" />
    </server> 	
</application>

      帖子模块ForumService服务群中各个进程的配置相同,配置如下:

<application port="5050">  
    <resources>	
        <group name="USER_SERVICE">
            <node addr="192.168.50.110" port="7070"/>
            <node addr="192.168.50.111" port="7070"/>
        </group>
        <group name="NOTIFY_SERVICE">
            <node addr="192.168.50.120" port="7070"/>
            <node addr="192.168.50.121" port="7070"/>
        </group>
        <group name="FORUM_SERVICE">
            <node addr="192.168.50.130" port="7070"/>
            <node addr="192.168.50.131" port="7070"/>
        </group>
    </resources>

    <!-- SNCP 监听 Server -->
    <server protocol="SNCP" port="7070"> 
        <services autoload="true">	
            <service value="org.redkale.demo.UserService" groups="USER_SERVICE"/>
            <service value="org.redkale.demo.NotifyService" groups="NOTIFY_SERVICE"/>
            <!-- 存在DataSource必须配置DataSourceService -->
            <service name="demodb" value="org.redkale.service.DataSourceService" groups="FORUM_SERVICE"/>
        </services>
        <servlets autoload="true" />
    </server> 	
</application>

      由这个范例可以看出,RedKale提供了非常强大的架构,集中式到微服务架构不需要增加修改一行代码即可随意切换,复杂的系统都可以如小系统般快速地开发出来。
      为了降低接入层与业务层代码的耦合, 可以将Service分接口与实现两个类,接入层只加载接口包、业务层使用实现包。

appplication.xml 配置说明

<?xml version="1.0" encoding="UTF-8"?>
<!-- 
    文件说明:
        ${APP_HOME} 指当前程序的根目录APP_HOME
        required: 被声明required的属性值不能为空
-->
<!--     
    address:  本地的IP地址, 默认值为默认网卡的ip,当不使用默认值需要指定值,如127.0.0.1
    port: required  程序的管理Server的端口,用于关闭或者与监管系统进行数据交互
    host:  程序的管理Server的地址; 默认为127.0.0.1。
    lib:  加上额外的lib路径,多个路径用分号;隔开; 默认为空。  例如: ${APP_HOME}/lib/a.jar;${APP_HOME}/lib2/b.jar;
-->
<application port="6560" lib="">   
    
    <!-- 所有服务所需的资源 -->      
    <resources>  
        <!--
            transport节点只能有一个,用于配置所有Transport的池参数,没配置该节点将自动创建一个。
            threads: 线程总数, 默认: <group>节点数*CPU核数*8
            bufferCapacity: ByteBuffer的初始化大小, 默认: 8K; 
            bufferPoolSize: ByteBuffer池的大小,默认: <group>节点数*CPU核数*8
        -->
        <transport capacity="8192" bufferPoolSize="32" threads="32"/>
        <!--
            一个组包含多个NODE, 同一Service服务可以由多个进程提供,这些进程称为一个GROUP,且同一GROUP内的进程必须在同一机房或局域网内
            一个group节点对应一个 Transport 对象。
            name: 服务组ID,长度不能超过11个字节. 默认为空字符串。 注意: name不能包含$符号。
            protocol:值只能是UDP TCP, 默认TCP
            注意: 一个node只能所属一个group。只要存在protocol=SNCP的Server节点信息, 就必须有group节点信息。
        -->
        <group name="" protocol="TCP">
            <!--
                需要将本地node的addr与port列在此处。
                同一个<node>节点值只能存在一个<group>节点内,即同一个addr+port只能属于一个group。
                addr: required IP地址
                port: required 端口
                clients: 连接池数, 默认: CPU核数*4
                buffers: ByteBuffer对象池的大小, 默认: CPU核数*8
            -->
            <node addr="127.0.0.1" port="7070"/>
        </group>
        <!-- 
            全局的参数配置, 可以通过@Resource(name="property.xxxxxx") 进行注入, 被注解的字段类型只能是String、primitive class
            如果name是system.property.开头的值将会在进程启动时进行System.setProperty("yyyy", "YYYYYY")操作。
            如果name是mimetype.property.开头的值将会在进程启动时进行MimeType.add("yyyy", "YYYYYY")操作。
            load:  加载文件,多个用;隔开。
            默认置入的system.property.的有:
               System.setProperty("convert.json.tiny", "true");
               System.setProperty("convert.bson.tiny", "true");
               System.setProperty("convert.json.pool.size", "128");
               System.setProperty("convert.bson.pool.size", "128");
               System.setProperty("convert.json.writer.buffer.defsize", "4096");
               System.setProperty("convert.bson.writer.buffer.defsize", "4096");
        -->
        <properties load="config.properties">
            <property name="system.property.yyyy" value="YYYYYY"/>
            <property name="xxxxxx" value="XXXXXXXX"/>
            <property name="xxxxxx" value="XXXXXXXX"/>
            <property name="xxxxxx" value="XXXXXXXX"/>
        </properties>
    </resources> 
    <!--
        protocol: required  server所启动的协议,有HTTP、SNCP, 目前只支持HTTP、SNCP。SNCP使用TCP实现; 
        host:  服务所占address , 默认: 0.0.0.0
        port:  required 服务所占端口 
        root:  如果是web类型服务,则包含页面  默认:{APP_HOME}/root
        lib: server额外的class目录, 默认为空        
        charset: 文本编码, 默认: UTF-8
        backlog:  默认10K
        threads: 线程总数, 默认: CPU核数*16
        maxbody: request.body最大值, 默认: 64K
        bufferCapacity: ByteBuffer的初始化大小, 默认: 8K;  如果是HTTP协议则默认: 16K + 8B (兼容HTTP 2.0)
        bufferPoolSize: ByteBuffer池的大小,默认: CPU核数*512
        responsePoolSize: Response池的大小,默认: CPU核数*256
        readTimeoutSecond: 读操作超时秒数, 默认0, 表示永久不超时
        writeTimeoutSecond:  写操作超时秒数, 默认0, 表示永久不超时        
    -->
    <server protocol="HTTP" host="127.0.0.1" port="6060" root="root" lib=""> 
        
        <!-- 
           加载所有的Service服务;
           在同一个进程中同一个name同一类型的Service将共用同一个实例
           autoload="true"  默认值. 自动加载以下目录(如果存在的话)下所有的Service类:
                                 server.lib;    server.lib/*;    server.classes;    
           autoload="false" 需要显著的指定Service类
           includes: 当autoload="true", 拉取类名与includes中的正则表达式匹配的类, 多个正则表达式用分号;隔开
           excludes: 当autoload="true", 排除类名与excludes中的正则表达式匹配的类, 多个正则表达式用分号;隔开           
           groups:   所属组的节点,多个节点值用;隔开,如果配置文件中存在多个SNCP协议的Server节点,需要显式指定group属性.
                    当 protocol == SNCP 时 group表示当前Server与哪些节点组关联。
                    当 protocol != SNCP 时 group只能是空或者一个group的节点值,不能为多个节点值。
        -->
        <services autoload="true" includes="" excludes="">

            <!-- 显著加载指定的Service的接口类 -->
            <service value="com.xxx.XXX1Service"/>
            <!-- 
               name:   显式指定name,覆盖默认的空字符串值。 注意: name不能包含$符号。
               groups: 显式指定groups,覆盖<services>节点的groups默认值。
            -->
            <service value="com.xxx.XXX2Service" name="" groups="xxx;yyy"/>
            <!--   给Service增加配置属性 -->
            <service value="com.xxx.XXX1Service">
                <property name="xxxxxx" value="XXXXXXXX"/>
                <property name="xxxxxx" value="XXXXXXXX"/>
            </service>
        </services>
        
        <!--
           当Server为HTTP协议时, request节点才有效。
           remoteaddr 节点: 替换请求方节点的IP地址, 通常请求方是由nginx等web静态服务器转发过的则需要配置该节点。
           且value值只能是以request.headers.开头,表示从request.headers中获取对应的header值。
           例如下面例子获取request.getRemoteAddr()值,如果header存在X-RemoteAddress值则返回X-RemoteAddress值,不存在返回getRemoteAddress()。
        -->
        <request>
            <remoteaddr value="request.headers.X-RemoteAddress"/>
        </request>
        
        <!--
           当Server为HTTP协议时, response节点才有效。
           defcookie 节点: 当response里输出的cookie没有指定domain 和path时,使用该节点的默认值。
           如果addheader、setheader 的value值以request.headers.开头则表示从request.headers中获取对应的header值
           例如下面例子是在Response输出header时添加两个header(一个addHeader, 一个setHeader)。
        -->
        <response>
            <defcookie domain="" path=""/>
            <addheader name="Access-Control-Allow-Origin" value="request.headers.Origin" /> 
            <setheader name="Access-Control-Allow-Credentials" value="true"/> 
        </response>
            
        <!-- 
           加载所有的Servlet服务;
           path:  servlet的ContextPath前缀 默认为空
           autoload="true"  默认值. 自动加载以下目录(如果存在的话)下所有的Servlet类:
                                 ${APP_HOME}/lib;    ${APP_HOME}/root/lib/*;    ${APP_HOME}/root/classes;   
           autoload="false" 需要显著的指定Service类
           includes: 当autoload="true", 拉取类名与includes中的正则表达式匹配的类, 多个正则表达式用分号;隔开
           excludes: 当autoload="true", 排除类名与excludes中的正则表达式匹配的类, 多个正则表达式用分号;隔开
        -->
        <servlets path="/pipes" autoload="true" includes="" excludes="">
            <!-- 
               当Server为HTTP协议时,ResourceServlet才有效. 默认存在一个有默认属性的resource-servlet节点
               webroot: web资源的根目录, 默认取server节点中的root值
            -->
            <resource-servlet webroot="root">
                <!--
                    资源缓存的配置, 默认存在一个含默认属性的caches节点
                    limit:  资源缓存最大容量, 默认: 128M, 为0表示不缓存, 单位可以是B、K、M、G,不区分大小写
                    lengthmax: 可缓存的文件大小上限, 默认: 1M(超过1M的文件不会被缓存)
                -->
                <caches  limit="128M" lengthmax="1M" />
                <!--
                   支持类似nginx中的rewrite, 目前只支持静态资源对静态资源的跳转。
                   type: 匹配的类型, 目前只支持location(匹配requestURI), 默认: location
                   match: 匹配的正则表达式
                   forward: 需跳转后的资源链接
                   例如下面例子是将/xxx-yyy.html的页面全部跳转到/xxx.html
                -->
                <rewrite type="location" match="^/([^-]+)-[^-\.]+\.html(.*)" forward="/$1.html"/>
            </resource-servlet>
            <!-- 显著加载指定的Servlet -->
            <servlet value="com.xxx.XXX1Servlet" />
            <servlet value="com.xxx.XXX2Servlet" />
            <servlet value="com.xxx.XXX3Servlet" >
                <property name="xxxxxx" value="XXXXXXXX"/>
                <property name="yyyyyy" value="YYYYYYYY"/>
            </servlet>
        </servlets>
    </server>
    
    <server protocol="SNCP" host="127.0.0.1" port="7070" root="root" lib=""> 
        <!-- 参数完全同上 -->
        <services autoload="true" includes="" excludes="" />
    </server>
</application>