Tars源码解析一

Tars源码解析(一)

背景

接手的服务治理底层是依赖tars的,不过只是对tars做了二次开发,所以需要对tars的源码做一定的了解
tars的版本现在已经到v1.5版本
此分析是用的v1.03版本
具体版本是

https://github.com/Tencent/Tars/tree/508de1d5b664106e9b435eda0c478a9427128edb

Server端

阅读这类源码首先得理解他的配置功能,我们首先来看下Server端的配置

配置

  1. 暴露服务的配置(以XML为例 servants.xml)

<servants>
    <servant name="HelloObj" >
        <home-api>com.qq.tars.quickstart.server.testapp.HelloServanthome-api>
        <home-class>com.qq.tars.quickstart.server.testapp.impl.HelloServantImplhome-class>
    servant>
servants>

其实例子里给的属性不全,还有,属性
这个配置主要配置暴露出去的服务

  1. 整个服务的配置(ServerConfig)
<tars>
  <application>
    enableset=n
    setdivision=NULL
    <server>
       #本地node的ip:port
       node=tars.tarsnode.ServerObj@tcp -h x.x.x.x -p 19386 -t 60000
       #应用名称
       app=TestApp
       #服务名称
       server=HelloServer
       #本机ip
       localip=x.x.x.x
       #管理端口
       local=tcp -h 127.0.0.1 -p 20001 -t 3000
       #服务可执行文件,配置文件等
       basepath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/bin/
       #服务数据目录
       datapath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/data/
       #日志路径
       logpath=/usr/local/app/tars/app_log/
       #配置中心的地址
       config=tars.tarsconfig.ConfigObj
       #通知上报的地址[可选]
       notify=tars.tarsnotify.NotifyObj
       #远程日志的地址[可选]
       log=tars.tarslog.LogObj
       #服务停止的超时时间
       deactivating-timeout=2000
       #日志等级
       logLevel=DEBUG
        #配置绑定端口
       <TestApp.HelloServer.HelloObjAdapter>
            #允许的IP地址
            allow
            #监听IP地址
            endpoint=tcp -h x.x.x.x -p 20001 -t 60000
            #处理组
            handlegroup=TestApp.HelloServer.HelloObjAdapter
            #最大连接数
            maxconns=200000
            #协议
            protocol=tars
            #队列大小
            queuecap=10000
            #队列超时时间毫秒
            queuetimeout=60000
            #处理对象
            servant=TestApp.HelloServer.HelloObj
            #当前线程个数
            threads=5
       TestApp.HelloServer.HelloObjAdapter>
    server>
    <client>
       #主控的地址
       locator=tars.tarsregistry.QueryObj@tcp -h x.x.x.x -p 17890
       #同步超时时间
       sync-invoke-timeout=3000
       #异步超时时间
       async-invoke-timeout=5000
       #刷新ip列表的时间间隔
       refresh-endpoint-interval=60000
       #上报数据的时间间隔
       report-interval=60000
       #采样率
       sample-rate=100000
       #最大采样数
       max-sample-count=50
       #模版名称
       modulename=TestApp.HelloServer
    client>
  application>
tars>

