Thrift network stack

Thrift network stack

本文翻译自Thrift network stack

Apache Thrift网络栈的简要描述

+-------------------------------------------+
| Server                                    |
| (single-threaded, event-driven etc)       |
+-------------------------------------------+
| Processor                                 |
| (compiler generated)                      |
+-------------------------------------------+
| Protocol                                  |
| (JSON, compact etc)                       |
+-------------------------------------------+
| Transport                                 |
| (raw TCP, HTTP etc)                       |
+-------------------------------------------+

传输

传输层提供了读写网络的简单抽象。这使得Thrift可以与系统的底层传输解耦(如序列化/反序列化)。
下面是传输层接口暴露的一些方法:

  • open
  • close
  • read
  • write
  • flush

除了上面的传输接口外,Thrift还使用ServerTransport接口来接受或创建原始的传输对象。正如名字所写的,服务器端端使用ServerTransport为到来的连接创建一个传输对象(Transport objects)。

  • open
  • listen
  • accept
  • close

大多数支持Thrift支持的语言的可选的一些传输如下:

  • file: read/write to/from a file on disk
  • http: as the name suggests

###协议
协议的抽象定义了将内存中结构映射为有线格式(wire-format)的机制。换句话说,协议指明了数据如何使用底层的传输来进行编解码。因此协议实现控制了编码格式,并负责序列化和反序列化。协议举例,如JSON,XML,plain text, compact binary 。

下面是协议接口:

writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, size)
writeListEnd()
writeSetBegin(etype, size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)

name, type, seq = readMessageBegin()
                  readMessageEnd()
name = readStructBegin()
       readStructEnd()
name, type, id = readFieldBegin()
                 readFieldEnd()
k, v, size = readMapBegin()
             readMapEnd()
etype, size = readListBegin()
              readListEnd()
etype, size = readSetBegin()
              readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()

Thrift协议是面向流的。没有必要需要任何明确的框架。举例,开始序列化的时候,不需要知道字符串的长度或列表中的个数。大多数支持Thrift语言可选的协议:

  • binary:相当简单的二进制编码–字段的长度和类型都编码为字节
  • compact:在THRIFT-100中描述
  • json

###处理器(Processor)

一个处理器有两个能力:从输入流读取数据,将数据写入到输出流。协议对象(protocal objects)代表输入流和输出流。处理器接口可以非常简单:

interface TProcessor {
    bool process(TProtocol in, TProtocol out) throws TException
}

特定服务处理器(service-specific)的实现是由compiler生成的。本质上处理器processor使用输入协议从wire读取数据,将处理过程委托给用户自己实现的handler,然后使用输出协议将数据写入wire。

服务

一个server将上面描述的所有的不同的特性聚合在一起:

  • 创建一个传输 transport
  • 为传输创建一个input/output协议
  • 基于Input/output协议创建一个处理器processor
  • 等待输入连接,并将其交给processor

其他文章

Thrift Feature

Thrift interface description language

Thrift install

Thrift types

Thrift:可扩展的跨语言的服务实现[论文翻译]

本文翻译Thrift: Scalable Cross-Language Services Implementation

有一些地方没有翻译到,就纯纯的用英文直接读咯.

概要

Thrift是什么呢?Thrift由Facebook开发;Thrift是一个软件库,一个代码生成工具的集合;通过这些工具可以加快高效可扩展的后端服务的开发和实现。Thrift的主要目标是:实现跨语言的高效可靠的通信。实现方式:将各个语言中需要的定制化抽象为一个通用的库。这个库是由各个语言实现的一个库。 特别地,Thrift允许开发者以一个中立语言(language-neutral)文件定义数据类型和服务接口,并且生成构建RPC客户端和服务器端的所需的所有必要的代码。

这篇论文详细介绍了Thrift中的动机和设计思想。 这并不是作为一个研究,而是详细阐述了我们做了什么以及为什么这么做。

1. 说明

一句话,单纯的LAMP框架不能够满足Facebook的需求,Facebook的文化就是选择最好的工具和实现来达到最好的性能。那么带来的问题就是需要在各个语言之间建立一个透明和高性能的桥。我们发现大多数可选系统要么限制太多,要么有性能问题。

我们的实现方案:将一个中立语言软件栈与一个代码生成引擎进行组装。
中立语言软件栈可以由许多编程语言实现。
代码生成引擎可以可以将简单接口和数据定义语言转换为客户端和服务端调用的远程库。

在一个动态系统上选择一个静态代码生成,允许我们可以创建有效的代码;这些代码不需要进行实时的类型检查。对于开发者而言,可以在一个简短的文件中定义复杂服务所需要的数据类型和接口。

令人惊讶的是竟然一个健壮的开源的解决方案,因此我们将Thrift的实现进行了开源。

