Envoy 源码分析--程序启动过程

申明:本文的 Envoy 源码分析基于 Envoy1.10.0。

前面几章分析了 event事件底层网络, 但对创建服务的过程并没有串起来,只是分析了底层的网络公共库。这次我们分析下整个服务的创建过程。

初始化

main 入口

服务启动的总入口 main 函数,会先创建 MainCommon

int main(int argc, char** argv) {
... ...
std::unique_ptr<Envoy::MainCommon> main_common; try {
main_common = std::make_unique<Envoy::MainCommon>(argc, argv);
... ...
}

MainCommon 初始化

MainCommon 在构造函数时,会先解析程序的参数,然后再调用 MainCommonBase

MainCommon::MainCommon(int argc, const char* const* argv)
: options_(argc, argv, &MainCommon::hotRestartVersion, spdlog::level::info),
base_(options_, real_time_system_, default_test_hooks_,prod_component_factory_,
std::make_unique<Runtime::RandomGeneratorImpl>(),platform_impl_.threadFactory(),
platform_impl_.fileSystem()) {}

OptionsImpl 使用开源的 tclap 解析库。OptionsImpl 支持很多参数配置,具体的参数配置参考 operation/cli

MainCommonBase 会初始化全局的参数,接着调用服务进行初始化。

MainCommonBase::MainCommonBase(... ...)
: options_(options), component_factory_(component_factory), thread_factory_(thread_factory),file_system_(file_system) {
//全局的或第三方库预先初始化
Thread::ThreadFactorySingleton::set(&thread_factory_);
ares_library_init(ARES_LIB_INIT_ALL);
Event::Libevent::Global::initialize();
RELEASE_ASSERT(Envoy::Server::validateProtoDescriptors(), "");
Http::Http2::initializeNghttp2Logging(); ... ... //初始化服务
server_ = std::make_unique<Server::InstanceImpl>(
options_, time_system, local_address, test_hooks, *restarter_, *stats_store_,
access_log_lock, component_factory, std::move(random_generator), *tls_, thread_factory_,
file_system_);

服务 InstanceImpl 初始化

InstanceImpl 主要是初始化 admin 管理服务,各个静态 XDS 的加载以及初始化服务。

InstanceImpl 在核心函数中先加载配置文件,通过参数 -c 配置。

void InstanceImpl::initialize(const Options& options,
Network::Address::InstanceConstSharedPtr local_address,
ComponentFactory& component_factory, TestHooks& hooks) {
... ...
//加载Bootstrap
InstanceUtil::loadBootstrapConfig(bootstrap_, options, *api_);
bootstrap_config_update_time_ = time_source_.systemTime();
... ...
}

配置文件是一个 json 格式,包括以下几项:

  • node:节点标识,会在管理服务器中呈现,用于标识目的(例如,头域中生成相应的字段)。
  • static_resources:指定静态资源配置。
  • dynamic_resources:动态发现服务源配置。
  • cluster_manager:该服务所有的上游群集的群集管理配置。
  • hds_config:服务健康检查配置。
  • flags_path:用于启动文件标志的路径。
  • stats_sinks:统计汇总设置
  • stats_config:配置内部处理统计信息。
  • stats_flush_interval:刷新到统计信息服务的周期时间。出于性能方面的考虑,Envoy锁定计数器,并且只是周期性地刷新计数器和计量器。如果未指定,则默认值为5000毫秒(5秒)。
  • watchdog:看门狗配置。
  • tracing:配置外置的追踪服务程序。如果没有指定,则不会执行追踪。
  • runtime:配置运行时配置分发服务程序。
  • admin: 配置本地管理的HTTP服务。
  • overload_manager:过载管理配置(资源限制)。

各个项的具体作用和配置参考Bootstrap

加载完配置项,接着会启动 admin 本地的HTTP服务。

初始化 admin 服务

admin 先创建一个 AdminImpl,在构造函数里初始化 URI。

AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server)
: server_(server), profile_path_(profile_path),
... ...
handlers_{
{"/", "Admin home page", MAKE_ADMIN_HANDLER(handlerAdminHome), false, false},
{"/certs", "print certs on machine", MAKE_ADMIN_HANDLER(handlerCerts), false, false},
// 导出 cluster 统计信息
{"/clusters", "upstream cluster status", MAKE_ADMIN_HANDLER(handlerClusters), false,
false},
// 导出配置文件
{"/config_dump", "dump current Envoy configs (experimental)",
MAKE_ADMIN_HANDLER(handlerConfigDump), false, false},
// 导出连接统计信息
{"/contention", "dump current Envoy mutex contention stats (if enabled)",
MAKE_ADMIN_HANDLER(handlerContention), false, false},
... ...
admin_filter_chain_(std::make_shared<AdminFilterChain>()) {}

接着启动一个服务。

admin_->startHttpListener(initial_config.admin().accessLogPath(), options.adminAddressPath(),
initial_config.admin().address(),
stats_store_.createScope("listener.admin."));

在启动服务函数内部会创建 TcpListenSocket 和 AdminListener。

void AdminImpl::startHttpListener(const std::string& access_log_path,
const std::string& address_out_path,
Network::Address::InstanceConstSharedPtr address,
Stats::ScopePtr&& listener_scope) {
... ...
socket_ = std::make_unique<Network::TcpListenSocket>(address, nullptr, true);
listener_ = std::make_unique<AdminListener>(*this, std::move(listener_scope));
... ...
}

初始化 TcpListenSocket 时会在内部创建一个 socket 后再进行 bind。

using TcpListenSocket = NetworkListenSocket<NetworkSocketTrait<Address::SocketType::Stream>>;

template <typename T> class NetworkListenSocket : public ListenSocketImpl {
public:
NetworkListenSocket(const Address::InstanceConstSharedPtr& address,
const Network::Socket::OptionsSharedPtr& options, bool bind_to_port)
// socket 创建
: ListenSocketImpl(address->socket(T::type), address) {
RELEASE_ASSERT(io_handle_->fd() != -1, ""); setPrebindSocketOptions(); setupSocket(options, bind_to_port);
} void ListenSocketImpl::setupSocket(const Network::Socket::OptionsSharedPtr& options, bool bind_to_port) {
setListenSocketOptions(options);
//准备进行绑定
if (bind_to_port) {
doBind();
}
} void ListenSocketImpl::doBind() {
//绑定
const Api::SysCallIntResult result = local_address_->bind(io_handle_->fd());
... ...
}

AdminListener 构造函数内只是参数的初始化。

AdminListener(AdminImpl& parent, Stats::ScopePtr&& listener_scope)
: parent_(parent), name_("admin"), scope_(std::move(listener_scope)),
stats_(Http::ConnectionManagerImpl::generateListenerStats("http.admin.", *scope_)) {}

做完 socket ,bind 后面就是进行 listen 处理。将 AdminListener 通过 handler 加入监听队列。handler 是在 InstanceImpl 的构造函数内初始化的。

InstanceImpl::InstanceImpl(... ...)
: handler_(new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher_)),
... ...{
} void InstanceImpl::initialize(... ...)
{
... ...
//将 AdminListener 加入 ConnectionHandler
if (initial_config.admin().address()) {
admin_->addListenerToHandler(handler_.get());
}
... ...
} void AdminImpl::addListenerToHandler(Network::ConnectionHandler* handler) {
// 这里的 listener_ 就是上面生成的 AdminListener
if (listener_) {
handler->addListener(*listener_);
}
}

在 addListener 内会新建一个 ActiveListener 内部类,先置为 disable 状态。

void ConnectionHandlerImpl::addListener(Network::ListenerConfig& config) {
ActiveListenerPtr l(new ActiveListener(*this, config));
if (disable_listeners_) {
l->listener_->disable();
}
listeners_.emplace_back(config.socket().localAddress(), std::move(l));
}

在 ActiveListener 构造函数内创建 listen,里面dispatcher 会创建回调。等有新连接到来时,会回调 onAccept.

ConnectionHandlerImpl::ActiveListener::ActiveListener(ConnectionHandlerImpl& parent,Network::ListenerConfig& config)
: ActiveListener(
parent,
// 创建listen
parent.dispatcher_.createListener(config.socket(), *this, config.bindToPort(),
config.handOffRestoredDestinationConnections()),
config) {} Network::ListenerPtr
DispatcherImpl::createListener(Network::Socket& socket, Network::ListenerCallbacks& cb, bool bind_to_port, bool hand_off_restored_destination_connections) {
ASSERT(isThreadSafe());
return Network::ListenerPtr{new Network::ListenerImpl(*this, socket, cb, bind_to_port,
hand_off_restored_destination_connections)};
} void ListenerImpl::setupServerSocket(Event::DispatcherImpl& dispatcher, Socket& socket) {
listener_.reset(
//创建 evconnlistener_new ,有连接回调listenCallback
evconnlistener_new(&dispatcher.base(), listenCallback, this, 0, -1, socket.ioHandle().fd()));
... ...
} void ListenerImpl::listenCallback(evconnlistener*, evutil_socket_t fd, sockaddr* remote_addr,int remote_addr_len, void* arg) {
... ... //回调ActiveListener的onAccept
listener->cb_.onAccept(
std::make_unique<AcceptedSocketImpl>(std::move(io_handle), local_address, remote_address),
listener->hand_off_restored_destination_connections_);
}

onAccept 对 Listern 过滤后,创建新连接。

void ConnectionHandlerImpl::ActiveListener::onAccept()
{
... ...
active_socket->continueFilterChain(true);
... ...
} void ConnectionHandlerImpl::ActiveSocket::continueFilterChain(bool success) {
... ...
listener_.newConnection(std::move(socket_));
... ...
} void ConnectionHandlerImpl::ActiveListener::onNewConnection() {
... ...
if (new_connection->state() != Network::Connection::State::Closed) {
ActiveConnectionPtr active_connection(
new ActiveConnection(*this, std::move(new_connection), parent_.dispatcher_.timeSource()));
active_connection->moveIntoList(std::move(active_connection), connections_);
parent_.num_connections_++;
}
... ...
}

这样,新连接就建立起来。

配置文件 XDS 初始化

初始化 Bootstrap 的 XDS 时,先初始化 static sercret,先初始化 cluster,接着初始化 listeners。

void MainImpl::initialize(... ...) {
//初始化secrets
const auto& secrets = bootstrap.static_resources().secrets();
for (ssize_t i = 0; i < secrets.size(); i++) {
ENVOY_LOG(debug, "static secret #{}: {}", i, secrets[i].name());
server.secretManager().addStaticSecret(secrets[i]);
} //初始化 cluster
bootstrap.static_resources().clusters().size());
cluster_manager_ = cluster_manager_factory.clusterManagerFromProto(bootstrap); // 初始化listeners
const auto& listeners = bootstrap.static_resources().listeners();
for (ssize_t i = 0; i < listeners.size(); i++) {
ENVOY_LOG(debug, "listener #{}:", i);
server.listenerManager().addOrUpdateListener(listeners[i], "", false);
}

初始化 cluster 会分两阶段初始化。先初始化非 EDS 部分,再初始化 EDS 部分。分两个阶段的初始化是因为在 v2 配置中每个 EDS 集群单独设置订阅。此订阅是 API 源时  群集将依赖于非 EDS 群集,因此必须首先初始化非 EDS 群集。cluster 的类型有 5个类型:

enum DiscoveryType {
// Refer to the :ref:`static discovery
STATIC = 0; // Refer to the :ref:`strict DNS discovery
STRICT_DNS = 1; // Refer to the :ref:`logical DNS discovery
LOGICAL_DNS = 2; // Refer to the :ref:`service discovery
EDS = 3; // Refer to the :ref:`original destination discovery
ORIGINAL_DST = 4;
}

cluster 初始化顺序:

非 EDS 部分 -> ADS -> EDS -> CDS

ClusterManagerImpl::ClusterManagerImpl(... ...) {
... ...
for (const auto& cluster : bootstrap.static_resources().clusters()) {
// 第一次初始化非 EDS 部分
if (cluster.type() != envoy::api::v2::Cluster::EDS) {
loadCluster(cluster, "", false, active_clusters_);
}
} // 初始化 ADS
if (bootstrap.dynamic_resources().has_ads_config()) {
ads_mux_ = std::make_unique<Config::GrpcMuxImpl>(
local_info,
Config::Utility::factoryForGrpcApiConfigSource(
*async_client_manager_, bootstrap.dynamic_resources().ads_config(), stats)
->create(),
main_thread_dispatcher,
*Protobuf::DescriptorPool::generated_pool()->FindMethodByName(
"envoy.service.discovery.v2.AggregatedDiscoveryService.StreamAggregatedResources"),
random_, stats_,
Envoy::Config::Utility::parseRateLimitSettings(bootstrap.dynamic_resources().ads_config()));
} else {
ads_mux_ = std::make_unique<Config::NullGrpcMuxImpl>();
} for (const auto& cluster : bootstrap.static_resources().clusters()) {
// 初始化 EDS
if (cluster.type() == envoy::api::v2::Cluster::EDS) {
loadCluster(cluster, "", false, active_clusters_);
}
} ... ...
//初始化 CDS
if (bootstrap.dynamic_resources().has_cds_config()) {
cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), *this);
init_helper_.setCds(cds_api_.get());
} else {
init_helper_.setCds(nullptr);
} }