其中大的配置项:

  • Application:应用名称,如果配置文件没有配置,默认为UNKNOWN;
  • ServerName:服务名称;
  • BasePath:基本路径,通常表示可执行文件的路径;
  • DataPath:数据文件路径,通常表示存在服务自己的数据;
  • LocalIp:本地ip,默认是本机非127.0.0.1的第一块网卡IP;
  • LogPath:日志文件路径,日志的写法请参考后续;
  • LogLevel:滚动log日志级别;
  • Local:服务可以有管理端口,可以通过管理端口发送命令给服务,该参数表示绑定的管理端口的地址,例如tcp -h 127.0.0.1 -p 8899,如果没有设置则没有管理端口;
  • Node:本地NODE地址,如果设置,则定时给NODE发送心跳,否则不发送心跳,通常只有发布到框架上面的服务才有该参数;
  • Log:日志中心地址,例如:tars.tarslog.LogObj@tcp –h .. –p …,如果没有配置,则不记录远程日志;
  • Config:配置中心地址,例如:tars.tarsconfig.ConfigObj@tcp –h … -p …,如果没有配置,则addConfig函数无效,无法从远程配置中心拉取配置;
  • Notify:信息上报中心地址,例如:tars.tarsnotify.NotifyObj@tcp –h … -p …,如果没有配置,则上报的信息直接丢弃;
  • SessionTimeOut:防空闲连接超时设置;
  • SessionCheckInterval:防空闲连接超时检查周期;
  • Adapter表示了绑定端口。服务新增一个绑定端口,则新建立一个Adapter,并设置相关的参数和处理对象则可以非常方便的完成对这个端口上的处理,通常用这个功能来完成在其他协议的支撑。
    对于TARS服务而言,在服务配置文件中增加adapter项,即可以完成增加一个Servant处理对象。

代码分析

组件构成

目录名称 功能
net 框架rpc网络库
core 框架rpc的实现
tools 框架工具的实现,maven插件等
examples 框架的示例代码
distributedContext 框架的分布式上下文
protobuf 框架的pb协议支持
spring 框架的spring支持

代码入口

package com.qq.tars.server.startup;

import com.qq.tars.server.core.Server;

public class Main {

    public static void main(String[] args) {
        new Server().startUp(args);
    }
}
public void startUp(String args[]) {
    try {
        initCommunicator();

        configLogger();

        startManagerService();

        startAppContainer(); <-----只关心这里

        startSessionManager();

        registerServerHook();

        System.out.println("[SERVER] server is ready...");
    } catch (Throwable ex) {
        System.out.println("[SERVER] failed to start server...");
        ex.printStackTrace();
        System.out.close();
        System.err.close();
        System.exit(-1);
    }
}

整个代码的核心在于startAppContainer 我们重点关注它

protected void startAppContainer() throws Exception {
    this.container = new AppContainer();
    ContainerManager.registerContainer(this.container);
    this.container.start();
}

我们看下AppContainer和他的接口Container
Container接口定义启动和关闭函数

public interface Container {

    public void start() throws Exception;----启动

    public void stop() throws Exception;-----关闭
}

ContainManger提供了全局的管理Container的功能,从代码可以看出来全局只会有一个Container

public final class ContainerManager {

    private static Container _container = null;

    public static void registerContainer(final Container container) {
        _container = container;
    }

    @SuppressWarnings("unchecked")
    public static  T getContainer(Class cointainerClazz) {
        return (T) _container;
    }
}

全局只有一个AppContainer,但是AppContainer里含有多个AppContext

public class AppContainer implements Container {

    AppContext defaultApp = null;

    private final ConcurrentHashMap contexts = new ConcurrentHashMap();

接下来看下APPContainer的start方法

public void start() throws Exception {
    loadApp();
    defaultApp = contexts.get("");
    System.out.println("[SERVER] The container started successfully.");
}

loadApp就是构建AppContext的过程,注意到前面分析的配置,我们配置的servants.xml就是为了生成AppContext,我们关注XmlAppcontext分支

private void loadApp() throws Exception {
    String root = ConfigurationManager.getInstance().getServerConfig().getBasePath();
    File path = new File(root);
    AppContext context = null;

    URL servantXML = getClass().getClassLoader().getResource("servants.xml");
    if (servantXML != null) {
        context = new XmlAppContext();
    } else if (getClass().getClassLoader().getResource("servants-spring.xml") != null){
        System.out.println("[SERVER] find servants-spring.xml, use Spring mode.");
        Class clazz = Class.forName("com.qq.tars.server.apps.SpringAppContext");
        context = (AppContext) clazz.newInstance();
    } else {
        System.out.println("[SERVER] servants profile does not exist, start failed.");
        throw new TarsException("servants profile does not exist");
    }

    contexts.put("", context);
}

看下设计逻辑
- interface AppContext
- abstract class BaseAppContext
- class XmlAppContext
- class SpringAppContext

public interface AppContext {

