SOFAJRaft模块启动过程

本篇文章旨在分析SOFAJRaft中jraft-example模块的启动过程,由于SOFAJRaft在持续开源的过程中,所以无法保证示例代码永远是最新的,要是源代码有较大的变动,亦或出现纰漏、错误的地方,欢迎大家留言讨论。
@Author:Akai-yuan
更新时间:2023年1月20日

SOFAJRaft-Start Our Journey!

1. 开门见山:main方法概览

public static void main(final String[] args) throws IOException {
    if (args.length != 4) {
        System.out
            .println("Usage : java com.alipay.sofa.jraft.example.counter.CounterServer {dataPath} {groupId} {serverId} {initConf}");
        System.out
            .println("Example: java com.alipay.sofa.jraft.example.counter.CounterServer /tmp/server1 counter 127.0.0.1:8081 127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083");
        System.exit(1);
    }
    //日志存储路径
    final String dataPath = args[0];
    //SOFAJRaft集群的名字
    final String groupId = args[1];
    //当前节点的ip和端口
    final String serverIdStr = args[2];
    //集群节点的ip和端口
    final String initConfStr = args[3];

    final NodeOptions nodeOptions = new NodeOptions();
    // for test, modify some params
    // 设置选举超时时间为 1 秒
    nodeOptions.setElectionTimeoutMs(1000);
    // 关闭 CLI 服务
    nodeOptions.setDisableCli(false);
    // 每隔30秒做一次 snapshot
    nodeOptions.setSnapshotIntervalSecs(30);
    // 解析参数
    final PeerId serverId = new PeerId();
    if (!serverId.parse(serverIdStr)) {
        throw new IllegalArgumentException("Fail to parse serverId:" + serverIdStr);
    }
    final Configuration initConf = new Configuration();
    //将raft分组加入到Configuration的peers数组中
    if (!initConf.parse(initConfStr)) {
        throw new IllegalArgumentException("Fail to parse initConf:" + initConfStr);
    }
    // 设置初始集群配置
    nodeOptions.setInitialConf(initConf);

    // 启动raft server
    final CounterServer counterServer = new CounterServer(dataPath, groupId, serverId, nodeOptions);
    System.out.println("Started counter server at port:"
                       + counterServer.getNode().getNodeId().getPeerId().getPort());
    // GrpcServer need block to prevent process exit
    CounterGrpcHelper.blockUntilShutdown();
}

我们在启动CounterServer的main方法的时候,会将传入的String[]类型参数args分别转化为日志存储的路径、SOFAJRaft集群的名字、当前节点的ip和端口、集群节点的ip和端口,并设值到NodeOptions中,作为当前节点启动的参数。

2. 对象转换:创建PeerId

引子:在main方法中,我们可以看到,程序将String类型参数转换成了PeerId对象,那么接下来我们需要探究转换的具体过程。

在转换当前节点并初始化为一个PeerId对象的过程中,调用了PeerId中的parse方法:

public boolean parse(final String s) {
    if (StringUtils.isEmpty(s)) {
        return false;
    }

    final String[] tmps = Utils.parsePeerId(s);
    if (tmps.length < 2 || tmps.length > 4) {
        return false;
    }
    try {
        final int port = Integer.parseInt(tmps[1]);
        this.endpoint = new Endpoint(tmps[0], port);
        switch (tmps.length) {
            case 3:
                this.idx = Integer.parseInt(tmps[2]);
                break;
            case 4:
                if (tmps[2].equals("")) {
                    this.idx = 0;
                } else {
                    this.idx = Integer.parseInt(tmps[2]);
                }
                this.priority = Integer.parseInt(tmps[3]);
                break;
            default:
                break;
        }
        this.str = null;
        return true;
    } catch (final Exception e) {
        LOG.error("Parse peer from string failed: {}.", s, e);
        return false;
    }
}

该方法内部又调用了工具类Utils.parsePeerId,最终达到的效果如下:
其中,a、b分别对应IP和Port端口号,组成了PeerId的EndPoint属性;c指代idx【同一地址中的索引,默认值为0】;d指代priority优先级【节点的本地优先级值,如果节点不支持优先级选择,则该值为-1】。

PeerId.parse("a:b")          = new PeerId("a", "b", 0 , -1)
PeerId.parse("a:b:c")        = new PeerId("a", "b", "c", -1)
PeerId.parse("a:b::d")       = new PeerId("a", "b", 0, "d")
PeerId.parse("a:b:c:d")      = new PeerId("a", "b", "c", "d")

3. 渐入佳境:构造CountServer

引子:在main方法中,我们可以看到,进行初步的参数解析后,调用了CountServer的构造器,要说这个构造器,第一次看里面的步骤确实会感觉挺复杂的,接下来我们一起分析一下源码。

CountServer构造器的源码如下:

public CounterServer(final String dataPath, final String groupId, final PeerId serverId,
                         final NodeOptions nodeOptions) throws IOException {
        // 初始化raft data path, 它包含日志、元数据、快照
        FileUtils.forceMkdir(new File(dataPath));

        // 这里让 raft RPC 和业务 RPC 使用同一个 RPC server, 通常也可以分开
        final RpcServer rpcServer = RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint());
        // GrpcServer need init marshaller
        CounterGrpcHelper.initGRpc();
        CounterGrpcHelper.setRpcServer(rpcServer);

        // 注册业务处理器
        CounterService counterService = new CounterServiceImpl(this);
        rpcServer.registerProcessor(new GetValueRequestProcessor(counterService));
        rpcServer.registerProcessor(new IncrementAndGetRequestProcessor(counterService));
        // 初始化状态机
        this.fsm = new CounterStateMachine();
        // 设置状态机到启动参数
        nodeOptions.setFsm(this.fsm);
       // 设置存储路径 (包含日志、元数据、快照)
        // 日志(必须)
        nodeOptions.setLogUri(dataPath + File.separator + "log");
        // 元数据(必须)
        nodeOptions.setRaftMetaUri(dataPath + File.separator + "raft_meta");
        // 快照(可选, 一般都推荐)
        nodeOptions.setSnapshotUri(dataPath + File.separator + "snapshot");
        // 初始化 raft group 服务框架
        this.raftGroupService = new RaftGroupService(groupId, serverId, nodeOptions, rpcServer);
        // 启动
        this.node = this.raftGroupService.start();
    }