上面都初始化完成后,再初始化 lds,最后再初始化 hds。

// 初始化lds
if (bootstrap_.dynamic_resources().has_lds_config()) {
listener_manager_->createLdsApi(bootstrap_.dynamic_resources().lds_config());
} //初始化hds
if (bootstrap_.has_hds_config()) {
const auto& hds_config = bootstrap_.hds_config();
async_client_manager_ = std::make_unique<Grpc::AsyncClientManagerImpl>(
*config_.clusterManager(), thread_local_, time_source_, *api_);
... ...
}

初始化 ListenerManager

ListenerManager 的初始化只是事先创建 worker。

ListenerManagerImpl::ListenerManagerImpl(... ...) {
... ...
// 创建worker子线程
for (uint32_t i = 0; i < server.options().concurrency(); i++) {
workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
}
} WorkerPtr ProdWorkerFactory::createWorker(OverloadManager& overload_manager) {
// 新建子线程,每个线种一个dispatchr
Event::DispatcherPtr dispatcher(api_.allocateDispatcher());
return WorkerPtr{new WorkerImpl(
tls_, hooks_, std::move(dispatcher),
Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
overload_manager, api_)};
} WorkerImpl::WorkerImpl(... ...)
: tls_(tls), hooks_(hooks), dispatcher_(std::move(dispatcher)), handler_(std::move(handler)), api_(api) {
tls_.registerThread(*dispatcher_, false);
overload_manager.registerForAction(
OverloadActionNames::get().StopAcceptingConnections, *dispatcher_,
[this](OverloadActionState state) { stopAcceptingConnectionsCb(state); });
}