    public String getInitParameter(String name);

    public abstract String name();

    public void stop();

    public ServantHomeSkeleton getCapHomeSkeleton(String homeName);
}

BaseAppContext的成员变量

public abstract class BaseAppContext implements AppContext {
    boolean ready = true;

    ConcurrentHashMap skeletonMap = new ConcurrentHashMap();
    ConcurrentHashMap servantAdapterMap = new ConcurrentHashMap();

    HashMap contextParams = new HashMap();

    Set listeners = new HashSet(4);
  • contextParams—保存servants.xml里配置的属性
  • listeners—-保存servants.xml里配置的监听器(观察者模式,start完成调用外部方法)
  • servantAdapterMap—-保存homeName和对应的Adapter
  • skeletonMap—–保存homeName和servantHomeSkeleton
public XmlAppContext() {
    try {
        initFromConfigFile();
        injectAdminServant();
        initServants();
        appContextStarted();
        System.out.println("[SERVER] The application started successfully.  {appname=}");
    } catch (Exception ex) {
        ready = false;
        System.out.println("[SERVER] failed to start the applicaton. {appname=}");
    }
}

private void initFromConfigFile() throws Exception {
    XMLConfigFile cfg = new XMLConfigFile();
    cfg.parse(getClass().getClassLoader().getResource("servants.xml").openStream());
    XMLConfigElement root = cfg.getRootElement();
    ArrayList elements = root.getChildList();

    loadInitParams(root.getChildListByName("context-param"));<-----把xml里的的参数对写入contextParams

    loadAppContextListeners(elements);<----把xml里配置的监听器写入listeners

    loadAppServants(elements);<----处理xml配置的
}

这里servant从xml的配置转成内部的结构是重中之重

private void loadAppServants(ArrayList elements) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    for (XMLConfigElement element : elements) {
        if ("servant".equals(element.getName())) {
            try {
                ServantHomeSkeleton skeleton = loadServant(element);
                skeletonMap.put(skeleton.name(), skeleton);
                appServantStarted(skeleton);
            } catch (Exception e) {
                System.err.println("init a service failed:context=[]");
            }
        }
    }
}
private ServantHomeSkeleton loadServant(XMLConfigElement element) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
    String homeName = null, homeApiName = null, homeClassName = null, processorClazzName = null,
            codecClazzName = null;
    Class homeApiClazz = null;
    Class codecClazz = null;
    Class processorClazz = null;
    Object homeClassImpl = null;
    ServantHomeSkeleton skeleton = null;
    int maxLoadLimit = -1;

    ServerConfig serverCfg = ConfigurationManager.getInstance().getServerConfig();


    //从xml里获取信息
    homeName = element.getStringAttribute("name");
    if (StringUtils.isEmpty(homeName)) {
        throw new RuntimeException("servant name is null.");
    }
    homeName = String.format("%s.%s.%s", serverCfg.getApplication(), serverCfg.getServerName(), homeName);
    homeApiName = getChildNodeValue(element, "home-api");
    homeClassName = getChildNodeValue(element, "home-class");
    processorClazzName = getChildNodeValue(element, "home-processor-class");
    codecClazzName = getChildNodeValue(element, "home-codec-class");

    //初始化接口和类,其中processClazz和codecClazz基本都是null
    homeApiClazz = Class.forName(homeApiName);
    homeClassImpl = Class.forName(homeClassName).newInstance();
    codecClazz = (Class) (StringUtils.isEmpty(codecClazzName) ? null : Class.forName(codecClazzName));
    processorClazz = (Class) (StringUtils.isEmpty(processorClazzName) ? null : Class.forName(processorClazzName));

    //判断是否注解如果有@Servent,从Servnet去name值
    if (TarsHelper.isServant(homeApiClazz)) {
        String servantName = homeApiClazz.getAnnotation(Servant.class).name();
        if (!StringUtils.isEmpty(servantName) && servantName.matches("^[\\w]+\\.[\\w]+\\.[\\w]+$")) {
            homeName = servantName;
        }
    }

