【注】本翻译来自https://theboostcpplibraries.com/boost.asio。boost.asio从v1.66起io_service变成了io_context,二者有一定差异,但基本应用相同。

目  录

  • IO服务和IO对象
  • 可扩展性和多线程
  • 网络编程
  • 协程
  • 与平台相关的IO对象

This chapter introduces the library Boost.Asio. Asio stands for asynchronous input/output. This library makes it possible to process data asynchronously. Asynchronous means that when operations are initiated, the initiating program does not need to wait for the operation to end. Instead, Boost.Asio notifies a program when an operation has ended. The advantage is that other operations can be executed concurrently.

Boost.Thread is another library that makes it possible to execute operations concurrently. The difference between Boost.Thread and Boost.Asio is that with Boost.Thread, you access resources inside of a program, and with Boost.Asio, you access resources outside of a program. For example, if you develop a function which needs to run a time-consuming calculation, you can call this function in a thread and make it execute on another CPU core. Threads allow you to access and use CPU cores. From the point of view of your program, CPU cores are an internal resource. If you want to access external resources, you use Boost.Asio.

Network connections are an example of external resources. If data has to be sent or received, a network card is told to execute the operation. For a send operation, the network card gets a pointer to a buffer with the data to send. For a receive operation the network card gets a pointer to a buffer it should fill with the data being received. Since the network card is an external resource for your program, it can execute the operations independently. It only needs time – time you could use in your program to execute other operations. Boost.Asio allows you to use the available devices more efficiently by benefiting from their ability to execute operations concurrently.

Sending and receiving data over a network is implemented as an asynchronous operation in Boost.Asio. Think of an asynchronous operation as a function that immediately returns, but without any result. The result is handed over later.

In the first step, an asynchronous operation is started. In the second step, a program is notified when the asynchronous operation has ended. This separation between starting and ending makes it possible to access external resources without having to call blocking functions.

本章介绍Boost.Asio,Asio代表异步输入输出。这个库使异步处理数据成为可能,异步意味着操作被触发后,触发它的程序不必等着操作完成,而是由Boost.Asio通知程序该操作已经完成了,这样做的好处就是其他操作也可以同时进行。

Boost.Thread是另一个能使操作同时进行的库。Boost.Thread和Boost.Asio的不同之处在于,使用Boost.Thread时,你是在使用程序内的资源,使用Boost.Asio时,你是在使用程序外的资源。例如,如果你写了一个耗时的计算函数,你可以从一个线程中调用这个函数,并且让它运行在CPU的另一个核中,线程允许你访问并使用CPU的内核,从你程序的角度,CPU核是内部资源。如果你想使用外部资源,就用Boost.Asio。

网络连接是外部资源的之一。如果数据被发生或被送达,网卡被告知要执行一个操作,对于发送,网卡得到一个指向待发送数据区的指针,对于接收,网卡得到一个能填充接收数据的缓存区指针。由于网卡相对于你程序是一个外部资源,它是独立执行这些操作的,它需要时间来完成操作,而这期间,你可以用你的程序来做些别的操作。Boost.Asio允许你通过同时运行操作来更加高效地使用这些设备。

Boost.Asio就是通过异步操作来完成数据的网络发送与接收,想象一下一个即刻返回的异步操作函数,它没有即刻结果,结果是在稍后来处理的。

第一个步,开始一个异步操作,第二步,程序被通知异步操作完成,开始与完成的之间的过程,可以使用外部资源而无需调用阻塞函数。

I/O Services and I/O Objects

Programs that use Boost.Asio for asynchronous data processing are based on I/O services and I/O objects. I/O services abstract the operating system interfaces that process data asynchronously. I/O objects initiate asynchronous operations. These two concepts are required to separate tasks cleanly: I/O services look towards the operating system API, and I/O objects look towards tasks developers need to do.

As a user of Boost.Asio you normally don’t connect with I/O services directly. I/O services are managed by an I/O service object. An I/O service object is like a registry where I/O services are listed. Every I/O object knows its I/O service and gets access to its I/O service through the I/O service object.

Boost.Asio defines boost::asio::io_service, a single class for an I/O service object. Every program based on Boost.Asio uses an object of type boost::asio::io_service. This can also be a global variable.