启动

main 启动入口

main 函数调用 main_common

int main(int argc, char** argv) {
... ...
return main_common->run() ? EXIT_SUCCESS : EXIT_FAILURE;
}

main_common 进一步调用 InstanceImpl

bool MainCommonBase::run() {
switch (options_.mode()) {
case Server::Mode::Serve:
server_->run();
return true;
... ...
}

InstanceImpl 启用 loop 循环。

void InstanceImpl::run() {
auto run_helper = RunHelper(*this, options_, *dispatcher_, clusterManager(), access_log_manager_,
init_manager_, overloadManager(), [this] { startWorkers(); }); auto watchdog = guard_dog_->createWatchDog(api_->threadFactory().currentThreadId());
watchdog->startWatchdog(*dispatcher_);
dispatcher_->post([this] { notifyCallbacksForStage(Stage::Startup); });
dispatcher_->run(Event::Dispatcher::RunType::Block);
ENVOY_LOG(info, "main dispatch loop exited");
guard_dog_->stopWatching(watchdog);
watchdog.reset(); terminate();
}

服务启动流程

本地 HTTP 管理服务的启动流程上面已经分析过,现在讨论本地服务的启动流程(XDS 下发的暂不讨论)。

在 cluster 初始化的时候,加入 listener。

void MainImpl::initialize(... ...) {
... ...
// 初始化listeners
const auto& listeners = bootstrap.static_resources().listeners();
for (ssize_t i = 0; i < listeners.size(); i++) {
ENVOY_LOG(debug, "listener #{}:", i);
server.listenerManager().addOrUpdateListener(listeners[i], "", false);
}
}

addOrUpdateListener 创建 ListenerImpl,ListenerImpl 做 bind 操作。

  // 创建ListenerImpl
ListenerImplPtr new_listener(
new ListenerImpl(config, version_info, *this, name, modifiable, workers_started_, hash));
ListenerImpl& new_listener_ref = *new_listener;
... ... //bind 地址将 socket 关联ListenerImpl
new_listener->setSocket(draining_listener_socket
? draining_listener_socket
: factory_.createListenSocket(new_listener->address(),
new_listener->socketType(),
new_listener->listenSocketOptions(),
new_listener->bindToPort())); Network::SocketSharedPtr ProdListenerComponentFactory::createListenSocket(... ...) {
... ...
// 调用 UdsListenSocket 做 bind() 操作。
if (io_handle->isOpen()) {
return std::make_shared<Network::UdsListenSocket>(std::move(io_handle), address);
}
return std::make_shared<Network::UdsListenSocket>(address);
} // 最终调用系统bind()操作
void ListenSocketImpl::doBind() {
const Api::SysCallIntResult result = local_address_->bind(io_handle_->fd());
... ...
}

在 InstanceImpl 启动时,调用 RunHelper。RunHelper 则启动 startWorkers。startWorker 将初始化得到的 listeners 加入到 work 中。

void ListenerManagerImpl::startWorkers(GuardDog& guard_dog) {
workers_started_ = true;
for (const auto& worker : workers_) {
ASSERT(warming_listeners_.empty());
for (const auto& listener : active_listeners_) {
addListenerToWorker(*worker, *listener);
}
worker->start(guard_dog);
}
}

work 将 linsteners 关联到 connectioHandler。

void ListenerManagerImpl::addListenerToWorker(Worker& worker, ListenerImpl& listener) {
worker.addListener(listener, [this, &listener](bool success) -> void {
... ...
} void WorkerImpl::addListener(Network::ListenerConfig& listener, AddListenerCompletion completion) {
dispatcher_->post([this, &listener, completion]() -> void {
try {
// 关联到connectioHandler。
handler_->addListener(listener);
hooks_.onWorkerListenerAdded();
completion(true);
} catch (const Network::CreateListenerException& e) {
completion(false);
}
});
}

connectioHandler 在 work 初始化时创建。

ListenerManagerImpl::ListenerManagerImpl(Instance& server,
ListenerComponentFactory& listener_factory,
WorkerFactory& worker_factory)
: server_(server), factory_(listener_factory), stats_(generateStats(server.stats())),
config_tracker_entry_(server.admin().getConfigTracker().add(
"listeners", [this] { return dumpListenerConfigs(); })) {
for (uint32_t i = 0; i < server.options().concurrency(); i++) {
// 初始化worker
workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
}
} WorkerPtr ProdWorkerFactory::createWorker(OverloadManager& overload_manager) {
Event::DispatcherPtr dispatcher(api_.allocateDispatcher());
return WorkerPtr{new WorkerImpl(
tls_, hooks_, std::move(dispatcher),
//创建connectioHandler
Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
overload_manager, api_)};
}

将 linsteners 关联到 connectioHandler 后,后面的 listen(),accept() 和创建连接过程和 admin 的 HTTP 启动流程是一样的。

LDS 服务启动流程

整个服务的启动流程基本就完成了,后面有新加服务的启动流程和上面的服务启动流程一样,调用 addOrUpdateListener。在 addOrUpdateListener 内判断服务是否已启动,如果已启动调用 ManagerImpl 等待初始化。

void ListenerImpl::initialize() {
last_updated_ = timeSource().systemTime();
if (workers_started_) {
//ManagerImpl
dynamic_init_manager_.initialize(*init_watcher_);
}
} void ManagerImpl::initialize(const Watcher& watcher) {
... ...
for (const auto& target_handle : target_handles_) {
// 等待 target_handle 初始化完成。
if (!target_handle->initialize(watcher_)) {
onTargetReady();
}
}
}

初始化完成后,调用函数指针。函数指针在初始化WatcherImpl传入。

void ManagerImpl::onTargetReady() {
if (--count_ == 0) {
// 初始化完成
ready();
}
} void ManagerImpl::ready() {
state_ = State::Initialized;
watcher_handle_->ready();
} bool WatcherHandleImpl::ready() const {
//调用函数指针
(*locked_fn)();
} ListenerImpl::ListenerImpl(... ...)
: ... ...
// 初始化watch
init_watcher_(std::make_unique<Init::WatcherImpl>(
"ListenerImpl", [this] { parent_.onListenerWarmed(*this); })){}

在 onListenerWarmed 内将 listener 加入 work。后面流程和 服务启动流程一样,不再分析。

void ListenerManagerImpl::onListenerWarmed(ListenerImpl& listener) {
for (const auto& worker : workers_) {
addListenerToWorker(*worker, listener);
}

最后

整个服务的初始化和启动流程就完成了。服务的启动有3个类型 : 本地 HTTP 服务管理服务、本地配置文件的服务和xDS下发的服务。本章节只分析了服务的启动流程,连接成功的后继处理,以后分析。

Envoy 源码分析--程序启动过程的更多相关文章

  1. mybatis源码分析:启动过程

    mybatis在开发中作为一个ORM框架使用的比较多,所谓ORM指的是Object Relation Mapping,直译过来就是对象关系映射,这个映射指的是java中的对象和数据库中的记录的映射,也 ...

  2. Nimbus&lt;三&gt;Storm源码分析--Nimbus启动过程

    Nimbus server, 首先从启动命令开始, 同样是使用storm命令"storm nimbus”来启动看下源码, 此处和上面client不同, jvmtype="-serv ...

  3. workerman源码分析之启动过程

    PHP一直以来以草根示人,它简单,易学,被大量应用于web开发,非常可惜的是大部分开发都在简单的增删改查,或者加上pdo,redis等客户端甚至分布式,以及规避语言本身的缺陷.然而这实在太委屈PHP了 ...

  4. elasticSearch6源码分析(1)启动过程

    1.找到bin目录,下面有elasticSearch的sh文件,查看执行过程 exec \ "$JAVA" \ $ES_JAVA_OPTS \ -Des.path.home=&qu ...

  5. 1 cocos2dx源码分析-程序启动与主循环

        1 启动   在iOS系统中,由main函数启动默认调用了AppController main.m NSAutoreleasePool * pool = [[NSAutoreleasePool ...

  6. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  7. Envoy 源码分析--LDS

    Envoy 源码分析--LDS LDS 是 Envoy 用来自动获取 listener 的 API. Envoy 通过 API 可以增加.修改或删除 listener. 先来总结下 listener ...

  8. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  9. Envoy 源码分析--network

    目录 Envoy 源码分析--network address Instance DNS cidr socket Option Socket ListenSocket ConnectionSocket ...

随机推荐

  1. Redis-秒杀场景应用

    Redis Util实现 package test.jedis; import java.util.List; import java.util.Set; import redis.clients.j ...

  2. Python学习笔记(十三)

    Python学习笔记(十三): 模块 包 if name == main 软件目录结构规范 作业-ATM+购物商城程序 1. 模块 1. 模块导入方法 import 语句 import module1 ...

  3. The packages can be overrided by Java Endorsed Standards

     Endorsed Standards APIs The Endorsed Standards for Java SE constitute all classes and interfaces ...

  4. Spark 跑 java 示例代码

    一.下载示例代码: git clone https://github.com/melphi/spark-examples.git 从示例代码中可以看到 pox中引入了 Spark开发所需要的依赖. 二 ...

  5. Spring Security 指定登陆入口

    spring security除通过form-login的熟悉指定登陆还可以通过entry-point-ref 指定登陆入口.具体配置如下: <?xml version="1.0&qu ...

  6. centos中yum命令删除还原的补救方法介绍

    前言 yum,是Yellow dog Updater Modified的简称,起初是由yellow dog这一发行版的开发者Terra Soft研发,用python写成,那时还叫做yup(yellow ...

  7. python基础----&gt;python的使用(五)

    这里记录一些python的一些基础知识,主要内容是高阶函数的使用.或许我的心包有一层硬壳,能破壳而入的东西是极其有限的.所以我才不能对人一往情深. python中的高阶函数 一.map().reduc ...

  8. windows乱码

    对于支持 UNICODE的应用程序,Windows 会默认使用 Unicode编码.对于不支持Unicode的应用程序Windows 会采用 ANSI编码 (也就是各个国家自己制定的标准编码方式,如对 ...

  9. 《Windows核心编程》第2章——字符和字符处理

    ANSI和UNICODE 计算char和wchar_t的长度都一样,都是5,但是二者在内存中的布局实际上是不同的:

  10. TransactionScope 的基本原理简介

    C# 的事务编程 1 Db事务 DbConnection 中创建基于当前连接的 DbTransaction 2  使用TransactionScope ,创建环境事务 一旦创建,在这个环境包含的DbC ...