    //获取ServantAdapter的配置 
    ServantAdapterConfig servantAdapterConfig = serverCfg.getServantAdapterConfMap().get(homeName);

    //根据配置初始化ServerAdapter类
    ServantAdapter ServerAdapter = new ServantAdapter(servantAdapterConfig);
    //初始化skeleton类
    skeleton = new ServantHomeSkeleton(homeName, homeClassImpl, homeApiClazz, codecClazz, processorClazz, maxLoadLimit);
    skeleton.setAppContext(this);
    //启动server服务
    ServerAdapter.bind(skeleton);
    servantAdapterMap.put(homeName, ServerAdapter);
    return skeleton;
}

这里出现了很多类,需要梳理下
- abstract class AppService
- ServantHomeSkeleton(可以理解为对外暴露接口的代理类)

AppService持有一个appContext

public abstract class AppService {

    public abstract AppContext getAppContext();

    public abstract String name();

}

public class ServantHomeSkeleton extends AppService {

    private String name = null;
    private Object servantImpl = null;
    private Class apiClass = null;
    private Class codecClazz = null;
    private Class processorClazz = null;
}
  • name 就是homeName
  • servantImpl就是homeClassImpl(对外暴露接口的实现类)
  • apiClass就是homeApiClazz(对外暴露接口)
  • codecClazz就是解码器(网络传输的二进制如何解码–可以理解为通信协议 具体包括二进制<—>Quest,二进制<—>Response)
  • processorClazz(Rquest<—->Response)

ServantAdapter 此类提供网络服务,此为网路服务的上层入口
Tars的网络服务底层是通过nio原生的实现,没有用mina和netty,整个底层的实现在net包下,我们可以暂缓了解。
这个类我们可以理解为开了一个服务器的端口,客户端连接上来,请求方法名和参数,这个类去调用相应的对外暴露的类实现,返回结果

public class ServantAdapter implements Adapter {

    private SelectorManager selectorManager = null;

    private final ServantAdapterConfig servantAdapterConfig;

    private ServantHomeSkeleton skeleton;
}
  • selectorManager 提供网络服务服务
  • servantAdapterConfig
  • skeleton 绑定一个ServantHomeSkeleton

思考疑问???
这里可以看出tars的设计一个不妥的地方
比如这里一个Adapter只持有一个skeleton,而一个skeleton对应一个接口和接口实现类
通俗易懂的说法:
tars只支持一个端口 对应一个接口,这是其设计决定的
一个接口,一个实现类,一个通信协议的解码器,一个序列化反序列化协议解析器和唯一一个端口绑定。底层是这么设计的。
为什么这么设计?如果想设计成一个端口对应多个接口怎么设计?
一个端口对应 一个通信协议的解码器,一个序列化反序列化协议,对应多个接口(配置文件得改成这样的格式)

public ServantAdapter(ServantAdapterConfig servantAdapterConfig) {
    this.servantAdapterConfig = servantAdapterConfig;
}