While there is only one class for an I/O service object, several classes for I/O objects exist. Because I/O objects are task oriented, the class that needs to be instantiated depends on the task. For example, if data has to be sent or received over a TCP/IP connection, an I/O object of type boost::asio::ip::tcp::socket can be used. If data has to be transmitted asynchronously over a serial port, boost::asio::serial_port can be instantiated. If you want to wait for a time period to expire, you can use the I/O object boost::asio::steady_timer.

boost::asio::steady_timer is like an alarm clock. Instead of waiting for a blocking function to return when the alarm clock rings, your program will be notified. Because boost::asio::steady_timer just waits for a time period to expire, it would seem as though no external resource is accessed. However, in this case the external resource is the capability of the operating system to notify a program when a time period expires. This frees a program from creating a new thread just to call a blocking function. Since boost::asio::steady_timer is a very simple I/O object, it will be used to introduce Boost.Asio.

Note

Because of a bug in Boost.Asio, it is not possible to compile some of the following examples with Clang. The bug has been reported in ticket 8835. As a workaround, if you replace the types from std::chrono with the respective types from boost::chrono, you can compile the examples with Clang.

使用Boost.Asio进行数据异步处理的程序以I/O服务和I/O对象为基础。I/O服务抽象出了操作系统的异步数据操作接口。I/O对象发起异步操作。这两个概念将任务清爽地区分开:I/O服务关注操作系统API而I/O对象关注开发者需要做的事情。

正常情况下,Boost.Asio的用户不会直接连接到I/O服务,I/O服务是由I/O服务对象来管理的,一个I/O服务对象像一个注册表,上面有I/O服务的列表。每个I/O对象知道它的I/O服务,并且通过I/O服务对象来访问I/O服务。

Boost.Asio定义了只定义了一个I/O服务对象:boost::asio::io_service。每个基于Boost.Asio的程序都要使用一个boost::asio::io_service类型的对象,这个对象是一个全局变量。

虽然只有一个I/O服务对象,但I/O对象有几个类。由于I/O对象是面向用户任务的,所以应基于任务来使用该类。例如,如果需要通过TCP/IP连接来接收或发送数据,那么可以使用一个boost::asio::ip::tcp::socket类型的对象,如果数据通过串口来异步传输,那么可以使用boost::asio::serial_port,如果你想等待一段时间,那么可使用boost::asio::steady_timer。

boost::asio::steady_timer像一个报警钟,但你的程序不必等待阻塞函数返回,你的程序将被通知。由于boost::asio::steady_timer就是等待一段时间后超时,看起来无需访问外部资源,但是,外部资源其实是操作系统通知程序的能力,这样,程序就不必创建一个新线程然后调用阻塞函数了。由于boost::asio::steady_timer是一个非常简单的I/O对象,它将被用来介绍Boost.Asio。

由于Boost.Asio的一个bug问题,用Clang来编译以下的一些例子会有问题。这个bug记录在ticket8835中。采用下面迂回的方法,你可以用Clang来编译这些例子:将td::chrono 替换为对应的boost::chrono。

Example 32.1. Using boost::asio::steady_timer

#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <iostream>using namespace boost::asio;int main()
{io_service ioservice;steady_timer timer{ioservice, std::chrono::seconds{3}};timer.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });ioservice.run();
}

Example 32.1 creates an I/O service object, ioservice, and uses it to initialize the I/O object timer. Like boost::asio::steady_timer, all I/O objects expect an I/O service object as a first parameter in their constructor. Since timer represents an alarm clock, a second parameter can be passed to the constructor of boost::asio::steady_timer that defines the specific time or time period when the alarm clock should ring. In Example 32.1, the alarm clock is set to ring after 3 seconds. The time starts with the definition of timer.

Instead of calling a blocking function that will return when the alarm clock rings, Boost.Asio lets you start an asynchronous operation. To do this, call the member function async_wait(), which expects a handler as the sole parameter. A handler is a function or function object that is called when the asynchronous operation ends. In Example 32.1, a lambda function is passed as a handler.

async_wait() returns immediately. Instead of waiting three seconds until the alarm clock rings, the lambda function is called after three seconds. When async_wait() returns, a program can do something else.

