Fabric-ca server端初始化过程源码分析
本文从Fabric-ca源码入手,简单分析server启动时的过程。Fabric-ca源码可以从github.com下载,本文以v1.4.6为例进行简单分析。
Fabric-ca是有go语言编写的,与C/C++类似,程序都有一个mian()函数,不同的是,go的main函数必须存在于package main
中:
// fabric-ca/cmd/fabric-ca-server/main.go
package main
import "os"
var (
blockingStart = true
)
// The fabric-ca server main
func main() {
if err := RunMain(os.Args); err != nil {
os.Exit(1)
}
}
// RunMain is the fabric-ca server main
func RunMain(args []string) error {
// Save the os.Args
saveOsArgs := os.Args
os.Args = args
cmdName := ""
if len(args) > 1 {
cmdName = args[1]
}
scmd := NewCommand(cmdName, blockingStart)
// Execute the command
err := scmd.Execute()
// Restore original os.Args
os.Args = saveOsArgs
return err
}
从上述代码中可以看出,程序执行时会调用RunMain()
函数,而在RunMain()
中调用NewCommand()
来生成一个*ServerCmd
对象,之后调用该对象的Execute()
方法。那么接下来就是分析NewCommand()
函数了。
// fabric-ca/cmd/fabric-ca-server/servercmd.go
// ServerCmd encapsulates cobra command that provides command line interface
// for the Fabric CA server and the configuration used by the Fabric CA server
type ServerCmd struct {
// name of the fabric-ca-server command (init, start, version)
name string
// rootCmd is the cobra command
rootCmd *cobra.Command
// My viper instance
myViper *viper.Viper
// blockingStart indicates whether to block after starting the server or not
blockingStart bool
// cfgFileName is the name of the configuration file
cfgFileName string
// homeDirectory is the location of the server's home directory
homeDirectory string
// serverCfg is the server's configuration
cfg *lib.ServerConfig
}
// NewCommand returns new ServerCmd ready for running
func NewCommand(name string, blockingStart bool) *ServerCmd {
s := &ServerCmd{
name: name,
blockingStart: blockingStart,
myViper: viper.New(),
}
s.init()
return s
}
可以看到,在NewCommand()
函数中只有三个操作:构造一个*ServerCmd
的对象、调用它的init()
函数,然后返回该对象。
在*ServerCmd.init()
函数中:
// fabric-ca/cmd/fabric-ca-server/servercmd.go
// init initializes the ServerCmd instance
// It intializes the cobra root and sub commands and
// registers command flgs with viper
func (s *ServerCmd) init() {
// root command
rootCmd := &cobra.Command{
Use: cmdName,
Short: longName,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
err := s.configInit()
if err != nil {
return err
}
cmd.SilenceUsage = true
util.CmdRunBegin(s.myViper)
return nil
},
}
s.rootCmd = rootCmd
// initCmd represents the server init command
initCmd := &cobra.Command{
Use: "init",
Short: fmt.Sprintf("Initialize the %s", shortName),
Long: "Generate the key material needed by the server if it doesn't already exist",
}
initCmd.RunE = func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return errors.Errorf(extraArgsError, args, initCmd.UsageString())
}
err := s.getServer().Init(false)
if err != nil {
util.Fatal("Initialization failure: %s", err)
}
log.Info("Initialization was successful")
return nil
}
s.rootCmd.AddCommand(initCmd)
// startCmd represents the server start command
startCmd := &cobra.Command{
Use: "start",
Short: fmt.Sprintf("Start the %s", shortName),
}
startCmd.RunE = func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return errors.Errorf(extraArgsError, args, startCmd.UsageString())
}
err := s.getServer().Start()
if err != nil {
return err
}
return nil
}
s.rootCmd.AddCommand(startCmd)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Prints Fabric CA Server version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Print(metadata.GetVersionInfo(cmdName))
},
}
s.rootCmd.AddCommand(versionCmd)
s.registerFlags()
}
// registerFlags registers command flags with viper
func (s *ServerCmd) registerFlags() {
// Get the default config file path
cfg := util.GetDefaultConfigFile(cmdName)
// All env variables must be prefixed
s.myViper.SetEnvPrefix(envVarPrefix)
s.myViper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Set specific global flags used by all commands
pflags := s.rootCmd.PersistentFlags()
pflags.StringVarP(&s.cfgFileName, "config", "c", "", "Configuration file")
pflags.MarkHidden("config")
// Don't want to use the default parameter for StringVarP. Need to be able to identify if home directory was explicitly set
pflags.StringVarP(&s.homeDirectory, "home", "H", "", fmt.Sprintf("Server's home directory (default \"%s\")", filepath.Dir(cfg)))
util.FlagString(s.myViper, pflags, "boot", "b", "",
"The user:pass for bootstrap admin which is required to build default config file")
// Register flags for all tagged and exported fields in the config
s.cfg = &lib.ServerConfig{}
tags := map[string]string{
"help.csr.cn": "The common name field of the certificate signing request to a parent fabric-ca-server",
"help.csr.serialnumber": "The serial number in a certificate signing request to a parent fabric-ca-server",
"help.csr.hosts": "A list of comma-separated host names in a certificate signing request to a parent fabric-ca-server",
}
err := util.RegisterFlags(s.myViper, pflags, s.cfg, nil)
if err != nil {
panic(err)
}
caCfg := &lib.CAConfig{}
err = util.RegisterFlags(s.myViper, pflags, caCfg, tags)
if err != nil {
panic(err)
}
}
// Configuration file is not required for some commands like version
func (s *ServerCmd) configRequired() bool {
return s.name != version
}
// getServer returns a lib.Server for the init and start commands
func (s *ServerCmd) getServer() *lib.Server {
return &lib.Server{
HomeDir: s.homeDirectory,
Config: s.cfg,
BlockingStart: s.blockingStart,
CA: lib.CA{
Config: &s.cfg.CAcfg,
ConfigFilePath: s.cfgFileName,
},
}
}
在函数中,首先是创建了一个*cobra.Command
的对象,在该对象中的主要有两个操作:执行配置初始化以及util.CmdRunBegin()
操作,之后将该对象赋值给s.rootCmd
;接下来又创建了三个*cobra.Command
对象:initCmd
、startCmd
和versionCmd
,分别是服务初始化命令、启动命令和获取server版本命令,创建完成后将这三个命令使用AddCommand()
添加到s.rootCmd
,随后执行s.registerFlags()
操作。registerFlags()
函数中主要是注册一些命令行参数,这里就不细究了。在 initCmd
和startCmd
中都用到了getServer()
函数,该函数返回一个*libServer
对象:
// fabric-ca/lib/server.go
// Server is the fabric-ca server
type Server struct {
// The home directory for the server.
HomeDir string
// BlockingStart determines if Start is blocking.
// It is non-blocking by default.
BlockingStart bool
// The server's configuration
Config *ServerConfig
// Metrics are the metrics that the server tracks for API calls.
Metrics servermetrics.Metrics
// Operations is responsible for the server's operation information.
Operations operationsServer
// CA is the default certificate authority for the server.
CA
// metrics for database requests
dbMetrics *db.Metrics
// mux is used to server API requests
mux *gmux.Router
// listener for this server
listener net.Listener
// An error which occurs when serving
serveError error
// caMap is a list of CAs by name
caMap map[string]*CA
// caConfigMap is a list CA configs by filename
caConfigMap map[string]*CAConfig
// levels currently supported by the server
levels *dbutil.Levels
wait chan bool
mutex sync.Mutex
}
initCmd
在initCmd
中,调用了*libServer
对象Init()
函数,该函数中调用了init()
函数来执行server的初始化:
// init initializses the server leaving the DB open
func (s *Server) init(renew bool) (err error) {
s.Config.Operations.Metrics = s.Config.Metrics
s.Operations = operations.NewSystem(s.Config.Operations)
s.initMetrics()
serverVersion := metadata.GetVersion()
err = calog.SetLogLevel(s.Config.LogLevel, s.Config.Debug)
if err != nil {
return err
}
log.Infof("Server Version: %s", serverVersion)
s.levels, err = metadata.GetLevels(serverVersion)
if err != nil {
return err
}
log.Infof("Server Levels: %+v", s.levels)
s.mux = gmux.NewRouter()
// Initialize the config
err = s.initConfig()
if err != nil {
return err
}
// Initialize the default CA last
err = s.initDefaultCA(renew)
if err != nil {
return err
}
// Successful initialization
return nil
}
在init()
函数中,首先调用initMetrics()
函数来初始化一系列的系统参数,之后是initConfig()
来初始化配置,initDefaultCA()
来初始化CA信息。
initConfig()
函数里面内容很简单,这里就不赘述了。
initDefaultCA()
函数中首先调用initCA()
来创建一个CA,之后再addCA()
到server中。在initCA()
中,
// fabric-ca/lib/ca.go
// CA represents a certificate authority which signs, issues and revokes certificates
type CA struct {
// The home directory for the CA
HomeDir string
// The CA's configuration
Config *CAConfig
// The file path of the config file
ConfigFilePath string
// The database handle used to store certificates and optionally
// the user registry information, unless LDAP it enabled for the
// user registry function.
db db.FabricCADB
// The crypto service provider (BCCSP)
csp bccsp.BCCSP
// The certificate DB accessor
certDBAccessor *CertDBAccessor
// The user registry
registry user.Registry
// The signer used for enrollment
enrollSigner signer.Signer
// Idemix issuer
issuer idemix.Issuer
// The options to use in verifying a signature in token-based authentication
verifyOptions *x509.VerifyOptions
// The attribute manager
attrMgr *attrmgr.Mgr
// The tcert manager for this CA
tcertMgr *tcert.Mgr
// The key tree
keyTree *tcert.KeyTree
// The server hosting this CA
server *Server
// DB levels
levels *dbutil.Levels
// CA mutex
mutex sync.Mutex
}
...
func initCA(ca *CA, homeDir string, config *CAConfig, server *Server, renew bool) error {
ca.HomeDir = homeDir
ca.Config = config
ca.server = server
err := ca.init(renew)
if err != nil {
return err
}
log.Debug("Initializing Idemix issuer...")
ca.issuer = idemix.NewIssuer(ca.Config.CA.Name, ca.HomeDir,
&ca.Config.Idemix, ca.csp, idemix.NewLib())
err = ca.issuer.Init(renew, ca.db, ca.levels)
if err != nil {
return errors.WithMessage(err, fmt.Sprintf("Failed to initialize Idemix issuer for CA '%s'", err.Error()))
}
return nil
}
调用*CA.init()
函数来初始化一个CA服务,之后使用idemix.NewIssuer()
实例化一个*CA.issuer
对象:
//fabric-ca/lib/server/idemix/issuer.go
type issuer struct {
name string
homeDir string
cfg *Config
idemixLib Lib
db db.FabricCADB
csp bccsp.BCCSP
// The Idemix credential DB accessor
credDBAccessor CredDBAccessor
// idemix issuer credential for the CA
issuerCred IssuerCredential
// A random number used in generation of Idemix nonces and credentials
idemixRand *amcl.RAND
rc RevocationAuthority
nm NonceManager
isInitialized bool
mutex sync.Mutex
}
// NewIssuer returns an object that implements Issuer interface
func NewIssuer(name, homeDir string, config *Config, csp bccsp.BCCSP, idemixLib Lib) Issuer {
issuer := issuer{name: name, homeDir: homeDir, cfg: config, csp: csp, idemixLib: idemixLib}
return &issuer
}
func (i *issuer) Init(renew bool, db db.FabricCADB, levels *dbutil.Levels) error {
if i.isInitialized {
return nil
}
i.mutex.Lock()
defer i.mutex.Unlock()
// After obtaining a lock, check again to see if issuer has been initialized by another thread
if i.isInitialized {
return nil
}
if db == nil || reflect.ValueOf(db).IsNil() || !db.IsInitialized() {
log.Debugf("Returning without initializing Idemix issuer for CA '%s' as the database is not initialized", i.Name())
return nil
}
i.db = db
err := i.cfg.init(i.homeDir)
if err != nil {
return err
}
err = i.initKeyMaterial(renew)
if err != nil {
return err
}
i.credDBAccessor = NewCredentialAccessor(i.db, levels.Credential)
log.Debugf("Intializing revocation authority for issuer '%s'", i.Name())
i.rc, err = NewRevocationAuthority(i, levels.RAInfo)
if err != nil {
return err
}
log.Debugf("Intializing nonce manager for issuer '%s'", i.Name())
i.nm, err = NewNonceManager(i, &wallClock{}, levels.Nonce)
if err != nil {
return err
}
i.isInitialized = true
return nil
}
随后调用issuer.Init()
函数来初始化idemix证书服务。
startCmd
在startCmd()
命令中,调用了*lib.Server.Start()
函数:
// fabric-ca/lib/server.go
// Start the fabric-ca server
func (s *Server) Start() (err error) {
log.Infof("Starting server in home directory: %s", s.HomeDir)
s.serveError = nil
if s.listener != nil {
return errors.New("server is already started")
}
// Initialize the server
err = s.init(false)
if err != nil {
err2 := s.closeDB()
if err2 != nil {
log.Errorf("Close DB failed: %s", err2)
}
return err
}
// Register http handlers
s.registerHandlers()
log.Debugf("%d CA instance(s) running on server", len(s.caMap))
// Start operations server
err = s.startOperationsServer()
if err != nil {
return err
}
err = s.Operations.RegisterChecker("server", s)
if err != nil {
return nil
}
// Start listening and serving
err = s.listenAndServe()
if err != nil {
err2 := s.closeDB()
if err2 != nil {
log.Errorf("Close DB failed: %s", err2)
}
return err
}
return nil
}
其中再次调用了init()
函数,但这次参数为false
,表明这次不用重新初始化默认的CA服务了。之后调用了registerHandlers()
函数,用来注册所有提供服务的终端句柄。接着调用startOperationsServer()
来开启服务:
// operationsServer defines the contract required for an operations server
type operationsServer interface {
metrics.Provider
Start() error
Stop() error
Addr() string
RegisterChecker(component string, checker healthz.HealthChecker) error
}
func (s *Server) startOperationsServer() error {
err := s.Operations.Start()
if err != nil {
return err
}
return nil
}
在startOperationsServer()
中,使用了operationsServer.Start()
,在operationsServer
中定义了server提供操作接口。
之后调用了operationsServer.RegisterChecker()
接口来检查server的健康状态。随后,调用了*Server.listenAndServe()
开始监听和提供服务:
// Starting listening and serving
func (s *Server) listenAndServe() (err error) {
var listener net.Listener
var clientAuth tls.ClientAuthType
var ok bool
c := s.Config
// Set default listening address and port
if c.Address == "" {
c.Address = DefaultServerAddr
}
if c.Port == 0 {
c.Port = DefaultServerPort
}
addr := net.JoinHostPort(c.Address, strconv.Itoa(c.Port))
var addrStr string
if c.TLS.Enabled {
log.Debug("TLS is enabled")
addrStr = fmt.Sprintf("https://%s", addr)
// If key file is specified and it does not exist or its corresponding certificate file does not exist
// then need to return error and not start the server. The TLS key file is specified when the user
// wants the server to use custom tls key and cert and don't want server to auto generate its own. So,
// when the key file is specified, it must exist on the file system
if c.TLS.KeyFile != "" {
if !util.FileExists(c.TLS.KeyFile) {
return fmt.Errorf("File specified by 'tls.keyfile' does not exist: %s", c.TLS.KeyFile)
}
if !util.FileExists(c.TLS.CertFile) {
return fmt.Errorf("File specified by 'tls.certfile' does not exist: %s", c.TLS.CertFile)
}
log.Debugf("TLS Certificate: %s, TLS Key: %s", c.TLS.CertFile, c.TLS.KeyFile)
} else if !util.FileExists(c.TLS.CertFile) {
// TLS key file is not specified, generate TLS key and cert if they are not already generated
err = s.autoGenerateTLSCertificateKey()
if err != nil {
return fmt.Errorf("Failed to automatically generate TLS certificate and key: %s", err)
}
}
cer, err := util.LoadX509KeyPair(c.TLS.CertFile, c.TLS.KeyFile, s.csp)
if err != nil {
return err
}
if c.TLS.ClientAuth.Type == "" {
c.TLS.ClientAuth.Type = defaultClientAuth
}
log.Debugf("Client authentication type requested: %s", c.TLS.ClientAuth.Type)
authType := strings.ToLower(c.TLS.ClientAuth.Type)
if clientAuth, ok = clientAuthTypes[authType]; !ok {
return errors.New("Invalid client auth type provided")
}
var certPool *x509.CertPool
if authType != defaultClientAuth {
certPool, err = LoadPEMCertPool(c.TLS.ClientAuth.CertFiles)
if err != nil {
return err
}
}
config := &tls.Config{
Certificates: []tls.Certificate{*cer},
ClientAuth: clientAuth,
ClientCAs: certPool,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
CipherSuites: stls.DefaultCipherSuites,
}
listener, err = tls.Listen("tcp", addr, config)
if err != nil {
return errors.Wrapf(err, "TLS listen failed for %s", addrStr)
}
} else {
addrStr = fmt.Sprintf("http://%s", addr)
listener, err = net.Listen("tcp", addr)
if err != nil {
return errors.Wrapf(err, "TCP listen failed for %s", addrStr)
}
}
s.listener = listener
log.Infof("Listening on %s", addrStr)
err = s.checkAndEnableProfiling()
if err != nil {
s.closeListener()
return errors.WithMessage(err, "TCP listen for profiling failed")
}
// Start serving requests, either blocking or non-blocking
if s.BlockingStart {
return s.serve()
}
s.wait = make(chan bool)
go s.serve()
return nil
}
func (s *Server) serve() error {
listener := s.listener
if listener == nil {
// This can happen as follows:
// 1) listenAndServe above is called with s.BlockingStart set to false
// and returns to the caller
// 2) the caller immediately calls s.Stop, which sets s.listener to nil
// 3) the go routine runs and calls this function
// So this prevents the panic which was reported in
// in https://jira.hyperledger.org/browse/FAB-3100.
return nil
}
s.serveError = http.Serve(listener, s.mux)
log.Errorf("Server has stopped serving: %s", s.serveError)
s.closeListener()
err := s.closeDB()
if err != nil {
log.Errorf("Close DB failed: %s", err)
}
if s.wait != nil {
s.wait <- true
}
return s.serveError
}
从上面的函数可以看出,Fabric-ca支持TLS服务。
在函数中调用checkAndEnableProfiling()
来检查FABRIC_CA_SERVER_PROFILE_PORT
是否可用:
// checkAndEnableProfiling checks for FABRIC_CA_SERVER_PROFILE_PORT env variable
// if it is set, starts listening for profiling requests at the port specified
// by the environment variable
func (s *Server) checkAndEnableProfiling() error {
// Start listening for profile requests
pport := os.Getenv(fabricCAServerProfilePort)
if pport != "" {
iport, err := strconv.Atoi(pport)
if err != nil || iport < 0 {
log.Warningf("Profile port specified by the %s environment variable is not a valid port, not enabling profiling",
fabricCAServerProfilePort)
} else {
addr := net.JoinHostPort(s.Config.Address, pport)
listener, err1 := net.Listen("tcp", addr)
log.Infof("Profiling enabled; listening for profile requests on port %s", pport)
if err1 != nil {
return err1
}
go func() {
log.Debugf("Profiling enabled; waiting for profile requests on port %s", pport)
err := http.Serve(listener, nil)
log.Errorf("Stopped serving for profiling requests on port %s: %s", pport, err)
}()
}
}
return nil
}
最后调用server()
,启动服务。
至此,Fabric-ca server端的启动过程就完成了。当然,文章中省略了很多细节,比如服务的初始化过程、默认CA的生成过程、idemix的issuer证书生成过程等等,这些过程就需要各位自行了解了。
Fabric-ca server端初始化过程源码分析的更多相关文章
- Bootstrap初始化过程源码分析--netty客户端的启动
Bootstrap初始化过程 netty的客户端引导类是Bootstrap,我们看一下spark的rpc中客户端部分对Bootstrap的初始化过程 TransportClientFactory.cr ...
- A2dp初始化流程源码分析
蓝牙启动的时候,会涉及到各个profile 的启动.这篇文章分析一下,蓝牙中a2dp profile的初始化流程. 我们从AdapterState.java中对于USER_TURN_ON 消息的处理说 ...
- A2dp sink 初始化流程源码分析
A2dp sink的初始化流程和A2dp 的初始化流程,基本一样,这里做简单分析.这里分析的android的版本是Android O. 我们先从service的启动说起吧. 下面 是启动的时候的log ...
- Netty入门一:服务端应用搭建 & 启动过程源码分析
最近周末也没啥事就学学Netty,同时打算写一些博客记录一下(写的过程理解更加深刻了) 本文主要从三个方法来呈现:Netty核心组件简介.Netty服务端创建.Netty启动过程源码分析 如果你对Ne ...
- SpringSecurity 初始化流程源码
SpringSecurity 初始化流程源码 本篇主要讲解 SpringSecurity初始化流程的源码部分,包括核心的 springSecurityFilterChain 是如何创建的,以及在介绍哪 ...
- [Android]从Launcher开始启动App流程源码分析
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5017056.html 从Launcher开始启动App流程源码 ...
- Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)
上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...
- Dubbo消费方服务调用过程源码分析
参考:dubbo消费方服务调用过程源码分析dubbo基于spring的构建分析Dubbo概述--调用过程dubbo 请求调用过程分析dubbo集群容错机制代码分析1dubbo集群容错策略的代码分析2d ...
- [Android]Android系统启动流程源码分析
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5013863.html Android系统启动流程源码分析 首先 ...
- Android系统默认Home应用程序(Launcher)的启动过程源码分析
在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还须要有一个Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home应 ...
随机推荐
- 对话 BitSail Contributor | 吴畅:从好奇,到深入
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 2022 年 10 月,字节跳动 BitSail 数据引擎正式开源.同期,社区推出 Contributor 激励计 ...
- NOKOV度量光学动作捕捉系统工作流程
如果你对影视.动画或者游戏有一定关注,相信你一定听说过"动作捕捉".事实上,无论是屏幕中的战场,还是真实的军事领域,从2K游戏中的虚拟球员,到医疗.康复.运动领域的专业研究:从机器 ...
- 23年校招Java开发同花顺、滴滴等面经
前言 已经工作近半年时间了,最近突然翻到这份面经,于是想整理一下一些面试的经验,大中小公司都有 青书一面 50min 数据库.java基础. Cas机制. Tcp/udp区别 堆排序介绍,答错了,弄成 ...
- django DRF
博客目录 web应用模式 api接口 接口测试工具postman restful规范 drf安装 序列化和反序列化 CBV源码分析 drf之APIView分析 drf之Request对象分析 drf- ...
- CodeForces - 469A I Wanna Be the Guy
There is a game called "I Wanna Be the Guy", consisting of n levels. Little X and his frie ...
- Android NativeCrash 捕获与解析
Android 开发中,NE一直是不可忽略却又异常难解的一个问题,原因是这里面涉及到了跨端开发和分析,需要同时熟悉 Java,C&C++,并且需要熟悉 NDK开发,并且解决起来不像 Java异 ...
- vue3引入使用svg图标
vue3使用svg图标 安装 // 通过命令安装2个插件 npm i vite-plugin-svg-icons -D npm i fast-glob -D 在vue.config.js中配置 //v ...
- spring管理实务有几种方式
一:事务认识 大家所了解的事务Transaction,它是一些列严密操作动作,要么都操作完成,要么都回滚撤销.Spring事务管理基于底层数据库本身的事务处理机制.数据库事务的基础,是掌握Spring ...
- SpringBoot发布https服务
一.生成SSL证书 1.进入本地jdk的路径 cd D:\Program\jdk1.8.0_77\jre\lib\security cmd窗口生成证书HSoftTiger.keystore到D盘 ke ...
- 二、Mycat安装
系列导航 一.Mycat实战---为什么要用mycat 二.Mycat安装 三.mycat实验数据 四.mycat垂直分库 五.mycat水平分库 六.mycat全局自增 七.mycat-ER分片 万 ...