public ServantHomeSkeleton(String name, Object servantImpl, Class apiClass, Class codecClazz,
                               Class processorClazz, int loadLimit) {
    this.name = name;
    this.servantImpl = servantImpl;
    this.apiClass = apiClass;
    this.codecClazz = codecClazz;
    this.processorClazz = processorClazz;
}
public void bind(AppService appService) throws IOException {
    this.skeleton = (ServantHomeSkeleton) appService;
    ServerConfig serverCfg = ConfigurationManager.getInstance().getServerConfig();

    boolean keepAlive = true;
    //获取Codec
    Codec codec = createCodec(serverCfg);
    //获取process
    Processor processor = createProcessor(serverCfg);
    //获取ThreadPool
    Executor threadPool = ServantThreadPoolManager.get(servantAdapterConfig);

    Endpoint endpoint = this.servantAdapterConfig.getEndpoint();
    //如果是tcp,开启tcp端口
    if (endpoint.type().equals("tcp")) {
        this.selectorManager = new SelectorManager(Utils.getSelectorPoolSize(), new ServantProtocolFactory(codec), threadPool, processor, keepAlive, "server-tcp-reactor", false);
        this.selectorManager.setTcpNoDelay(serverCfg.isTcpNoDelay());
        this.selectorManager.start();

        System.out.println("[SERVER] server starting at " + endpoint + "...");
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(endpoint.host(), endpoint.port()), 1024);
        serverChannel.configureBlocking(false);

        selectorManager.getReactor(0).registerChannel(serverChannel, SelectionKey.OP_ACCEPT);

        System.out.println("[SERVER] server started at " + endpoint + "...");

    } else if (endpoint.type().equals("udp")) {

        this.selectorManager = new SelectorManager(1, new ServantProtocolFactory(codec), threadPool, processor, false, "server-udp-reactor", true);
        this.selectorManager.start();

        System.out.println("[SERVER] server starting at " + endpoint + "...");
        DatagramChannel serverChannel = DatagramChannel.open();
        DatagramSocket socket = serverChannel.socket();
        socket.bind(new InetSocketAddress(endpoint.host(), endpoint.port()));
        serverChannel.configureBlocking(false);

        this.selectorManager.getReactor(0).registerChannel(serverChannel, SelectionKey.OP_READ);
        System.out.println("[SERVER] servant started at " + endpoint + "...");
    }
}

private Processor createProcessor(ServerConfig serverCfg) throws TarsException {
    Processor processor = null;
    Class processorClass = skeleton.getProcessorClass();
    if (processorClass == null) {
        return new TarsServantProcessor();
    }

    if (processorClass != null) {
        Constructor constructor;
        try {
            constructor = processorClass.getConstructor(new Class[] { ServantAdapter.class });
            processor = constructor.newInstance(this);
        } catch (Exception e) {
            throw new TarsException("error occurred on create codec, codec=" + processorClass.getName());
        }
    }
    return processor;
}

private Codec createCodec(ServerConfig serverCfg) throws TarsException {
    Codec codec = null;
    Class codecClass = skeleton.getCodecClass();
    if (codecClass == null) {
        ServantCodec servantCodec = skeleton.getApiClass().getAnnotation(ServantCodec.class);
        if (servantCodec != null) {
            codecClass = servantCodec.codec();
        }
    }

    if (codecClass == null) {
        codecClass = TarsCodec.class;
    }

    if (codecClass != null) {
        Constructor constructor;
        try {
            constructor = codecClass.getConstructor(new Class[] { String.class });
            codec = constructor.newInstance(serverCfg.getCharsetName());
        } catch (Exception e) {
            throw new TarsException("error occurred on create codec, codec=" + codecClass.getName());
        }
    }
    return codec;
}

createCodec,createProcessor逻辑都是相似的,先从skeleton里取,如果配置没配置,再从annotation里取,如果没有就给默认值

  • createCodec(默认的类TarsCodec)
  • Processor (默认的类TarsServantProcessor)

这样服务器就启动起来了,回到上层函数 XmlAppContext的构造函数还有以下几个函数

  • injectAdminServant(); <–暂时不管它
  • initServants(); <–暂时不管它
  • appContextStarted();<–这个函数是把服务器启动的函数通知调用xml配置的listener
void appContextStarted() {
    for (AppContextListener listener : listeners) {
        listener.appContextStarted(new DefaultAppContextEvent(this));
    }
}

再回到上层函数 startAppContainer(); 这个就执行完毕
- startSessionManager();<–暂时不管它

  • registerServerHook();<–暂时不管它

这样服务端的核心部分的入口就解析完毕

最后梳理下:
全局只有一个appContain,理论上可以有多个appContext(实际上只有一个,这里是以XmlAppContext为例子),一个appContext下包含多个appService

  • Server启动类
    持有Container

  • ContainerManager
    持有全局的Container

  • AppContainer implements Container
    持有 ConcurrentHashMap

你可能感兴趣的:(Tars)