A member function like async_wait() is called non-blocking. I/O objects usually also provide blocking member functions as alternatives. For example, you can call the blocking member function wait() on boost::asio::steady_timer. Because this member function is blocking, no handler is passed. wait() returns at a specific time or after a time period.

The last statement in main() in Example 32.1 is a call to run() on the I/O service object. This call is required because operating system-specific functions have to take over control. Remember that it is the I/O service in the I/O service object which implements asynchronous operations based on operating system-specific functions.

While async_wait() initiates an asynchronous operation and returns immediately, run() blocks. Many operating systems support asynchronous operations only through a blocking function. The following example shows why this usually isn’t a problem.

示例32.1创建了一个I/O服务对象ioservice,然后用它初始化了一个I/O定时器对象。就像boost::asio::steady_timer类一样,所有I/O对象的构造函数都要求有一个I/O服务对象作为其第一个参数,由于定时器是一个报警钟,所以boost::asio::steady_timer构造函数的第二个参数可以是特定的时刻或一个时间段,示例32.1中,这个报警钟设置在3秒之后报警,开始计时的时刻就是定时器被定义的时刻。

Boost.Asio让你开始一个异步操作,而不是调用一个仅当报警发生时才返回的阻塞函数。为此,调用成员函数async_wait(),该函数仅有一个句柄参数,这个句柄是一个函数或函数对象,当异步操作完成时被调用。示例32.1中,这个句柄是一个lambda函数。

async_wait()即刻返回,lambda函数不是一直等待直到定时器报警为止,而是3秒后才被调用,在async_wait()返回后,程序可以做点别的事情。

像async_wait()这样的成员函数被叫做非阻塞的,I/O对象通常也有对应的阻塞成员函数。例如,你也可以boost::asio::steady_timer的阻塞成员函数wait(),由于它是阻塞的,不需要句柄做参数。wait()只在特定时刻或一段时间之后才返回。

示例32.1中最后一行是调用了I/O服务对象的run(),这个调用是必需的,因为操作系统函数要接管控制。记住是I/O服务对象中的I/O服务实现了与操作系统相关的异步操作。

当async_wait()初始化一个异步操作并即刻返回后,run()运行阻塞。许多操作系统仅通过阻塞函数来支持异步操作。下面的示例显示了为什么这通常不是问题。

Example 32.2. Two asynchronous operations with boost::asio::steady_timer
In Example 32.2, two objects of type boost::asio::steady_timer are used. The first I/O object is an alarm clock that rings after three seconds. The other is an alarm clock ringing after four seconds. After both time periods expire, the lambda functions that were passed to async_wait() will be called.

run() is called on the only I/O service object in this example. This call passes control to the operating system functions that execute asynchronous operations. With their help, the first lambda function is called after three seconds and the second lambda function after four seconds.

It might come as a surprise that asynchronous operations require a call to a blocking function. However, this is not a problem because the program has to be prevented from exiting. If run() wouldn’t block, main() would return, and the program would exit. If you don’t want to wait for run() to return, you only need to call run() in a new thread.

The reason why the examples above exit after a while is that run() returns if there are no pending asynchronous operations. Once all alarm clocks have rung, no asynchronous operations exist that the program needs to wait for.

Example 32.2. Two asynchronous operations with boost::asio::steady_timer

#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <iostream>using namespace boost::asio;int main()
{io_service ioservice;steady_timer timer1{ioservice, std::chrono::seconds{3}};timer1.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });steady_timer timer2{ioservice, std::chrono::seconds{4}};timer2.async_wait([](const boost::system::error_code &ec){ std::cout << "4 sec\n"; });ioservice.run();
}

示例32.2中,使用了两个boost::asio::steady_timer类型对象,第一对象是一个定时器,3秒之后报警,另一个定时器4秒之后报警。时间超时后,传递给async_wait()的lambda函数被调用。

示例调用了唯一的I/O服务对象的run(),这个调用将控制权交给操作系统函数以执行异步操作,在操作系统的帮助下,第一个lambda函数在3秒后被调用,第二个lambda函数4秒之后被调用。可能有点惊讶,异步操作需要调用一个阻塞函数,不过,这不是问题,因为程序不得不防止退出,如果run()不阻塞,main()将返回,程序就退出了。如果你不想等待run()返回,你只需在另一个新线程调用run()即可。