评估了网络环境下,跨语言交互的挑战后,我们明确了一些关键组件:

  • types:必须有一个通用的类型系统;跨语言的时候,应用开发者不需要使用自定义的thrift数据类型或者写自己的序列化代码。也就是说,一个C++程序员能够透明的将一个STL map转换为python的字字典。不需要程序员写应用层以下的代码。
  • transport。每个语言都需要一个通用的双向数据传输的接口。服务开发者(service developer)不需要关心特定传输是如何实现的。相同的应用代码可以跑在不同的传输上,比如TCP流,内存中数据,或者硬盘上的文件。
  • protocol:数据类型必须有某种使用传输层的方式来编解码自己。当然,应用开发者不需要关心协议这一层的实现。对于应用层代码而言,使用XML或二进制协议都是一样的。所有要确定的就是,必须以始终一样的确定的协议进行传输
  • versioning。对于健壮的服务,所包含的数据类型必须提供一个版本化自己的机制。特别的,能够在不破坏服务的情况下增加或删除对象(或者函数列表)中的字段。Section 5 details Thrift’s versioning system.
  • Processors:最后,我们生成能够处理数据流的代码来完成远程进程调用。Section 6 details the generated code and TProcessor paradigm。
  • Section 7 discusses implementation details, and Section 8 describes our conclusions.

2. Types

Thrift类型系统是为了:不管使用什么编程语言,都使用完全原生的类型进行开发。设计上,Thrift类型系统不会引入任何特别的动态类型或者wrapper objects。并且,不需要开发者写任何对象序列化或传输的代码。Thrift IDL(Interface Definition Language)文件以最简单的方式告诉代码生成工具,如何安全的跨语言的传输对象。

2.1 base types

类型系统依赖一些基础类型。考虑支持哪些类型时,我们目标是在所有编程语言中都可用的数据类型。
在Thrift中支持的基础类型是

  • bool
  • byte
  • i16: a 16-bit signed integer
  • i32: a 32-bit signed integer
  • i64: a 64-bit signed integer
  • double: a 64-bit floating point
  • string: 一个不知道编码的文本或者二进制文件。

特别注意的是unsigned integer类型的缺失。因为在很多语言中这个类型不能明确的转换为基础类型。并且,在一些语言如python中,应用开发者会给整形变量赋值一个负值。从设计的角度来说,我们发现unsigned integers在算术运算中很少用,实际中大都用作key或者标识符。这种情况下,sign就无关紧要了。signed integers同样可以做,并且可以在需要的时候安全的转换为unsigned。

2.2 structs

一个Thrift的struct定义了一个对象,可以在不同语言中使用。本质上,结构体等同于面向对象语言的类。定义Thrfit结构体的基本语法类似C的结构体定义。字段可能会用一个整形来进行注释,并且有一个默认值。如果忽略的话,会有一个默认值;不过强烈建议有取值,这对于稍后的版本有很大用处。

2.3 Containers

Thrift中的container可以映射到很多语言中的container。可以用C++模板的样式进行标记。有三种可选的类型:

  • list:元素的有序列表。直接转换为STL vector,Java ArrayList, 或JS的native array
  • set:元素的无序集合。转为了STL set, Java HashSet,python的set,或PHP中的字典
  • map<type1, type2>:key到value的映射。转为STL map,Java HashMap,PHP的关联数组

当提供默认值时,类型的映射并不是固定的。Container中的元素可以是任何有效的Thrift类型,包含其他结构体的containers。

struct Example {
    1:i32 number = 10,
    2:i64 bigNumber,
    3:double decimals,
    4:string name="thrifty"
}

在目标语言中,每个定义都会生成两种方法的类型,read和write,用于执行序列化和使用Thrift TProtocol对象传输。

2.4 Exception

异常在语法和功能上等同于结构体,除了使用exception关键字而不是使用struct关键字。
生成的对象继承自某个异常基类。目的是:用一种应用层开发者熟悉的代码风格。

2.5 Services

使用Thrift类型定义服务service。在语义上,service的定义等同于接口的定义(或者一个纯虚抽象类)。Thrift编译器生成实现这些接口的全部功能的client和server stubs。service定义如下:

service <name> {
    <returntype> <name>(<arguments>)
        [throw (<exceptions>)]
}

一个例子:

service StringCache {
    void set(1:i32 key 2:string value),
    string get(1:i32 key) throws (1:KeyNotFound knf),
    void delete(1:i32 key)
}

注意到void是函数返回的有效类型,当然也可以返回其他已经定义的thrift类型。另外,async关键字可以加到void函数上,其生成的代码不需要等待服务器的响应。对于一个纯净的void函数,只有完成服务器端的操作以后才会给客户端返回响应。使用async方法调用个,客户端只保证传输层的请求成功。
也需要注意到参数列表和异常列表也可以实现为thrift structs。在notation和行为上,三种的构造是相同的。