接下来仔细说说CountServer的构造器里面具体做了什么。

4. 追根溯源:RpcServer

引子:CountServer构造器中调用的RaftRpcServerFactory.createRaftRpcServer()方法,底层到底是如何构造出一个RpcServer的呢,接下来会和大家讨论createRaftRpcServer()方法的具体实现

首先请看RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint())方法:
createRaftRpcServer方法目前有createRaftRpcServer(final Endpoint endpoint)和
createRaftRpcServer(final Endpoint endpoint, final Executor raftExecutor,final Executor cliExecutor)两个重载方法,其实不管哪个方法,本质上实现过程都有如下两个步骤:
(1)首先调用了GrpcRaftRpcFactory的createRpcServer方法,这里涉及gRpc构建server的底层知识,有时间会再写一篇文章探究一下gRpc,这里可以简单理解为构建了一个rpc服务端。该方法实现如下:

public RpcServer createRpcServer(final Endpoint endpoint, final ConfigHelper<RpcServer> helper) {
    final int port = Requires.requireNonNull(endpoint, "endpoint").getPort();
    Requires.requireTrue(port > 0 && port < 0xFFFF, "port out of range:" + port);
    final MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry();
    final Server server = ServerBuilder.forPort(port) //
        .fallbackHandlerRegistry(handlerRegistry) //
        .directExecutor() //
        .maxInboundMessageSize(RPC_MAX_INBOUND_MESSAGE_SIZE) //
        .build();
    final RpcServer rpcServer = new GrpcServer(server, handlerRegistry, this.parserClasses, getMarshallerRegistry());
    if (helper != null) {
        helper.config(rpcServer);
    }
    return rpcServer;
}

(2)紧接着调用addRaftRequestProcessors,这个方法为RpcServer添加RAFT和CLI服务核心请求处理器,关于RpcProcessor这个实体类,会在后面的文章中具体分析,这里可以先"不求甚解"。

  //添加RAFT和CLI服务请求处理器
public static void addRaftRequestProcessors(final RpcServer rpcServer, final Executor raftExecutor,
                                                final Executor cliExecutor) {
        // 添加raft核心处理器
        final AppendEntriesRequestProcessor appendEntriesRequestProcessor = new AppendEntriesRequestProcessor(
            raftExecutor);
        rpcServer.registerConnectionClosedEventListener(appendEntriesRequestProcessor);
        rpcServer.registerProcessor(appendEntriesRequestProcessor);
        rpcServer.registerProcessor(new GetFileRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new InstallSnapshotRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new RequestVoteRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new PingRequestProcessor());
        rpcServer.registerProcessor(new TimeoutNowRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new ReadIndexRequestProcessor(raftExecutor));
        // 添加raft cli服务处理器
        rpcServer.registerProcessor(new AddPeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new RemovePeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ResetPeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ChangePeersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new GetLeaderRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new SnapshotRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new TransferLeaderRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new GetPeersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new AddLearnersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new RemoveLearnersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ResetLearnersRequestProcessor(cliExecutor));
    }

5. 一探究竟:CounterGrpcHelper做了什么

CountServer构造器在初步创建RpcServer后,调用了CounterGrpcHelper.initGRpc()和CounterGrpcHelper.setRpcServer(rpcServer)两个方法,接下来和大家分析这两个方法的实现过程

首先请看initGRpc方法:
RpcFactoryHelper.rpcFactory()实际是调用了GrpcRaftRpcFactory(因为GrpcRaftRpcFactory实现了RaftRpcFactory接口),GrpcRaftRpcFactory中维护了一个ConcurrentHashMap<String, Message> parserClasses 其中【key为各种请求/响应实体的名称,value为对应请求/响应的实例】。
然后通过反射获取到MarshallerHelper的registerRespInstance方法,实际上MarshallerHelper里面维护了一个ConcurrentHashMap<String, Message> messages 其中【key为请求实体的名称,value为对应响应的实例

    public static void initGRpc() {
        if ("com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory".equals(RpcFactoryHelper.rpcFactory().getClass()
            .getName())) {
            
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(CounterOutter.GetValueRequest.class.getName(),
                CounterOutter.GetValueRequest.getDefaultInstance());
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(
                CounterOutter.IncrementAndGetRequest.class.getName(),
                CounterOutter.IncrementAndGetRequest.getDefaultInstance());
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(CounterOutter.ValueResponse.class.getName(),
                CounterOutter.ValueResponse.getDefaultInstance());

            try {
                Class<?> clazz = Class.forName("com.alipay.sofa.jraft.rpc.impl.MarshallerHelper");
                Method registerRespInstance = clazz.getMethod("registerRespInstance", String.class, Message.class);
                registerRespInstance.invoke(null, CounterOutter.GetValueRequest.class.getName(),
                    CounterOutter.ValueResponse.getDefaultInstance());
                registerRespInstance.invoke(null, CounterOutter.IncrementAndGetRequest.class.getName(),
                    CounterOutter.ValueResponse.getDefaultInstance());
            } catch (Exception e) {
                LOG.error("Failed to init grpc server", e);
            }
        }
    }

接着我们再看setRpcServer方法:
CounterGrpcHelper里面还维护了一个RpcServer实例,CounterGrpcHelper.setRpcServer(rpcServer)实际上会将构造的RpcServer装配到CounterGrpcHelper里面。

public static void setRpcServer(RpcServer rpcServer) {
        CounterGrpcHelper.rpcServer = rpcServer;
}

6.乘胜追击:RaftGroupService

在CountServer构造器中,经过上述一系列操作步骤,走到了RaftGroupService构造器中,在构造RaftGroupService实体后,调用了它的start方法,这一步在于初始化 raft group 服务框架