以上示例中,为什么程序运行一会就退出了,是因为如果没有挂起的异步操作任务,run()就会返回。一旦所有的定时器都报警了,程序中所等待完成的异步操作都完成退出了。

Scalability and Multithreading

Developing a program based on a library like Boost.Asio differs from the usual C++ style. Functions that may take longer to return are no longer called in a sequential manner. Instead of calling blocking functions, Boost.Asio starts asynchronous operations. Functions which should be called after an operation has finished are now called within the corresponding handler. The drawback of this approach is the physical separation of sequentially executed functions, which can make code more difficult to understand.

A library such as Boost.Asio is typically used to achieve greater efficiency. With no need to wait for an operation to finish, a program can perform other tasks in between. Therefore, it is possible to start several asynchronous operations that are all executed concurrently – remember that asynchronous operations are usually used to access resources outside of a process. Since these resources can be different devices, they can work independently and execute operations concurrently.

Scalability describes the ability of a program to effectively benefit from additional resources. With Boost.Asio it is possible to benefit from the ability of external devices to execute operations concurrently. If threads are used, several functions can be executed concurrently on available CPU cores. Boost.Asio with threads improves the scalability because your program can take advantage of internal and external devices that can execute operations independently or in cooperation with each other.

If the member function run() is called on an object of type boost::asio::io_service, the associated handlers are invoked within the same thread. By using multiple threads, a program can call run() multiple times. Once an asynchronous operation is complete, the I/O service object will execute the handler in one of these threads. If a second operation is completed shortly after the first one, the I/O service object can execute the handler in a different thread. Now, not only can operations outside of a process be executed concurrently, but handlers within the process can be executed concurrently, too.

开发一个基于Boost.Asio的程序不同于通常风格的C++程序。耗时返回的函数将不再以顺序方式调用。Boost.Asio开启异步操作方式而不是调用阻塞函数。那些原本在操作完成之后需要调用的函数,现在通过对应的句柄来调用。这样做的缺点是从物理上(代码顺序)分割了函数的顺序执行过程,从而使代码比较难理解。

像Boost.Asio这样的库,典型是为了提高效率而应用的,没有了等待操作完成,程序就可以在其中做点其他的事情,因而,同时可以进行几项异步操作,请注意,异步操作通常需要访问进程外的资源,这些资源可以是不同的设备,它们独立工作,并行运行。

可扩展性是指程序获得额外资源后所带来的收益能力。利用Boost.Asio,就有可能利用外部设备带来操作的并行能力,如果采用多线程,几个并行函数可以在能得到的几个CPU核上运行。Boost.Asio加上多线程,大大提高了可扩展性,使你的程序充分利用可独立运行或交互配合运行的内部和外部资源。

如果一个类型为boost::asio::io_service的对象的成员函数run()被调用,在同一线程内,相关联的句柄就被触发。采用多线程,程序可以多次调用run()。一旦一个异步操作完成,I/O服务对象就在一个线程中运行对应的句柄。如果第二个异步操作随即也完成了,I/O服务对象会在另一个线程执行对应的句柄。现在,不但进程外部的操作可以并发执行,进程内部的句柄也能并发执行。

Example 32.3. Two threads for the I/O service object to execute handlers concurrently

#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <thread>
#include <iostream>using namespace boost::asio;int main()
{io_service ioservice;steady_timer timer1{ioservice, std::chrono::seconds{3}};timer1.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });steady_timer timer2{ioservice, std::chrono::seconds{3}};timer2.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });std::thread thread1{[&ioservice](){ ioservice.run(); }};std::thread thread2{[&ioservice](){ ioservice.run(); }};thread1.join();thread2.join();
}

The previous example has been converted to a multithreaded program in Example 32.3. With std::thread, two threads are created in main(). run() is called on the only I/O service object in each thread. This makes it possible for the I/O service object to use both threads to execute handlers when asynchronous operations complete.