3. Transport

生成的代码使用传输层加速数据传输

3.1 接口

在Thrift设计中一个关键的设计选择是:从代码生成层中解耦传输层。Though Thrift is typically used on top of the TCP/IP stack with streaming sockets as the base layer of communication, there was no compelling reason to build that constraint into the system。与真实的I/O操作相比,抽象I/O层带来的性能损耗是无关紧要的。

根本上,生成的Thrift代码只需要知道如何读取和写入数据。数据的起始是无关紧要的;它可能是一个socket,一个共享内存的片段,或者本地磁盘的一个文件。Thrift传输接口支持下面的方法:

  • open: 打开传输
  • close:关闭传输
  • isOpen:标识传输是否打开
  • read:读取
  • write:写入
  • flush:

有一些接口没有列出来,这些接口用于批量读取 and optionally signaling the comple- tion of a read or write operation from the generated code.

除了上面的TTransport接口,还有一个TServerTransport接口用于接收或创建原始的transport objects。其接口如下:

  • open
  • listen
  • accept
  • close

3.2 实现

在任何语言中很容易实现传输层接口。如果需要的话,开发者可以实现新的传输层机制。

3.2.1 TSocket

在所有目标语言中,实现TSocket类。提供了一个TCP/IP stream socket的通用的简单的接口。

3.2.2 TFileTransport

TFileTransport是硬盘文件数据流的抽象。可以将到来的thrift请求写入到硬盘文件中。硬盘数据可以从日志中重放,用于post-process或reproduction 或者过去事件的同步。

3.2.3 Utilities

The Transport interface is designed to support easy extension us- ing common OOP techniques, such as composition. Some sim- ple utilites include the TBufferedTransport, which buffers the writes and reads on an underlying transport, the TFramedTransport, which transmits data with frame size headers for chunking op- timization or nonblocking operation, and the TMemoryBuffer, which allows reading and writing directly from the heap or stack memory owned by the process

4. Protocol

Thrift中的第二个重要的抽象是:数据结构和传输层表示的分离。 当传输数据时,Thrift加强了(enforce)一种消息结构,但是对于正在用的协议编码是不可知的。也就是,不需要关心数据是编码为XML,还是ASCII,还是二进制;只要数据支持固定的操作集合就行。

4.1 接口

Thrift的Protocol接口很直接。根本上它支持两件事:1. 双向的顺序消息 2. base type,containers,structs的编码。

writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, size)
writeListEnd()
writeSetBegin(etype, size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)

name, type, seq = readMessageBegin()
                  readMessageEnd()
name = readStructBegin()
       readStructEnd()
name, type, id = readFieldBegin()
                 readFieldEnd()
k, v, size = readMapBegin()
             readMapEnd()
etype, size = readListBegin()
              readListEnd()
etype, size = readSetBegin()
              readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()

Note that every write function has exactly one read counter- part, with the exception of writeFieldStop(). This is a special method that signals the end of a struct. The procedure for reading a struct is to readFieldBegin() until the stop field is encountered, and then to readStructEnd(). The generated code relies upon this call sequence to ensure that everything written by a protocol encoder can be read by a matching protocol decoder. Further note that this set of functions is by design more robust than necessary. For example, writeStructEnd() is not strictly necessary, as the end of a struct may be implied by the stop field. This method is a convenience for verbose protocols in which it is cleaner to separate these calls (e.g. a closing tag in XML).

4.2 structure

Thrift structures are designed to support encoding into a streaming protocol. The implementation should never need to frame or com- pute the entire data length of a structure prior to encoding it. This is critical to performance in many scenarios. Consider a long list of relatively large strings. If the protocol interface required reading or writing a list to be an atomic operation, then the implementation would need to perform a linear pass over the entire list before en- coding any data. However, if the list can be written as iteration is performed, the corresponding read may begin in parallel, theoreti- cally offering an end-to-end speedup of (kN − C), where N is the size of the list, k the cost factor associated with serializing a sin- gle element, and C is fixed offset for the delay between data being written and becoming available to read.
Similarly, structs do not encode their data lengths a priori. Instead, they are encoded as a sequence of fields, with each field having a type specifier and a unique field identifier. Note that the inclusion of type specifiers allows the protocol to be safely parsed and decoded without any generated code or access to the original IDL file. Structs are terminated by a field header with a special STOP type. Because all the basic types can be read deterministically, all structs (even those containing other structs) can be read deterministically. The Thrift protocol is self-delimiting without any framing and regardless of the encoding format.
In situations where streaming is unnecessary or framing is advan- tageous, it can be very simply added into the transport layer, using the TFramedTransport abstraction.

4.3 实现