public synchronized Node start(final boolean startRpcServer) {
    	//如果已经启动了,那么就返回
        if (this.started) {
            return this.node;
        }
    	//校验serverId和groupId
        if (this.serverId == null || this.serverId.getEndpoint() == null
            || this.serverId.getEndpoint().equals(new Endpoint(Utils.IP_ANY, 0))) {
            throw new IllegalArgumentException("Blank serverId:" + this.serverId);
        }
        if (StringUtils.isBlank(this.groupId)) {
            throw new IllegalArgumentException("Blank group id:" + this.groupId);
        }
         //设置当前node的ip和端口
        NodeManager.getInstance().addAddress(this.serverId.getEndpoint());
    	//创建node
        this.node = RaftServiceFactory.createAndInitRaftNode(this.groupId, this.serverId, this.nodeOptions);
        if (startRpcServer) {
             //启动远程服务
            this.rpcServer.init(null);
        } else {
            LOG.warn("RPC server is not started in RaftGroupService.");
        }
        this.started = true;
        LOG.info("Start the RaftGroupService successfully.");
        return this.node;
    }

这个方法会在一开始的时候对RaftGroupService在构造器实例化的参数进行校验,然后把当前节点的Endpoint添加到NodeManager的addrSet变量中,接着调用RaftServiceFactory#createAndInitRaftNode实例化Node节点。
每个节点都会启动一个rpc的服务,因为每个节点既可以被选举也可以投票给其他节点,节点之间需要互相通信,所以需要启动一个rpc服务。

7.刨根问底:Node节点的创建

以下就是Node节点的一系列创建过程,由于嵌套的层数比较多,所以就全部列举出来了,整个过程简而言之就是,createAndInitRaftNode方法首先调用createRaftNode实例化一个Node的实例NodeImpl,然后调用其init方法进行初始化,主要的配置都是在init方法中完成的。代码如下:

this.node = RaftServiceFactory.createAndInitRaftNode(this.groupId, this.serverId, this.nodeOptions);
public static Node createAndInitRaftNode(final String groupId, final PeerId serverId, final NodeOptions opts) {
        final Node ret = createRaftNode(groupId, serverId);
        if (!ret.init(opts)) {
            throw new IllegalStateException("Fail to init node, please see the logs to find the reason.");
        }
        return ret;
    }
public static Node createRaftNode(final String groupId, final PeerId serverId) {
        return new NodeImpl(groupId, serverId);
    }
public NodeImpl(final String groupId, final PeerId serverId) {
        super();
        if (groupId != null) {
            Utils.verifyGroupId(groupId);
        }
        this.groupId = groupId;
        this.serverId = serverId != null ? serverId.copy() : null;
        this.state = State.STATE_UNINITIALIZED;
        this.currTerm = 0;
        updateLastLeaderTimestamp(Utils.monotonicMs());
        this.confCtx = new ConfigurationCtx(this);
        this.wakingCandidate = null;
        final int num = GLOBAL_NUM_NODES.incrementAndGet();
        LOG.info("The number of active nodes increment to {}.", num);
    }

8.一窥到底:Node节点的初始化

老实说,NodeImpl#init方法确实挺长的,所以我打算分成几个部分来展示,方便分析

(1)《参数的赋值与校验》

这段代码主要是给各个变量赋值,然后进行校验判断一下serverId不能为0.0.0.0,当前的Endpoint必须要在NodeManager里面设置过等等(NodeManager的设置是在RaftGroupService的start方法里)。
然后会初始化一个全局的的定时调度管理器TimerManager:

    	//一系列判空操作
		Requires.requireNonNull(opts, "Null node options");
        Requires.requireNonNull(opts.getRaftOptions(), "Null raft options");
        Requires.requireNonNull(opts.getServiceFactory(), "Null jraft service factory");
        //JRaftServiceFactory目前有3个实现类
        // 1.BDBLogStorageJRaftServiceFactory    
        // 2.DefaultJRaftServiceFactory
        // 3.HybridLogJRaftServiceFactory
		this.serviceFactory = opts.getServiceFactory();
        this.options = opts;
        this.raftOptions = opts.getRaftOptions();
    	//基于 Metrics 类库的性能指标统计,具有丰富的性能统计指标,默认为false,不开启度量工具
        this.metrics = new NodeMetrics(opts.isEnableMetrics());
        this.serverId.setPriority(opts.getElectionPriority());
        this.electionTimeoutCounter = 0;
    	//Utils.IP_ANY = "0.0.0.0"
        if (this.serverId.getIp().equals(Utils.IP_ANY)) {
            LOG.error("Node can't started from IP_ANY.");
            return false;
        }

        if (!NodeManager.getInstance().serverExists(this.serverId.getEndpoint())) {
            LOG.error("No RPC server attached to, did you forget to call addService?");
            return false;
        }

        if (this.options.getAppendEntriesExecutors() == null) {
            this.options.setAppendEntriesExecutors(Utils.getDefaultAppendEntriesExecutor());
        }
    	//定时任务管理器
		//此处TIMER_FACTORY获取到的是DefaultRaftTimerFactory
    	//this.options.isSharedTimerPool()默认为false
		//this.options.getTimerPoolSize()取值为Utils.cpus() * 3 > 20 ? 20 : Utils.cpus() * 3
        this.timerManager = TIMER_FACTORY.getRaftScheduler(this.options.isSharedTimerPool(),
                this.options.getTimerPoolSize(), "JRaft-Node-ScheduleThreadPool");

此处浅析一下__TimerManager:
初始化一个线程池,根据传入的参数this.options.getTimerPoolSize()==Utils.cpus() * 3 > 20 ? 20 : Utils.cpus() * 3可以分析得知如果当前的服务器的cpu线程数_3 大于20 ,那么这个线程池的coreSize就是20,否则就是cpu线程数的_3倍。

public TimerManager(int workerNum, String name) {
        this.executor = ThreadPoolUtil.newScheduledBuilder() //
            .poolName(name) //
            .coreThreads(workerNum) //
            .enableMetric(true) //
            .threadFactory(new NamedThreadFactory(name, true)) //
            .build();
    }

(2)《计时器的初始化》

由于这些计时器的实现比较繁杂,所以具体功能等到后面对应章节再一并梳理。

  • voteTimer是用来控制选举的,如果选举超时,当前的节点又是候选者角色,那么就会发起选举。
  • electionTimer是预投票计时器。候选者在发起投票之前,先发起预投票,如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选。
  • stepDownTimer定时检查是否需要重新选举leader。当前的leader可能出现它的Follower可能并没有整个集群的1/2却还没有下台的情况,那么这个时候会定期的检查看leader的Follower是否有那么多,没有那么多的话会强制让leader下台。
  • snapshotTimer快照计时器。这个计时器会每隔1小时触发一次生成一个快照。