In Example 32.3, both alarm clocks should ring after three seconds. Because two threads are available, both lambda functions can be executed concurrently. If the second alarm clock rings while the handler of the first alarm clock is being executed, the handler can be executed in the second thread. If the handler of the first alarm clock has already returned, the I/O service object can use any thread to execute the second handler.

Of course, it doesn’t always make sense to use threads. Example 32.3 might not write the messages sequentially to the standard output stream. Instead, they might be mixed up. Both handlers, which may run in two threads concurrently, share the global resource std::cout. To avoid interruptions, access to std::cout would need to be synchronized. The advantage of threads is lost if handlers can’t be executed concurrently.

先前的示例在示例32.3中被转化为多线程。main()中,用std::thread创建两线程,每个线程上调用唯一的I/O服务对象的run(),这样,当异步操作完成时,I/O服务对象可利用两个线程来执行句柄函数。

示例32.3中,两个定时器都在3秒后报警,由于是两个线程,两个lambda函数能够同时运行。如果第二个报警产生时,第一个报警句柄函数正在执行,那么第二个句柄函数能在第二个线程执行。如果第一个句柄函数已经返回,那么I/O服务对象可用任何一个线程执行第二个句柄函数。

当然,使用多线程不并总是美好。示例32.3没有将信息顺序化输出到标准输出流(指屏幕)中,因此,输出信息有可能混在一起,因为运行于两个线程的两个句柄函数共享了全局的std::out。为避免输出混乱,访问std::out需要同步,这样,如果句柄们不能同时运行,多线程的好处就丧失了。

Example 32.4. One thread for each of two I/O service objects to execute handlers concurrently

#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <thread>
#include <iostream>using namespace boost::asio;int main()
{io_service ioservice1;io_service ioservice2;steady_timer timer1{ioservice1, std::chrono::seconds{3}};timer1.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });steady_timer timer2{ioservice2, std::chrono::seconds{3}};timer2.async_wait([](const boost::system::error_code &ec){ std::cout << "3 sec\n"; });std::thread thread1{[&ioservice1](){ ioservice1.run(); }};std::thread thread2{[&ioservice2](){ ioservice2.run(); }};thread1.join();thread2.join();
}

Calling run() repeatedly on a single I/O service object is the recommended method to make a program based on Boost.Asio more scalable. However, instead of providing several threads to one I/O service object, you could also create multiple I/O service objects.

Two I/O service objects are used next to two alarm clocks of type boost::asio::steady_timer in Example 32.4. The program is based on two threads, with each thread bound to another I/O service object. The two I/O objects timer1 and timer2 aren’t bound to the same I/O service object anymore. They are bound to different objects.

Example 32.4 works the same as before. It’s not possible to give general advice about when it makes sense to use more than one I/O service object. Because boost::asio::io_service represents an operating system interface, any decision depends on the particular interface.

On Windows, boost::asio::io_service is usually based on IOCP, on Linux, it is based on epoll(). Having several I/O service objects means that several I/O completion ports will be used or epoll() will be called multiple times. Whether this is better than using just one I/O completion port or one call to epoll() depends on the individual case.

基于Boost.Asio的应用的推荐方法是在一个I/O服务对象上多次调用run(),这样扩展性更好,但是,你也可以不采用多个线程一个I/O服务对象的方式,而采用多个I/O服务对象方式。

示例32.4中,用了boost::asio::steady_timer类型的两个定时器的同时用了两个I/O服务对象,这个程序基于两个线程,一个线程对应一个I/O服务对象,两个I/O对象timer1和timer2分别对应两个I/O服务对象,而不是一个。

示例32.4与上面的示例工作相同。对于什么情况下使用多个I/O服务对象,不太可能给出一般性的建议。因为boost::asio::io_service封装了操作系统接口,任何行为都是由特定接口决定的。

Windows平台,boost::asio::io_service通常基于IOCP,Linux平台,它通常基于epoll()。有几个I/O服务对象就意味着有几个I/O完成端口或epoll()被调用多次,这种用法是否会比用一个I/O完成端口或调用一次epoll()好,取决于特定的场合。