Facebook has implemented and deployed a space-efficient binary protocol which is used by most backend services. Essentially, it writes all data in a flat binary format. Integer types are converted to network byte order, strings are prepended with their byte length, and all message and field headers are written using the primitive integer serialization constructs. String names for fields are omitted - when using generated code, field identifiers are sufficient.
We decided against some extreme storage optimizations (i.e. pack- ing small integers into ASCII or using a 7-bit continuation for- mat) for the sake of simplicity and clarity in the code. These alter- ations can easily be made if and when we encounter a performance- critical use case that demands them.

5. Versioning

面对版本和数据定义改变,Thrift是健壮的。对于已经部署的服务,改变的阶段性健壮是必须的。系统必须支持 1. 从日志中读取旧数据 2. 接收来自过期客户端到新服务器的请求等等

5.1 Field Identifiers

Thrift中的版本是通过字段标识符来实现的。 最好的编程实践是清晰的制定字段标识符。

struct Example {
    1:i32 number=10,
    2:i64 bigNumber,                        
    3:double decimals,
    4:string name="thrifty"
}

5.2 Isset

5.3 Case Analysis

5.4 Protocol/Transport Versioning

6. RPC实现

6.1 TProcessor

Thrift设计中最后一个核心接口是TProcessor,可能是最简单的构造了。接口如下:

interface TProcessor {
    bool process(TProtocol in, TProtocol out)
        throws TException
}

这儿关键的设计观点是:我们创建的复杂系统从根本上可以看做是处理输入和输出的服务。大多数情况下,确实是一个需要处理的输入和输出系统。

6.2 Generated Code

当定义一个service时,我们生成一个能够处理RPC请求的TProcessor实例。基本结构如下:

Service.thrift
=> Service.cpp
interface ServiceIf
class ServiceClient : virtual ServiceIf
    TProtocol in
    TProtocol out
class ServiceProcessor : TProcessor
    ServiceIf handler


ServiceHandler.cpp
    class ServiceHandler : virtual ServiceIf

TServer.cpp
    TServer(TProcessor processor,
        TServerTransport transport,
        TTransportFactory tfactory,
        TProtocolFactory pfactory)
    serve()
  • 从Thrift定义文件,我们生成虚拟服务接口。
  • 生成的客户端类实现了接口,并使用两个TProtocol实例执行I/O操作。
  • 生成的processor实现了TProcessor接口
  • 生成的代码中有所有处理RPC请求的逻辑,通过process()调用来处理。
  • 用户提供一个应用接口的实现,这与生成的源码是独立的。

6.3 TServer

最后,Thrift 核心库提供了一个TServer的抽象。TServer对象通常如下工作:

  • 使用TServerTransport生成一个TTransport
  • 使用TTransportFactory将一个基本的transport转换为合适的应用层transport(典型的是使用TBufferedTransportFactory)
  • 使用TProtocolFactory 创建一个用于TTransport的input/output协议
  • 触发TProcessor对象的process()方法

这些层都是分开的,从而服务器代码不需要知道传输层、编码。server封装了连接处理、线程等逻辑。The only code written by the application developer lives in the definitional Thrift file and the interface implementation.

设计的TProcessor接口很通用。一个TServer没有带着一个生成的TProcessor对象。Thrift允许应用开发者写任何类型的server用于处理TProtocol对象。

7. 实现细节

7.1 Target Language

Thrift目前支持五种目标语言:C++, Java, Python, Ruby 和PHP。

7.2 Generated Structs

We made a conscious decision to make our generated structs as transparent as possible. All fields are publicly accessible; there are no set() and get() methods. Similarly, use of the isset object is not enforced. We do not include any FieldNotSetException construct. Developers have the option to use these fields to write more robust code, but the system is robust to the developer ignor- ing the isset construct entirely and will provide suitable default behavior in all cases.
This choice was motivated by the desire to ease application devel- opment. Our stated goal is not to make developers learn a rich new library in their language of choice, but rather to generate code that allow them to work with the constructs that are most familiar in each language.
We also made the read() and write() methods of the generated objects public so that the objects can be used outside of the con- text of RPC clients and servers. Thrift is a useful tool simply for generating objects that are easily serializable across programming languages.

7.3 RPC Method Identification

在RPC中是通过发送方法名来调用方法的。 这种方法的一个问题是需要更多的带宽。

在方法调用时,我们希望避免很多不必要的字符串比较。为了处理这个,我们生成字符串到函数指针的映射,因此通常情况下能够以常量时间完成方法调用。在C++中,我们使用一个相对深奥的语言构造:member function pointers。

std::map<std::string,
    void (ExampleServiceProcessor::*)(int32_t,
    facebook::thrift::protocol::TProtocol*,
    facebook::thrift::protocol::TProtocol*)>
    processMap_;