这些计时器有一个共同的特点就是会根据不同的计时器返回一个在一定范围内随机的时间。返回一个随机的时间可以防止多个节点在同一时间内同时发起投票选举从而降低选举失败的概率。

    	//设置投票计时器
        final String suffix = getNodeId().toString();
        String name = "JRaft-VoteTimer-" + suffix;
        this.voteTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(), TIMER_FACTORY.getVoteTimer(
                this.options.isSharedVoteTimer(), name)) {
        	//处理投票超时
            @Override
            protected void onTrigger() {
                handleVoteTimeout();
            }
        	//在一定范围内返回一个随机的时间戳
            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };
		//设置预投票计时器
		//当leader在规定的一段时间内没有与 Follower 舰船进行通信时,
		// Follower 就可以认为leader已经不能正常担任旗舰的职责,则 Follower 可以去尝试接替leader的角色。
		// 这段通信超时被称为 Election Timeout
		//候选者在发起投票之前,先发起预投票
        name = "JRaft-ElectionTimer-" + suffix;
        this.electionTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(),
                TIMER_FACTORY.getElectionTimer(this.options.isSharedElectionTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleElectionTimeout();
            }

            //在一定范围内返回一个随机的时间戳
        	//为了避免同时发起选举而导致失败
            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };

    	//leader下台的计时器
		//定时检查是否需要重新选举leader
        name = "JRaft-StepDownTimer-" + suffix;
        this.stepDownTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs() >> 1,
                TIMER_FACTORY.getStepDownTimer(this.options.isSharedStepDownTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleStepDownTimeout();
            }
        };
		//快照计时器
        name = "JRaft-SnapshotTimer-" + suffix;
        this.snapshotTimer = new RepeatedTimer(name, this.options.getSnapshotIntervalSecs() * 1000,
                TIMER_FACTORY.getSnapshotTimer(this.options.isSharedSnapshotTimer(), name)) {

            private volatile boolean firstSchedule = true;

            @Override
            protected void onTrigger() {
                handleSnapshotTimeout();
            }

            @Override
            protected int adjustTimeout(final int timeoutMs) {
                if (!this.firstSchedule) {
                    return timeoutMs;
                }

                // Randomize the first snapshot trigger timeout
                this.firstSchedule = false;
                if (timeoutMs > 0) {
                    int half = timeoutMs / 2;
                    return half + ThreadLocalRandom.current().nextInt(half);
                } else {
                    return timeoutMs;
                }
            }
        };

(3)《消费队列Disruptor》

关于Disruptor的内容,后面有时间会写一篇相关的文章进行分享

这里初始化了一个Disruptor作为消费队列,然后校验了metrics是否开启,默认是不开启的

 this.configManager = new ConfigurationManager();
		//初始化一个disruptor,采用多生产者模式
        this.applyDisruptor = DisruptorBuilder.<LogEntryAndClosure>newInstance() //
            //设置disruptor大小,默认16384    
            	.setRingBufferSize(this.raftOptions.getDisruptorBufferSize()) //
                .setEventFactory(new LogEntryAndClosureFactory()) //
                .setThreadFactory(new NamedThreadFactory("JRaft-NodeImpl-Disruptor-", true)) //
                .setProducerType(ProducerType.MULTI) //
                .setWaitStrategy(new BlockingWaitStrategy()) //
                .build();
		//设置事件处理器
        this.applyDisruptor.handleEventsWith(new LogEntryAndClosureHandler());
		//设置异常处理器
        this.applyDisruptor.setDefaultExceptionHandler(new LogExceptionHandler<Object>(getClass().getSimpleName()));
        // 启动disruptor的线程
		this.applyQueue = this.applyDisruptor.start();
    	//如果开启了metrics统计
        if (this.metrics.getMetricRegistry() != null) {
            this.metrics.getMetricRegistry().register("jraft-node-impl-disruptor",
                    new DisruptorMetricSet(this.applyQueue));
        }

(4)《功能初始化》

对快照、日志、元数据等功能进行初始化

    	//fsmCaller封装对业务 StateMachine 的状态转换的调用以及日志的写入等
		this.fsmCaller = new FSMCallerImpl();
    	//初始化日志存储功能
        if (!initLogStorage()) {
            LOG.error("Node {} initLogStorage failed.", getNodeId());
            return false;
        }
		//初始化元数据存储功能
        if (!initMetaStorage()) {
            LOG.error("Node {} initMetaStorage failed.", getNodeId());
            return false;
        }
    	//对FSMCaller初始化
        if (!initFSMCaller(new LogId(0, 0))) {
            LOG.error("Node {} initFSMCaller failed.", getNodeId());
            return false;
        }
    	//实例化投票箱
        this.ballotBox = new BallotBox();
        final BallotBoxOptions ballotBoxOpts = new BallotBoxOptions();
        ballotBoxOpts.setWaiter(this.fsmCaller);
        ballotBoxOpts.setClosureQueue(this.closureQueue);
    	//初始化ballotBox的属性
        if (!this.ballotBox.init(ballotBoxOpts)) {
            LOG.error("Node {} init ballotBox failed.", getNodeId());
            return false;
        }
    	//初始化快照存储功能
        if (!initSnapshotStorage()) {
            LOG.error("Node {} initSnapshotStorage failed.", getNodeId());
            return false;
        }
    	//校验日志文件索引的一致性
        final Status st = this.logManager.checkConsistency();
        if (!st.isOk()) {
            LOG.error("Node {} is initialized with inconsistent log, status={}.", getNodeId(), st);
            return false;
        }
    	//配置管理raft group中的信息
        this.conf = new ConfigurationEntry();
        this.conf.setId(new LogId());
        // if have log using conf in log, else using conf in options
        if (this.logManager.getLastLogIndex() > 0) {
            checkAndSetConfiguration(false);
        } else {
            this.conf.setConf(this.options.getInitialConf());
            // initially set to max(priority of all nodes)
            this.targetPriority = getMaxPriorityOfNodes(this.conf.getConf().getPeers());
        }

        if (!this.conf.isEmpty()) {
            Requires.requireTrue(this.conf.isValid(), "Invalid conf: %s", this.conf);
        } else {
            LOG.info("Init node {} with empty conf.", this.serverId);
        }