Boost.Asio初步(一)相关推荐

  1. boost::asio::streambuf 基本用法和注意事项

    streamsize  sgetn(char_type *store,streamsize n)    返回缓冲区下n个字符并存储到store中,并将缓冲区位置后移n个字节 代码说明:本来是想不断的通 ...

  2. boost::asio使用UDP协议通信源码实现

    说明:以下源码来自参考文献[1], 比原文更丰富, 更有指导意义, 方便日后参考. udp servr端源码 //g++ -g udp_server.cpp -o udp_server -lboost ...

  3. boost::asio异步模式的C/S客户端源码实现

    异步模式的服务器源码 //g++ -g async_tcp_server.cpp -o async_tcp_server -lboost_system //#include <iostream& ...

  4. boost::asio中的C/S同步实例源码

    近来狂热地研究boost的开发技术,现将读书笔记整理如下: 需要说明的是, 本博该专题下面关于boost的源码是采用boost1.55版本, 运行在Ubuntu 14.04 64bit下面, 使用ap ...

  5. muduo 与 boost asio 吞吐量对比

    muduo (http://code.google.com/p/muduo) 是一个基于 Reactor 模式的 C++ 网络库,我在编写它的时候并没有以高并发高吞吐为主要目标,但出乎我的意料,pin ...

  6. boost.asio包装类st_asio_wrapper开发教程(2014.5.23更新)(一)-----转

    一:什么是st_asio_wrapper 它是一个c/s网络编程框架,基于对boost.asio的包装(最低在boost-1.49.0上调试过),目的是快速的构建一个c/s系统: 二:st_asio_ ...

  7. boost asio io_service学习笔记

    转自:http://hi.baidu.com/jrckkyy/blog/item/e86835d61e60722506088b6a.html 构造函数 构造函数的主要动作就是调用CreateIoCom ...

  8. Boost asio 定时器

    Boost asio入门学习笔记 版权声明:本文为博主原创文章,未经博主允许不得转载.文章中有连接失效或是技术谬误的地方,请与我联系. https://blog.csdn.net/luchengtao ...

  9. 网上收集下boost::asio发送与传输相关的几个函数,老是忘记

    刚连接上:调用async_accept 1 boost::shared_ptr<tcp::socket> spMySocket(new tcp::socket(m_ioservice)); ...

最新文章

  1. 求教大牛!关于后缀树
  2. 常考数据结构与算法:用两个栈实现队列
  3. 动态规划之背包模型及其扩展应用
  4. android roboto字体下载,Android字体设置及Roboto字体使用方法
  5. vs 本地调试(IIS)
  6. Python之路---------Python介绍
  7. 求职产品经理【十六】笔试真题串讲之百度地图与大数据结合的产品
  8. 服务端渲染与 Universal React App
  9. Could not create directory F:\Qt\Test\Error in Util.asciify(build-First_02-Desktop_Qt_5_6_3_Min
  10. PCR之父凯利·穆利斯:有才,真的可以为所欲为
  11. python如何并发运行2个软件_如何利用并发性加速你的python程序(二):I/O 绑定程序加速...
  12. jenkins配置ssh免密码登陆
  13. 箭头 运算符_Java 运算符
  14. AndroidStudio_Android中使用非阻塞延迟的方法_利用Handler实现---Android原生开发工作笔记218
  15. emoji表情mysql报错_让MySQL支持Emoji表情 mysql 5.6
  16. QT连接MySQL记录
  17. 金山词霸 只能最大最小
  18. 【Multisim】导入spice器件详细教程以及库资源分享
  19. SolidWorks工程图比例 1:1 配置
  20. C/C++框架、库、资源

热门文章

  1. C#中FileSystemWatcher的使用教程
  2. 木耳不能和什么一起吃 木耳的禁忌人群
  3. CSS background-image背景图片相关介绍
  4. JSP连接MySQL数据库
  5. 36种简便、有效的记忆方法(摘抄)
  6. 装饰者设计模式(结构型设计模式)
  7. 联想v800手机看电子书的方法
  8. 换工作,需要注意社保系统退工的事宜
  9. 【实战】轻轻松松使用StyleGAN(六):StyleGAN Encoder找到真实人脸对应的特征码,核心源代码+中文注释
  10. mac 每次启动终端都会提示 zsh compinit: insecure directories, run compaudit for list. Ignore insecure