使用这些技术,处理字符串的消耗被降到最低。

7.4 Servers and Multithreading

Thrift服务需要基本的多线程来处理并发请求。对于Python和Java的Thrift服务器逻辑的实现,语言自带的线程库提供了足够的支持。对于C++的实现,没有标准的多线程实时库。特别地,健壮的、轻量的、可移植的线程管理和timer类不存在。我们研究了已有的实现,有boost::thread, boost:threadpool, ACE_Thread_Manager, ACE_TImer。

boost::threads:提供了多线程的轻量、健壮的实现;但是没有提供线程管理或timer实现。
boost::threadpool:看起来不错但还是没有达到我们的目标。我们希望尽可能的限制第三方以来。因为boost::threadpool不是一个纯净的模板库,还需要实时库,因为目前还不是官方boost的一部分。

7.5 Thread Primitives

在命名空间facebook::thrift::concurrency中实现了Thrift线程库,有三个组件:

  • primitives
  • thread pool manager
  • timer manager

正如上面提到的,我们在由于要不要在thrift中引入其他依赖。我们决定使用boost::shared_ptr,因为这对于多线程应用是很有用的,不需要link-time或runtime库,并且它编程了C++0x标准的一部分。

我们实现了标准的Mutex和Condition类,和一个Monitor类。后者是一个mutex和condition变量的组合,有点类似Java Object的Monitor实现。

This is also sometimes referred to as a barrier. We provide a Synchronized guard class to allow Java-like synchronized blocks. This is just a bit of syntactic sugar, but, like its Java counterpart, clearly delimits critical sec- tions of code. Unlike its Java counterpart, we still have the ability to programmatically lock, unlock, block, and signal monitors.

xxxx
xxxx
xxxx

7.6 Thread, Runnable, and shared_ptr

7.7 ThreadManager

7.8 TimerManager

7.9 Nonblocking Operation

7.10 Compiler

7.11 TFileTransport

laravel到lumen

在Lumen中,默认不会加载Facades,默认不会注册Provider,可以在bootstrap/app.php中看到。

  1. 关于Facades
    如果想使用Facades,那么修改bootstrap/app.php

    $app->withFacades();
    

    如果你自己定义了Facades的话,那么需要使用如下方式加载:

    $user_facades_alias = [
        App\Http\Facades\KLog::class => 'KLog',
    ];
    $app->withFacades(true, $user_facades_alias);
    

    当然不能忘了加载Provider,

    $app->register(App\Providers\KLogProvider::class);
    
  2. 关于加载配置文件

    在Laravel中,我们已经习惯config('config_file.config_item'),但是在Lumen中服务都是按需绑定并加载的。为了与Laravel中代码尽可能保持一致,方便二者代码的迁移,因此依然在bootstrap/app.php中修改,修改如下:

    $cfg_files = [
        'database_split',
        'database',
        'didicfg',
        'errno',
        'timeout',
    ];
    
    foreach ($cfg_files as $cfg_file) {
        $app->configure($cfg_file);
    }
    
  3. 关于异常处理

    我们知道在app/Exception/Handler.php中可以对特定的异常进行特别处理,对于render返回return [],laravel会自动转为json输出,而对于Lumen必须return json_encode([])才可以。

  4. 目前我知道的需要改变的地方是这些,以后有的话会继续补充

Laravel等框架压测

在学习Laravel过程中,发现大家都讲到Laravel性能比较差,又了解到Laravel为了性能专门有一个Lumen框架,为了更加客观的知道各个框架的性能,现在进行测试。

  1. 压测工具
    采用boom

    安装 pip install boom

  2. 压测参数

    boom your.domain -c 2000 -n 100

  3. 压测框架

    对CI2,YII2,Laravel5.3, Lument5.2进行压测

  4. 代码及输出

    都是在TestController中输出json串

    {“hello”:”world”,”hello2”:”world2”}

  5. 压测结果

    CI2>LUMENT5.2>YII2>LARAVEL5.3(优化过)>LARAVEL5.3(没有优化)
    开启OPCACHE以后性能有显著提高,如下图所示:

    PHP框架QPS对比.png

附注:Laravel优化的语句是

配置信息缓存 php artisan config:cache
路由缓存 php artisan route:cache
类映射加载优化 php artisan optimize
自动加载优化 composer dumpautoload

Laravel中日志类

问题描述及改进方法

  1. Laravel中原有的日志门面Log

    在Laravel中大家都知道Log,其用法是

    use Log;
    Log::info();
    

    但是这个存在一个问题,通过Log门面打印的日志是打印到一个Log文件中,不利于日志监控。

  2. 改进的日志门面KLog

    通过添加一个自定义的日志门面KLog,将日志打印到不同的文件中,并且自动实现日志按天切割。

实现日志门面KLog