初始化replicatorGroup、rpcService以及readOnlyService:

// TODO RPC service and ReplicatorGroup is in cycle dependent, refactor it
        this.replicatorGroup = new ReplicatorGroupImpl();
    	//收其他节点或者客户端发过来的请求,转交给对应服务处理
        this.rpcService = new DefaultRaftClientService(this.replicatorGroup, this.options.getAppendEntriesExecutors());
        final ReplicatorGroupOptions rgOpts = new ReplicatorGroupOptions();
        rgOpts.setHeartbeatTimeoutMs(heartbeatTimeout(this.options.getElectionTimeoutMs()));
        rgOpts.setElectionTimeoutMs(this.options.getElectionTimeoutMs());
        rgOpts.setLogManager(this.logManager);
        rgOpts.setBallotBox(this.ballotBox);
        rgOpts.setNode(this);
        rgOpts.setRaftRpcClientService(this.rpcService);
        rgOpts.setSnapshotStorage(this.snapshotExecutor != null ? this.snapshotExecutor.getSnapshotStorage() : null);
        rgOpts.setRaftOptions(this.raftOptions);
        rgOpts.setTimerManager(this.timerManager);

        // Adds metric registry to RPC service.
        this.options.setMetricRegistry(this.metrics.getMetricRegistry());
    	//初始化rpc服务
        if (!this.rpcService.init(this.options)) {
            LOG.error("Fail to init rpc service.");
            return false;
        }
        this.replicatorGroup.init(new NodeId(this.groupId, this.serverId), rgOpts);

        this.readOnlyService = new ReadOnlyServiceImpl();
        final ReadOnlyServiceOptions rosOpts = new ReadOnlyServiceOptions();
        rosOpts.setFsmCaller(this.fsmCaller);
        rosOpts.setNode(this);
        rosOpts.setRaftOptions(this.raftOptions);
    	//只读服务初始化
        if (!this.readOnlyService.init(rosOpts)) {
            LOG.error("Fail to init readOnlyService.");
            return false;
        }

(5)《逻辑变动》

这段代码里会将当前的状态设置为Follower,然后启动快照定时器定时生成快照。
如果当前的集群不是单节点集群需要做一下stepDown,表示新生成的Node节点需要重新进行选举。
最下面有一个if分支,如果当前的jraft集群里只有一个节点,那么个节点必定是leader直接进行选举就好了,所以会直接调用electSelf进行选举。

    	// 将当前的状态设置为Follower
        this.state = State.STATE_FOLLOWER;

        if (LOG.isInfoEnabled()) {
            LOG.info("Node {} init, term={}, lastLogId={}, conf={}, oldConf={}.", getNodeId(), this.currTerm,
                    this.logManager.getLastLogId(false), this.conf.getConf(), this.conf.getOldConf());
        }
    	//如果快照执行器不为空,并且生成快照的时间间隔大于0,那么就定时生成快照
        if (this.snapshotExecutor != null && this.options.getSnapshotIntervalSecs() > 0) {
            LOG.debug("Node {} start snapshot timer, term={}.", getNodeId(), this.currTerm);
            this.snapshotTimer.start();
        }
    	//新启动的node需要重新选举
        if (!this.conf.isEmpty()) {
            stepDown(this.currTerm, false, new Status());
        }

        if (!NodeManager.getInstance().add(this)) {
            LOG.error("NodeManager add {} failed.", getNodeId());
            return false;
        }

        // Now the raft node is started , have to acquire the writeLock to avoid race
        // conditions
        this.writeLock.lock();
    	//这个分支表示当前的jraft集群里只有一个节点,那么个节点必定是leader直接进行选举就好了
        if (this.conf.isStable() && this.conf.getConf().size() == 1 && this.conf.getConf().contains(this.serverId)) {
            // The group contains only this server which must be the LEADER, trigger
            // the timer immediately.
            electSelf();
        } else {
            this.writeLock.unlock();
        }

        return true;

9.写在最后

SOFAJRaft 是一个基于 RAFT 一致性算法的生产级高性能 Java 实现。
第一次阅读这种复杂的开源代码,老实说确实非常吃力,但其实步步深入,反复推敲,逐渐会从恐惧陌生甚至抵触,转变为惊喜与赞叹。你会慢慢痴迷于里面很多优雅且优秀的实现。
在这里,感谢SOFAJRaft的每一位代码贡献者。源码的阅读过程中,的的确确学到了很多东西。我也会继续学习下去,希望能够巩固、深入我对RAFT一致性算法的理解与体悟。

本文转载于网络 如有侵权请联系删除