所谓门面facade,简单概括为以静态语法的方式调用底层类。门面的详细解释可以参考核心概念 —— 门面(Facades)

在 Laravel 应用中,门面就是一个为容器中对象提供访问方式的类。该机制原理由 Facade 类实现。Laravel 自带的门面,以及我们创建的自定义门面,都会继承自 Illuminate\Support\Facades\Facade 基类。

门面类只需要实现一个方法:getFacadeAccessor。正是 getFacadeAccessor 方法定义了从容器中解析什么,然后Facade 基类使用魔术方法 __callStatic() 从你的门面中调用解析对象。

因此门面类KLog的实现如下

<?php
// filepath:app/Http/Facades/KLog.php
namespace App\Http\Facades;

use Illuminate\Support\Facades\Facade;


/**
 * @see \APP\Http\Logic\KLogLogic
 */
class KLog extends Facade
{
    protected static function getFacadeAccessor()
    {
       return 'KLog';//该方法的工作就是返回服务容器绑定类的别名
    }
}

KLog门面继承 Facade 基类并定义了 getFacadeAccessor 方法,该方法的工作就是返回服务容器绑定类的别名,当用户引用 KLog类的任何静态方法时,Laravel 从服务容器中解析 KLog绑定,然后在解析出的对象上调用所有请求方法。getFacadeAccessor返回的是在config/app.php中aliases的别名。

前面提到门面就是一个为容器中对象提供访问方式的类,那么服务容器是什么呢?可以参考核心概念-服务容器 。既然提到服务容器,就必然提到服务提供者。服务提供者是Laravel应用启动的中心,你自己的应用以及所有Laravel的核心服务都是通过服务提供者启动。

KLog类的服务提供者实现如下:

<?php
// filepath:app/Providers/KLogProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class KLogProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('KLog', function () {
            return new \App\Http\Logic\KLogLogic();
        });
    }
}

在register方法中,你唯一要做的事情就是绑事物到服务容器。

既然服务提供者和门面类都已经创建好,那么接下来我们需要进行配置,在config/app.php中进行如下配置:

a. aliases中增加KLog的配置,
b. providers中增加KLogProvider

'aliases' => [
    ...
    'View' => Illuminate\Support\Facades\View::class,
    'KLog' => App\Http\Facades\KLog::class,

],
'providers' => [
    ...
    App\Providers\RouteServiceProvider::class,
    App\Providers\KLogProvider::class,
],

当然我们忘了一件重要的事情,那就是KLogLogic类的实现,这才是真正门面KLog调用的底层类。

<?php
namespace App\Http\Logic;

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Illuminate\Log\Writer;

class KLogLogic 
{
    private static $writers = null;
    const MAX_FILES = 5; // 最多保留MAX_FILES天的日志

    public function getBizProc()
    {
        return $this->getLogger('biz_proc');
    }

    public function getBizProcErr()
    {
        return $this->getLogger('biz_proc_err');
    }

    private function getLogger($channel_name)
    {
        if (isset(self::$writers[$channel_name])) {
            return self::$writers[$channel_name];
        }

        $new_writer = new Writer(new Logger($channel_name));
        $log_file_path = storage_path() . '/logs/' . $channel_name . '.log';
        $new_writer->useDailyFiles($log_file_path, self::MAX_FILES);

        self::$writers[$channel_name] = $new_writer;
        return self::$writers[$channel_name];
    }
}

搞定以后,我们如何使用KLog门面呢?很简单,notice和info后面的参数与门面Log一样,因为都是调用的Writer类的接口

use KLog;

KLog::getBizProcErr()->info($msg);
KLog::getBizProcErr()->notice($msg);

如果要想添加新的日志,那么在KLogLogic中添加新的方法即可。

匿名函数与闭包

这篇文章翻译了Anonymous Functions VS Closures

是不是很想知道匿名函数和闭包之间的关系?Lambda呢?这些在PHP中是如何实现的呢?在这个领域有很多困惑,现在让我们来深挖并回答这些问题。

第一件事:Lambda是匿名函数的别称。在一些编程语言(如python)中使用Lambda。

接下来让我们看看匿名函数和闭包到底是什么。如果现在你很困惑,不用担心,接下来我希望能够让你明白,下面是两者的一个简单定义:

  • 匿名函数:一个匿名函数是一个定义的偶尔触发的函数,不需要与标识符绑定。(An Anonymous Function is a function that is defined, and occasionally invoked, without being bound to an identifier. It also has the variable scoping characteristics of a Closure (see below))。
  • 闭包函数:闭包是一个能够捕获当前包含作用域,能够获取父作用域的函数。闭包将非局部变量的引用绑定到了闭包创建时的作用域。(A Closure is a function that captures the current containing scope, then provides access to that scope when the Closure is invoked. The Closure binds non-local variable references to the corresponding variables in scope at the time the Closure is created.)