相关文章

  • 腾讯多媒体实验室亮相首届世界5G大会,展现5G+多媒体前沿研究成果

    11月20-23日,由北京市政府、国家发改委、科技部、工信部共同主办的首届“世界5G大会”在北京举办,全球信息通信领域最具影响力的科学家、世界知名5G上下游企业、5G行业用户和知名投资人等,围绕“智慧城市、智生活”、“智慧交通、享出行”、“智能制造、创未来”、“智慧医疗、更健康”、“高清视频、看世界”五大主题展开交流与讨论。腾讯多媒体实验室携前沿科技成果——云游戏、VRdemo亮相展区,展示了多媒体实验室在5G与多媒体结合领域的最新研究成果,为参展企业、业内用户和观众带来云游戏、高清VR全景互动体验。设备硬件配置不够,不能畅快游玩;游戏外挂太多,毫无游戏体验……这些困扰当下游戏玩家的难题,都会随着未来云游戏的落地而“烟消云散”。首届世界5G大会现场,多媒体实验室带来了在云游戏领域的最新研究进展。(多媒体实验室start云游戏展示)云游戏是5G技术的重要应用场景,在这一以云计算为基础的游戏方式下,所有游戏都在服务器端运行,不需要顶级的配置就能享受更高端的游戏。然而,云游戏所需要的高分辨率、超低延迟、高帧率对编解码技术提出了挑战。5G技术飞速发展大背景下,多媒体实验室深度优化视频编解码技术

  • 如何使用dex搭建一个身份认证系统?

    导言一个成熟的软件系统一般必须有一个可靠的身份认证与权限验证功能。这一块要自研快速实现还是需要花费挺多精力的,幸好开源领域目前已经有不错的解决方案,一般拿过来按项目的实际需求进行一些简单的定制基本就可以实现业务目标了。最近刚好在这方面进行了一些工作,这里将如何实现身份认证及权限验证分两篇博文大概梳理一下,这篇先讲身份认证。什么是身份认证身份验证(英语:Authentication)又称“验证”,是指通过一定的手段,完成对用户身份的确认。 身份验证的目的是确认当前所声称为某种身份的用户,确实是所声称的用户。在日常生活中,身份验证并不罕见;比如,通过检查对方的证件,我们一般可以确信对方的身份。虽然日常生活中的这种确认对方身份的做法也属于广义的“身份验证”,但“身份验证”一词更多地被用在计算机、通信等领域。以上是维基百科的解释,说白了就是用某种方式确保用户是某种身份,这种确保需要保证其它用户没那么容易伪装其身份。一般只有经过身份认证得到可靠的用户身份后,才能基于该身份进行后续的权限验证流程。实现身份认证系统一般来说业务系统会专注于业务逻辑的处理,而身份认证相关的功能会放入独立的身份认证系

  • H3CNE实验系列 | 网络地址转换NAT

    编辑|排版|制图|测试|©瑞哥此文用时1小时19分钟,原创不易,坚持更不易,希望我的每一份劳动成果都可以得到大家的一个【好看】一、实验拓扑二、实验要求如下图所示,一个公司通过h3c路由器的地址转换功能连接到广域网。要求该公司能够通过h3c路由器s1/0访问internet,公司内部对外提供www、ftp和smtp服务,而且提供两台www的服务器。公司内部网址为10.110.0.0/16。其中,内部ftp服务器地址为10.110.10.1,内部www服务器1地址为10.110.10.2,内部www服务器2地址为10.110.10.3,内部smtp服务器地址为10.110.10.4,并且希望可以对外提供统一的服务器的ip地址。内部10.110.10.0/24网段可以访问internet,其它网段的pc机则不能访问internet。外部的pc可以访问内部的服务器。公司具有202.38.160.100至202.38.160.105六个合法的ip地址。选用202.38.160.100作为公司对外的ip地址,www服务器2对外采用8080端口。三、实验步骤<H3C>sys#配置地址池和

  • 2018年教育部产学合作协同育人腾讯公司项目指南

    为响应《国务院办公厅关于深化高等学校创新创业教育改革的实施意见》(国办发〔2015〕36号)和《国务院办公厅关于深化产教融合的若干意见》(国办发〔2017〕95号),贯彻落实习近平总书记在北京大学师生座谈会上重要讲话精神,进一步推进产学合作、产教融合、支持和鼓励高校教师和学生参与到人才培养和创新创业学习和工作中。在教育部高等教育司的指导下,腾讯公司在2018年教育部产学合作协同育人第二期项目中设立新工科建设、教学内容和课程体系改革、实践条件和实践基地建设项目、师资培训四类项目。项目安排时间 2018年12月30日申请截止,启动评审2019年1月15日评审截止,公示入选结果2019年1月25日启动协议签署,项目启动2019年6月25日前中期检查,提交项目中期报告2019年12月30日前项目结题,提交结题报告项目申请办法项目申请人根据高校情况和产学合作需要,自愿在“教育部产学合作协同育人项目平台”(项目平台网址:http://cxhz.hep.com.cn)注册申报,并加强项目实施过程管理。 如在教育部产学合作协同育人项目平台申报过程中,系统出现问题,项目申请人也可按照申请项目类型填写项目

  • spring-boot-2.0.3源码篇 - filter的注册,值得一看

    前言  开心一刻    过年女婿来岳父家走亲戚,当时小舅子主就问:姐夫,你什么时候能给我姐幸福,让我姐好好享受生活的美好。你们这辈子不准备买一套大点的房子吗?姐夫说:现在没钱啊!不过我有一个美丽可爱的女儿,等长大后找个有钱的老公嫁了,那时我就能和你姐一起住大房子了。岳父不乐意的说了一句:当初我也是这么认为的,可惜未能如愿。  路漫漫其修远兮,吾将上下而求索!  github:https://github.com/youzhibing  码云(gitee):https://gitee.com/youzhibingServletContext  ServletContext介绍    定义了servlet与其servlet容器通信的一些列方法,例如,获取文件的MIME类型,分派请求或写入日志文件。    Servlet的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随这个交易过程直到这个交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些制定参数通常就是一个配置类。所以对号入座,

  • 基于http的百度语音 REST api

    什么是RESTapi? --REpresentationalStateTransfer RESTapi是基于http请求的一种api,就百度语音识别的实例来讲,通过百度提供的url加上经过编码的音频文件,向百度服务器发出请求,然后百度服务器返回识别的内容。结束。优点不受平台限制(我在树莓派上操作的) 代码简单复制缺点:依赖网络 对要识别的音频格式要求高复制百度语音RESTapi支持的语言java、php、python、c#、Node.js。下面分享一个python2.7版的实例 1.先去注册开发者账号,新建应用,获得APP_ID,API_KEY,SECRET_KEY 2.安装SDK安装使用SDK有如下方式: 如果已安装pip,执行pipinstallbaidu-aip即可。 如果已安装setuptools,执行pythonsetup.pyinstall即可复制3.安装完了看代码 语音合成#-*-coding:UTF-8-*- fromaipimportAipSpeech #定义常量 APP_ID='9****418' API_KEY='uXmKYE0zX6

  • GDB高级技巧

    GDB高级技巧 yijian2009-2-22 难得有雅兴,边动手边记录操作步骤,本文主要示例一些平常较少使用到的GDB功能,掌握这些用法有助于提高GDB调试和解决问题的能力。 1)查看宏 默认情况下,在GDB中是不能查看宏的值及定义的,但通过如下方法,则可以达到目的: 编译源代码时,加上“-g3-gdwarf-2”选项,请注意不是“-g”,必须为“-g3”,查看宏的值使用命令p,这和查看变量的值的方法相同,如果想查看宏的定义,使用“macroexpand”命令即可 2)如果执行一连串命令? 这个借助命令“source”即可,“source”的参数为一个存有一串命令的文件名 3)如何同时给多个函数打断点? 使用“rb”命令,如果执行“rb”时不带参数,则表示在所有函数处打一个断点,“rb”后面可以接一个符合正则表达式的参数,用来对符合正则表达式的所有函数打断点 4).gdbinit文件 GDB在启动时,会在用户主目录中寻找这个文件,并执行该文件中所有命令,文件格式为: definecommand-alias command end 如要给“bmain”取一个别名“bm”: defin

  • 移动测试Appium之API手册

    移动测试Appium之API手册前言本文对AppiumPythonClient中webdriver.py代码进行分析说明。笔者使用python3.6版本,安装在C:\Python36目录下,在C:\Python36\Lib\site-packages\appium\webdriver\webdriver.py找到本文要分析的目标模块文件。源码概要我们先看一下webdriver.py中源码概要情况,如图:从上图来看,整个模块分为两大块:导入相关的基础模块或资源从webdriver.Remote继承,定义和实现了AppiumPython版本的客户端驱动webdriver类由此我们可以清楚的得出以下几个结论:AppiumPython的webdriver客户端是依赖selenium2中的webdriver的Appium-Python-Client定义和实现了针对android和ios移动设备专用的方法只要是selenium2中定义的webdriver方法在移动设备端可用,那么在移动测试时,可以直接用API说明下面我们把上述的API以一个表格的方式进行说明,以便后续大家在使用appium进行自动

  • 两强相争O2O,未来的生活秘书是腾讯?阿里?

    谁在为O2O添柴架火,是什么样的魔力让互联网巨擘为O2O拼的头破血流,电商是主战场还只是一个开端?中国最被看好的两家千亿市值企业腾讯、阿里掀起了两次国内互联网的冲动——一场移动入口的掠夺,一次义无反顾的打车运动。两位以往出双入对的闺蜜,最终因一个移动帅哥的出现而决裂,显然O2O的魅力程度已经让已为股市妇的腾讯和待字闺中的阿里神魂颠倒。什么是O2O?官方语言就是是指将线下的商务机构与互联网结合,让互联网成为线下交易的前台,互联网与现实世界的交互。这太过抽象的内容确实不好理解,用一段简单的话解释就是:一个人起床的时候拿手机看了下时间,然后用麦当劳的App点了一份外卖到公司,再用快旳打车叫了辆车去公司,下车的时候用支付付宝付款便宜了10元钱,到办公室早餐送到又用支付宝付了款,下班前在微信上团购了电影票,与朋友一起不用排队看了场电影,然后在大众点评上选了家不错的餐馆一起吃饭,然后用支付宝的AA制付款完,天色已晚交通工具只剩下地铁,打开百度地图找到最近的地铁口,坐车回家。这就是一个坐班族一天的O2O生活,当然除了这些还有出差旅游、娱乐等活动都已经可以在实现网上、线下的连接。而阿里与腾讯都是线上的

  • 腾讯云腾讯特效SDKiOS

    集成准备1.下载并解压Demo包,将Demo工程中的xmagic模块(bundle,XmagicIconRes,Xmagic文件夹)导入到实际项目工程中。2.导入SDK目录中的libpag.framework,Masonry.framework,XMagic.framework,YTCommonXMagic.framework。3.framework签名General-->Masonry.framework和libpag.framework选Embed&Sign。4.将BundleID修改成与申请的测试授权一致。开发者环境要求开发工具XCode11及以上:AppStore或单击下载地址。建议运行环境:设备要求:iPhone5及以上;iPhone6及以下前置摄像头最多支持到720p,不支持1080p。系统要求:iOS10.0及以上。C/C++层开发环境XCode默认C++环境。类型依赖库系统依赖库AccelerateAssetsLibraryAVFoundationCoreMediaCoreFoundationCoreMLFoundationJavaScriptCoreli

  • 主线程异常会导致 JVM 退出?

      大家好,我是坤哥 上周线程崩溃为什么不会导致JVM崩溃在其他平台发出后,有一位小伙伴留言说有个地方不严谨 他认为如果JVM中的主线程异常没有被捕获,JVM还是会崩溃,那么这个说法是否正确呢,我们做个试验看看结果是否是他说的这样 public class Test {    public static void main(String[] args) {        TestThread testThread = new TestThread();        TestThread.start();        Integer p = null

  • 抽象类和接口区别

     

  • 安装 RabbitMQ

      Ubuntu16.04安装RabbitMQ #1更新 $sudoapt-getupdate $sudoapt-getupgrade 复制   #2安装Erlang $cd/tmp $wgethttp://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc $sudoapt-keyadderlang_solutions.asc $sudoapt-getupdate $sudoapt-getinstallerlang $sudoapt-getinstallerlang-nox 复制   #3安装RabbitMQ 添加RabbitMQ仓库源: $sudovim/etc/apt/sources.list 复制   在文件中添加一行: debhttp://www.rabbitmq.com/debian/testingmain 复制   添加密钥: $cd/tmp $wgethttps://www.rabbitmq.com/r

  • leetcode 每日一题 94. 二叉树的中序遍历

    递归 思路: 先遍历左子树,再访问根节点,再遍历右子树。 代码: #Definitionforabinarytreenode. #classTreeNode: #def__init__(self,x): #self.val=x #self.left=None #self.right=None classSolution: definorderTraversal(self,root:TreeNode)->List[int]: defhelper(root,res): ifroot: ifroot.left: helper(root.left,res) res.append(root.val) ifroot.right: helper(root.right,res) res=[] helper(root,res) returnres复制 迭代 思路: 创建一个栈,从根节点遍历左子树,将对应根节点和左子树压入栈中,直到左子树为空。此时取出栈顶节点,将对应节点的值记录,继续对取出节点的右子树进行压栈和取出记录操作,取出的栈顶元素如果右子树为空,则记录后继续取出栈顶节点。 代码:

  • 模板库

    缺省源 -Wall-Wextra-Wunused-Wl,--stack=1024000000--std=c++11-O0-DLOCAL 复制 #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<ctime> #include<assert.h> #define_INT_INF((int)0x3f3f3f3f) #define_UINT_MAX((unsignedint)0xffffffff) #define_INT_MAX((int)0x7fffffff) #define_LL_INF((longlong)0x3f3f3f3f3f3f3f3f) #define_ULL_MAX((unsignedlonglong)0xffffffffffffffff) #define_LL_MAX((longlong)0x7fffffffffffffff) namespaceFastIO{ #ifdefLOCAL #def

  • 远程文件共享/搭建网盘:利用FRP将本地的FTP服务映射到公网

    FRP(FastReverseProxy)Github项目:https://github.com/fatedier/frp/ FRP独立页面:https://gofrp.org/ FRP帮助文档:https://gofrp.org/docs/ 参考博客: https://blog.csdn.net/shile/article/details/74392125   https://blog.csdn.net/deng_xj/article/details/89120458   操作时请注意本地数据安全性、传输保密性和公网服务器安全。 原理图:      说明: PS表示公网服务器,拥有其他设备都可访问的ip地址,这里用了阿里云服务器,OS=Cent7.3; LF表示本地文件服务器,需要能访问互联网,以与PS建立FRP关系、传输FTP到用户的数据,这里使用FileZillaServer; LG表示LF的网关,需要给LF提供固定的本地ip地址,例如通过dhcp为LF分配局域网地址192.168.1.17; CL表示受到服务的设备,

  • thinkphp微信浏览器内拉起微信支付

      vendor/wxpay/pay.php复制 <?php /* +-----------------------------------+ |微信支付类| +-----------------------------------+ */ require_once'config.php'; classpay{   public$params=array();   private$url='https://api.mch.weixin.qq.com/pay/unifiedorder';   private$prepay_id;//统一下单号   /**   *   *获取jsapi支付的参数   *@paramarray$UnifiedOrderResult统一支付接口返回的数据   *@throwsWxPayException   *@returnjson数据,可直接填入js函数作为参数   */   publicfunctionGetJsApiParameters()   {     $this->doneOrder();     $timeStam

  • vue3后台管理系统(模板)

    ?系统简介 此管理系统是基于Vite2和Vue3.0构建生成的后台管理系统。目的在于学习vite和vue3等新技术,以便于后续用于实际开发工作中; 本文章将从管理系统页面布局、vue路由鉴权、vuex状态管理、数据持久化、用户信息加密等方面进行介绍和记录; 这也是我边学习边实践的过程,此次记录一是方便自己日后开发过程中有用到时候便于借鉴和复习,再次是为了初学vue3和尝试上手vite2和vue3搭建管理系统的小伙伴提供一些学习方法和技术点; 本Vue后台管理系统使用的技术点主要有:vite2、vue3、vue-router4.x、vuex4.x、vuex-persistedstate(vuex数据持久化)、ElementPlus等。 ?用户登录 登录页面代码 <template> <divclass="login"> <el-cardclass="login_center"> <template#header> <divclass="card_header"> <span>用户登录</span>

  • 玩转X-CTR100 | STM32F4 l X-Assistant串口助手控制功能

    我造轮子,你造车,创客一起造起来!塔克创新资讯【塔克社区www.xtark.cn】【塔克博客www.cnblogs.com/xtark/】      X-CTR100控制器配套的X-Assistant串口调试助手的控制功能,实现与X-CTR100控制器的交互控制,控制LED灯的亮灭和舵机旋转角度。通过本教程可以学习上位机控制逻辑和串口通信协议相关知识。 原理 X-CTR100控制器配套X-Assistant串口调试助手软件。为方便PC与X-CTR100交互,增加控制功能,实现上下左右四个方向键和两个滑块交互,软件界面如下图所示。 控制协议如下所示,基于帧编码方式实现,具有帧头、帧校验内容。软件帮助也具有协议内容介绍。 -协议内容:0xAA+0x55+帧长度+帧编码+按键+滑块1+滑块2+校验和 -帧长度:固定值0x08 -帧编码:固定值0x11 -按键:无按键-0x00,上-0x01,下-0x02,左-0x04,右-0x08 -滑块1:数值范围0~0xFF -滑块2:数值范围0~0xFF -校验和:前面7位数据累加和的低8位 根据

  • maven导入多模块项目

    一、SVN上Maven多模块项目结构 使用eclipse导入SVN上的Maven多模块项目 Maven多模块项目所在SVN目录 二、eclipse通过SVN导入到工作空间 工作空间位于F:/HPCWorkspace   2.1File->Import,选择从SVN检出项目   2.2选择/新建SVN资源库位置   如果资源库还没创建好,选择创建新的资源库位置,如果已经创建好资源库了,那么选择使用现有的资源库位置  不存在的话新建 存在的话,选择已经存在的资源库     2.3选择要从SVN检出的文件夹   2.4重命名项目名称       2.5选择项目位置   2.6删除项目。注:不要勾选Deleteprojectcontentsondisk!    三、eclipse导入Maven多模块项目   3.1File->Import选择MavenExistingMavenProject   

  • TTS异步+同步

    微软TTS使用说明   一.SAPISDK的介绍 SAPI,全称是TheMicrosoftSpeechAPI。就是微软的语音API。由WindowsSpeechSDK提供。       WindowsSpeechSDK包含语音识别SR引擎和语音合成SS引擎两种语音引擎。语音识别引擎用于识别语音命令,调用接口完成某个功能,实现语音控制。语音合成引擎用于将文字转换成语音输出。 目前最常用的WindowsSpeechSDK版本有三种:5.1、5.3和5.4。       WindowsSpeechSDK5.1版本支持xp系统和server2003系统,需要下载安装。XP系统默认只带了个MicrosoftSam英文男声语音库,想要中文引擎就需要安装WindowsSpeechSDK5.1。       WindowsSpeechSDK5.3版本支持Vista系统和Server2008系统,已经集成到系统里。V

相关推荐

推荐阅读