从这儿可以看到:所有的匿名函数都是闭包,但并不是所有的闭包都是匿名函数。

用外行的话来说:

所有的匿名函数都是没有名字的闭包。闭包是一个函数,在闭包定义的时候会将非局部变量绑定为函数的局部变量。(原文:An Anonymous Function is a Closure without a name. A Closure is a function which binds references to non-local variables to local variables inside the function at the time of the Closure definition.)

关于PHP呢?

事情变得有趣了。在PHP5.3中加入了匿名函数和闭包。但是,有一些取巧的地方(其中一个已经在PHP5.4中修复)

匿名函数的实现是Closure对象。PHP贯彻了观点匿名函数是一个无名的闭包

有一个简单的匿名函数传递给array_map函数,目的是将所有的整数都乘以2。

$arr = array_map(function ($val) {
return is_int($val) ? $val * 2 : $val;
}, $arr);

很简答,对不?现在问一下自己:如果你想在函数内部访问非局部变量(non-local variables)?毕竟匿名函数也是闭包。当然可以,但是你必须清晰告诉PHP使用哪些非局部变量,这是诸多取巧的第一个。现在开始深挖。

####取巧1:
当使用非局部变量时,必须使用关键字use将这些非局部变量绑定到闭包的作用域。这与大部分其他语言都不一样。

$foo = 'foo';
$bar = 'bar';

$baz = function () use ($foo, $bar) {
    echo $foo, $bar;
};

【译者备注】这儿绑定到闭包的作用域,是将这些非局部变量作为了闭包的静态变量

####取巧2:
绑定的非局部变量是复制,而不是引用。如果你想在闭包中改变变量的值,那么你必须使用引用传递。

$foo = 'foo';
$bar = 'bar';

$baz = function () use (&$foo, &$bar) {
    $foo = 'Hello ';
    $bar = 'World!';
};
$baz();
echo $foo, $bar; // Outputs "Hello World!";

####取巧3:
在PHP5.3中,在类中使用闭包,闭包不能使用$this。你必须传递this的引用,而不能直接传递$this
在PHP5.4中修复,可以直接传递$this

PHP函数之匿名函数

匿名函数与闭包的关系

匿名函数是一类不需要指定指示符,而又可以被调用的函数或子例程,匿名函数可以方便的作为参数传递给其他函数, 最常见应用是作为回调函数。

闭包是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数, 这个被引用的自由变量将和这个函数一同存在,即使离开了创建它的环境也一样,所以闭包也可认为是有函数和与其相关引用组合而成的实体。

闭包和匿名函数很容易混用,但其实这是两个不同的概念。

当然在PHP中,一个观点就是Anonymous Functions are Closures without a name,即匿名函数是没有名字的闭包。

PHP中匿名函数基本用法

在PHP中匿名函数(Anonymous functions)也叫作闭包函数(closures),允许临时创建一个没有指定名称的函数。

  1. 使用use关键字从父作用域继承变量。继承的变量的取值是在函数定义时的值,而不是被调用的时候的值。因此下面的代码输出是hello hello

    <?php
    $helo = 'hello';
    
    $func = function() use ($helo) {
        echo $helo . "\n";
    };
    $func();
    
    $helo = 'world';
    $func();
    
  2. 当把use后面的变量改为引用以后,那么就可以改变了,输出 hello world

    <?php
    $helo = 'hello';
    
    $func = function() use (&$helo) {
        echo $helo . "\n";
    };
    $func();
    
    $helo = 'world';
    $func();
    

PHP中匿名函数

create_function

PHP5.3中才开始正式支持匿名函数,在此之前可以通过create_function函数来创建匿名函数

通过create_function函数创建的匿名函数并不是真的没有名字,而是生成了一个\0lambda_xxx的函数名,并且会在EG(function_table)中注册。因为函数名的第一个字符是\0,这样的函数名用户是不会出现的。

#define LAMBDA_TEMP_FUNCNAME    "__lambda_func"

ZEND_FUNCTION(create_function)
{
    // ... 省去无关代码
    function_name = (char *) emalloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG);
    function_name[0] = '\0';  // <--- 这里
    do {
        function_name_length = 1 + sprintf(function_name + 1, "lambda_%d", ++EG(lambda_count));
    } while (zend_hash_add(EG(function_table), function_name, function_name_length+1, &new_function, sizeof(zend_function), NULL)==FAILURE);
    zend_hash_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME));
    RETURN_STRINGL(function_name, function_name_length, 0);
}

通过create_function创建匿名函数有一些缺点:

  1. 函数的定义是通过将函数拼接为一个字符串来处理的。深入理解php之匿名函数解释,这样的话就无法进行基本的语法检查
  2. 这种函数与普通函数没有本质区别,无法实现闭包的效果

真正的匿名函数

  1. __invoke魔术方法
    这个魔术方法被调用的时机是:当一个对象当做函数调用的时候。通过__invoke方法可以把一个对象当做函数调用。

  2. 匿名函数的实现

    匿名函数的实现就是通过__invoke方式实现的。匿名函数是一个普通的类,类名是Closure

  3. 闭包的使用

    PHP使用闭包(Closure)来实现匿名函数。匿名函数在定义的时候如果需要使用作用域外的变量需要使用关键字use

    特别注意的是,在PHP中是通过拷贝的方式将上层变量传入匿名函数的。如果需要改变上层变量的值则需要通过引用的方式传递。

通过下面的代码看一下func的内容

    <?php
    $b = 1;
    $func = function($a) use($b) {
        $b = $b+1;
        echo $b . "\n";
    };
    var_dump($func);
可以看到

    object(Closure)#1 (2) {
      ["static"]=>
      array(1) {
        ["b"]=>
        int(1)
      }
      ["parameter"]=>
      array(1) {
        ["$a"]=>
        string(10) "<required>"
      }
    }

将代码中的`use ($b)` 改为`use (&$b)`,可以看到输出是:

    object(Closure)#1 (2) {
      ["static"]=>
      array(1) {
        ["b"]=>
        &int(1)
      }
      ["parameter"]=>
      array(1) {
        ["$a"]=>
        string(10) "<required>"
      }
    }
  1. 闭包的实现

    匿名函数是通过闭包来实现的,现在我们来看看闭包(类)是怎么实现的。匿名函数和普通函数除了是否有变量名以外并没有区别,闭包的实现代码在Zend/zend_closure.c中。

    使用关键字use引入的父作用域的变量会作为闭包类的静态变量,从上面的var_dump中也可以看出。

    ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST_CONST_HANDLER中调用zend_create_closure创建一个闭包对象。在zend_create_closure中会将zval_copy_static_var作为回调函数传递给zend_hash_apply_with_arguments。每次读取到hash表中的值以后都会由这个函数进行处理,而这个函数对所有use语句定义的变量值赋值给这个匿名函数的静态变量, 这样匿名函数就能访问到use的变量了。

##参考文章

  1. Anonymous Functions VS Closures
  2. 匿名函数及闭包
  3. 深入理解PHP之匿名函数
  4. PHP匿名函数基本用法

4. PHP变量之常量

首先看下常量与变量的区别,常量是在变量的zval结构的基础上添加了一额外的元素。如下所示为PHP中常量的内部结构。
常量的内部结构:

typedef struct _zend_constant {
zval value; /* zval结构,PHP内部变量的存储结构,在第一小节有说明 */
int flags;  /* 常量的标记如 CONST_PERSISTENT | CONST_CS */
char *name; /* 常量名称 */
uint name_len;  
int module_number;  /* 模块号 */
} zend_constant;

在了解了常量的存储结构后,来看PHP常量的定义过程,一个例子:

define('TIPI', 'Thinking In PHP Internal');

define定义常量的过程

define是PHP的内置函数,在zend/zend_builtin_functions.c文件中定义了此函数的实现。最后会调用zend_register_constant。在zend_register_constant中将常量注册到EG(zend_constants)中。

常量zend_constant的标记flag可以有CONST_CS、CONST_PERSISTENT、CONST_CT_SUBST的取值。

CONST_PERSISTENT表示这个常量需要持久化。这里的持久化是指内存申请时的持久化。非持久常量会在请求结束RSHUTDOWN阶段时释放该常量。持久化常量只会在MSHUTDOWN阶段将内存释放。用户空间定义的常量都是非持久化的,通常扩展和内核定义的常量会设置为持久化。

CONST_ST_SUBST可以知道其表示Allocw compile-time substitution(在编译时可被替换)。在php内核中这些常量表示:TRUE、FALSE、NULL、ZEND_THREAD_SAFE和ZEND_DEBUG_BUILD五个。

标准常量的初始化

通过define()函数定义的常量的模块编号是PHP_USER_CONSTANT,是用户定义的常量。
对于错误报告级别E_ALL, E_WARNING等常量就不同了,是PHP内置定义的常量属于标准常量。调用是php_module_startup()->zend_startup()->zend_register_standard_constants()->zend_register_constant。

zend_register_constant

对于define定义常量和标准常量都会调用zend_register_constant。该函数的核心是调用了
zend_hash_quick_add(EG(zend_constants), name, c->name_len, chash, (void *) c, ...)
其中EG(zend_constants)即全局变量execute_gloabls.zend_constants

即常量都是在execute_gloabls.zend_constants这个哈希表中

参考文章

PHP变量之常量