nginx 模块解析

      nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被连接的模块,这里虽说是模块连接,但nginx不会像apache或lighttpd那样在编译时生成so动态库而在程序执行时再进行动态加载,nginx模块源文件会在生成nginx时就直接被编译到其二进制执行文件中,所以如果要选用不同的功能模块,必须对nginx做重新配置和编译。对于功能模块的选择,如果要修改默认值,需要在进行configure时进行指定,比如新增http_flv功能模块(默认是没有这个功能的,各个选项的默认值可以在文件auto/options内看到)

    [root@localhost nginx-1.2.0]# ./configure --with-http_flv_module

执行后,生成的objs/ngx_modules.c文件内就包含有对ngx_http_flv_module模块的引用了,要再去掉http_flv功能模块,则需要重新configure,即不带--with-http_flv_module配置后再编译生成新的nginx执行程序。通过执行./configure –help,我们可以看到更多的配置选项。

   虽然nginx的模块的很多,并且某个模块的功能各不相同,但是可以根据功能特性,我们大致可以分为四类:

   1) handlers : 处理客户端请求并产生响应内容,比如ngx_http_static_moudle模块,负责客户端的静态页面请求,并将对应的静态磁盘文件作为响应内容输出.

   2)filters : 对handlers产生的响应内容做各种过滤处理(即增,删,改),比如 ngx_http_not_modify_filter_moudle,如果通过时间判断前后2次请求的响应内容没有发生任何改变,那么可以直接响应"304 Not Modified"状态标识,让客户端使用缓存即可,而原本发送的响应内容将被清除掉.

  3)upstream : 如果存在后端真实的服务器,nginx 可以利用upstream模块充当反向代理的角色,对客户端的请求只负责转发到后端的真实服务器,如ngx_http_proxy_moudle模块.

  4)load-balance : 在nginx充当中间代理时,由于后端真实服务器往往多于一个,对于某一次客户端的请求,如何选择对应的后端真实服务器来进行处理,这就有类似于ngx_http_upstream_ip_hash_module这样的模块来实现不同的负载均衡算法(Load Balance)。

 

在此,我们先来了解一些数据结构:

struct ngx_module_s {ngx_uint_t            ctx_index; //在同类模块中的序号ngx_uint_t            index;     //在所有模块中序号ngx_uint_t            spare0;ngx_uint_t            spare1;ngx_uint_t            spare2;ngx_uint_t            spare3;ngx_uint_t            version;  //当前模块的版本号void                 *ctx;     //指向当前模块特有的数据ngx_command_t        *commands;  //指向当前模块配置项解析数组ngx_uint_t            type;   //模块的类型//回调函数ngx_int_t           (*init_master)(ngx_log_t *log);ngx_int_t           (*init_module)(ngx_cycle_t *cycle);ngx_int_t           (*init_process)(ngx_cycle_t *cycle);ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);void                (*exit_thread)(ngx_cycle_t *cycle);void                (*exit_process)(ngx_cycle_t *cycle);void                (*exit_master)(ngx_cycle_t *cycle);//保留字uintptr_t             spare_hook0;uintptr_t             spare_hook1;uintptr_t             spare_hook2;uintptr_t             spare_hook3;uintptr_t             spare_hook4;uintptr_t             spare_hook5;uintptr_t             spare_hook6;uintptr_t             spare_hook7;
};

    结构体ngx_module_s中值得注意的几个字段 ctx , commands, type,其中commands字段表示当前模块的可以解析的配置项目,表示模块类型的type值只有5种,而同一类型的模块的ctx数据类型都是相同的。

序号

type值

ctx指向数据类型

1

NGX_CORE_MODULE

ngx_core_module_t

2

NGX_EVENT_MODULE

ngx_event_module_t

3

NGX_CONF_MODULE

NULL

4

NGX_HTTP_MODULE

ngx_http_module_t

5

NGX_MAIL_MODULE

ngx_mail_module_t

typedef struct {ngx_str_t             name;void               *(*create_conf)(ngx_cycle_t *cycle);char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;
............................
typedef struct {ngx_str_t              *name;void                 *(*create_conf)(ngx_cycle_t *cycle);char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);ngx_event_actions_t     actions;
} ngx_event_module_t;
........................typedef struct {ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);void       *(*create_main_conf)(ngx_conf_t *cf);char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);void       *(*create_srv_conf)(ngx_conf_t *cf);char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);void       *(*create_loc_conf)(ngx_conf_t *cf);char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

    上面表中的第3列的数据类型比较重要,它的字段基本上都是一些回调函数,这些回调函数会在其模块对应的配置文件解析过程 前/中/后 会适时被调用,做一些内存准备,初始化,配置值检查,配置值填充,合并,回调函数挂载等初始工作.

    下面我们以ngx_http_core_moudle模块为例,type 为 NGX_HTTP_MOUDLE, ctx 指向ngx_http_moudle_t结构体变量ngx_http_core_module_ctx.

static ngx_http_module_t  ngx_http_core_module_ctx = {ngx_http_core_preconfiguration,        /* preconfiguration */NULL,                                  /* postconfiguration */ngx_http_core_create_main_conf,        /* create main configuration */ngx_http_core_init_main_conf,          /* init main configuration */ngx_http_core_create_srv_conf,         /* create server configuration */ngx_http_core_merge_srv_conf,          /* merge server configuration */ngx_http_core_create_loc_conf,         /* create location configuration */ngx_http_core_merge_loc_conf           /* merge location configuration */
};

     根据上面的代码,我们可以很明显看到各个回调函数的回调时机,例如:ngx_http_core_preconfiguration将在进行http块配置解析前被调用,所以内在ngx_http_block()函数里看到这样的代码:

static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{for (m = 0; ngx_modules[m]; m++) {if (ngx_modules[m]->type != NGX_HTTP_MODULE) {continue;}module = ngx_modules[m]->ctx;if (module->preconfiguration) {if (module->preconfiguration(cf) != NGX_OK) {return NGX_CONF_ERROR;}}}
}

至于这些回调函数内的具体逻辑,如前所述一般是一些初始或默认值填充工作,但也有回调函数挂载的设置,比如ngx_http_static_module模块的postconfiguration字段回调函数ngx_http_static_init()就是将自己的处理函数ngx_http_static_handler()挂载在http处理状态机上,但总体来看这毕竟都只是一些简单的初始准备工作.

   handlers模块

      对于客户端http请求过程,为了获得更强的控制力,nginx将其细分成多个阶段处理,每个阶段都有零个或多个回调函数专门处理,当我们编写handler模块的时候,必须把模块功能挂载在正确的阶段点。如前面所描述的ngx_http_static_moudle 将自己的功能模块处理函数ngx_http_static_handler()挂载在NGX_HTTP_CONTENT_PHASE阶段.

     Http请求处理过程一共分为11个阶段,每一个阶段对应的处理功能都比较单一,这样能尽量让nginx模块代码更为内聚:

    

序号

阶段宏名

阶段描述

0

NGX_HTTP_POST_READ_PHASE

读取请求内容阶段

1

NGX_HTTP_SERVER_REWRITE_PHASE

Server请求地址重写阶段

2

NGX_HTTP_FIND_CONFIG_PHASE

配置查找阶段

3

NGX_HTTP_REWRITE_PHASE

Location请求地址重写阶段

4

NGX_HTTP_POST_REWRITE_PHASE

请求地址重写提交阶段

5

NGX_HTTP_PREACCESS_PHASE

访问权限检查准备阶段

6

NGX_HTTP_ACCESS_PHASE

访问权限检查阶段

7

NGX_HTTP_POST_ACCESS_PHASE

访问权限检查提交阶段

8

NGX_HTTP_TRY_FILES_PHASE

配置项try_files处理阶段 

9

NGX_HTTP_CONTENT_PHASE

内容产生阶段

10

NGX_HTTP_LOG_PHASE

日志模块处理阶段

    并非某个阶段都能挂载自定义的回调函数,比如NGX_HTTP_TRY_FILE_PHASE阶段就是针对配置项try_files的特定处理阶段段。NGX_HTTP_FIND_CONFIG_PHASE、NGX_HTTP_POST_ACCESS_PHASE与NGX_HTTP_POST_REWRITE_PHASE这三个阶段也是为了完成nginx特定的功能,就算给这几个阶段加上回调函数,也永远不会被调用。我们的自定义模块回调函数挂载在NGX_HTTP_CONTENT_PHASE阶段的情况比较多,毕竟大部分情况下的业务需求是修改HTTP响应数据,nginx自身的产生响应内容的模块,像ngx_http_static_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_gzip_static_module、ngx_http_dav_module等都是挂载在这个阶段。

 

   大多数情况下,功能模块会在其对应配置解析完后的回调函数,也就是ngx_http_moudle_t结构体的postconfiguration字段指向的函数内将当前模块的回调功能函数挂载到这11个阶段其中一个上.

以ngx_http_static_module为例:

ngx_http_module_t  ngx_http_static_module_ctx = {NULL,                                  /* preconfiguration */ngx_http_static_init,                  /* postconfiguration */NULL,                                  /* create main configuration */NULL,                                  /* init main configuration */NULL,                                  /* create server configuration */NULL,                                  /* merge server configuration */NULL,                                  /* create location configuration */NULL                                   /* merge location configuration */
};static ngx_int_t
ngx_http_static_init(ngx_conf_t *cf)
{ngx_http_handler_pt        *h;ngx_http_core_main_conf_t  *cmcf;cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);if (h == NULL) {return NGX_ERROR;}*h = ngx_http_static_handler;  return NGX_OK;

    在模块ngx_http_static_module的postconfiguration回调函数ngx_http_static_init()内,将ngx_http_static_module模块的核心功能函数ngx_http_static_handler()挂载在Http请求处理流程中的NGX_HTTP_CONTENT_PHASE阶段。这样,当一个客户端的http静态页面请求发送到nginx服务器,nginx就能够调用到我们这里注册的ngx_http_static_handler()函数,

   

各个功能模块将其自身的功能函数挂载在cmcf->phases后,内部的情况如下图所示: 

回调函数会根据模块的不同而不同.这些回调函数的调用都时有条件的,调用后也要做一些根据返回值的结果处理.比如某次处理是否进入到阶段NGX_HTTP_CONTENT_PARSE的回调函数的处理,这需要一个事前判断.所以在函数ngx_http_init_phase_handlers()里对所有这个回调函数进行一次重组.

struct ngx_http_phase_handler_s {ngx_http_phase_handler_pt checker;  //阶段检查函数ngx_http_handler_pt handler;ngx_uint_t next;
};

     但从上图中可以看到,该函数只把有回调函数的处理阶段给提取了出来,同时利用ngx_http_phase_handler_t结构体数组对这些回调函数进行重组,不仅加上了进入回调函数的条件判断checker函数,而且通过next字段的使用,把原本的二维数组实现转化为可直接在一维函数数组内部跳动;一般来讲,二维数组的遍历需要两层循环,而遍历一维函数数组就只需一层循环。

     再来看对http请求进行分段处理的核心函数ngx_http_core_run_phase:

void
ngx_http_core_run_phases(ngx_http_request_t *r)
{ngx_int_t                   rc;ngx_http_phase_handler_t   *ph;ngx_http_core_main_conf_t  *cmcf;cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);ph = cmcf->phase_engine.handlers;while (ph[r->phase_handler].checker) {rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);if (rc == NGX_OK) {return;}}
}
ngx_http_core_run_phases函数中r->phase_handler标志当前处理的序号,对于一个客户端的最开始的请求的时刻, 该值当然就是0了,while循环判断如果存在checker函数(末尾数组元素的checker函数为null),那就调用该checker函数并有可能调用相应的回调函数,以NGX_HTTP_ACCESS_PHASE阶段的ngx_http_core_access_phase()函数为例:
ngx_int_t
ngx_http_core_access_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{if (r != r->main) {r->phase_handler = ph->next;return NGX_AGAIN;}rc = ph->handler(r);if (rc == NGX_DECLINED) {r->phase_handler++;return NGX_AGAIN;}if (rc == NGX_AGAIN || rc == NGX_DONE) {return NGX_OK;} ngx_http_finalize_request(r, rc);return NGX_OK;
}

可以看到,一个功能模块的handler函数可以返回多种类型的值,并且这些值有其固有的含义:

序号

返回值

含义

1

NGX_OK

当前阶段已经被成功处理,必须进入到下一个阶段

2

NGX_DECLINED

当前回调不处理当前情况,进入到下一个回调处理

3

NGX_AGAIN

当前处理所需资源不足,需要等待所依赖事件发生

4

NGX_DONE

当前处理结束,仍需等待进一步事件发生后做处理

5

NGX_ERROR, NGX_HTTP_…

当前回调处理发生错误,需要进入到异常处理流程



Filter模块:
对于http请求处理handlers产生的响应内容,在输出客户端之前需要做过滤处理,这些过滤处理对于完整功能的增强实现和性能的提升是非常有必要的,比如过滤模块ngx_http_chunked_filter_moudle,那么就无法完整支持http中chunk的功能。如果没有ngx_http_not_modified_filter_module过滤模块,那么就无法让客户端使用本地缓存来提高性能;诸如这些都需要过滤模块的支持。由于响应数据包括响应头和响应体,所以以此对应,任一filter模块必须提供处理响应头的header过滤函数(比如ngx_http_not_modified_filter_module模块提供的ngx_http_not_modified_header_filter()函数)或处理响应体的body过滤功能函数(比如ngx_http_copy_filter_module模块提供的ngx_http_copy_filter()函数)或两者皆有(比如ngx_http_chunked_filter_module模块提供的ngx_http_chunked_header_filter()函数和ngx_http_chunked_body_filter()函数)。

  所有的header过滤功能函数和body过滤功能函数会分别组成各自的两条过滤链,如下图所示(使用附录A所列模块):

     这2条过滤链怎么形成的呢?在源文件ngx_http.c中,可以看到有2个函数指针变量:

   ngx_int_t  (*ngx_http_top_header_filter) (ngx_http_request_t *r);

   ngx_int_t  (*ngx_http_top_body_filter) (ngx_http_request_t *r, ngx_chain_t *ch);

    这是整个nginx范围内可见的全局变量;然后在每一个filter模块内,我们还会看到类似于这样的定义(如果当前模块只有header过滤功能函数或只有body过滤功能函数,那么如下定义也就只有相应的那个变量):

         static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;

         static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;

        注意到static修饰符,也就是说这两个变量是属于模块范围内可见的局部变量。有了这些函数指针变量,再在各个filter模块的postconfiguration回调函数(该函数会在其对应配置解析完后被调用做一些设置工作,前面已经描述过)内,全局变量与局部变量的巧妙赋值使得最终行成了两条过滤链。以header过滤链为例,通过附录A的模块列表ngx_modules变量,可以看到ngx_http_header_filter_module是具有header过滤功能函数的序号最小的过滤模块,其postconfiguration回调函数如下:

618:    ngx_http_header_filter_init(ngx_conf_t *cf)
619:    {
620:        ngx_http_top_header_filter = ngx_http_header_filter;
621:    
622:        return NGX_OK;
623:    }
232:    static ngx_int_t
233:    ngx_http_chunked_filter_init(ngx_conf_t *cf)
234:    {
235:        ngx_http_next_header_filter  = ngx_http_top_header_filter;
236:        ngx_http_top_header_filter   = ngx_http_chunked_header_filter;}

        其它过滤模块的类此加入,逐步形成最终的完整header过滤链;当然,body过滤链的形成过程也与此类似。两条过滤链形成后,其对应的调用入口分别在函数ngx_http_send_header()和函数ngx_http_output_filter()内:

1889:    ngx_int_t
1890:    ngx_http_send_header(ngx_http_request_t *r)
1891:    {
1892:    …
1897:        return ngx_http_top_header_filter(r);
1898:    }
1899:    
1901:    ngx_int_t
1902:    ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
1903:    {
1904:    …
1912:        rc = ngx_http_top_body_filter(r, in);
1913:    …
1919:        return rc;
1920:    }

      这两个函数非常简单,主要是通过过滤链的链头函数指针全局变量进入到两条过滤链内,进而依次执行链上的各个函数。比如这里ngx_http_top_header_filter指向的是ngx_http_not_modified_header_filter()函数,因此进入到该函数内执行,而在该函数的执行过程中又会根据情况,继续通过当前模块内的函数指针局部变量ngx_http_next_header_filter间接的调用到header过滤链的下一个过滤函数,这对保证过滤链的前后承接是非常必要的,除非我们遇到无法继续处理的错误,此时只有返回NGX_ERROR这样的值:   

52:    static ngx_int_t
53:    ngx_http_not_modified_header_filter(ngx_http_request_t *r)
54:    {
55:    …
70:        return ngx_http_next_header_filter(r);
71:    }

        根据HTTP协议具备的响应头影响或决定响应体内容的特点,所以一般是先对响应头进行过滤,根据头过滤处理返回值再对响应体进行过滤处理,如果在响应头过滤处理中出错或某些特定情况下,响应体过滤处理可以不用再进行。

         

转载于:https://www.cnblogs.com/fll369/archive/2012/11/28/2793417.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/377055.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python关键字和保留字_Python关键字

python关键字和保留字关键词 (Keywords) Keywords are the reserved words in Python programming language (and, any other programming languages like C, C, Java, etc) whose meanings are defined and we cannot change their meanings. In python programming languages…

《LeetcodeHot100非困难题补录》

最近比较闲,也比较焦虑,刷刷题吧 目录11. 盛最多水的容器22. 括号生成31. 下一个排列48. 旋转图像49. 字母异位词分组56. 合并区间75. 颜色分类79. 单词搜索114. 二叉树展开为链表141. 环形链表148. 排序链表152. 乘积最大子数组169. 多数元素207. 课程表…

Java里String.split需要注意的用法

我们常常用String的split()方法去分割字符串,有两个地方值得注意: 1. 当分隔符是句号时("."),需要转义: 由于String.split是基于正则表达式来分割字符串,而句号在正则表达式里表示任意字符。 //Wrong: //Str…

C# Socket 例子(控制台程序)

服务器代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; using System.IO;namespace TCPListener {class Program{static void Main(string[] args){const int BufferSize 1024;Con…

Scala中的值类

Value classes are a special mechanism in Scala that is used to help the compiler to avoid allocating run time objects. 值类是Scala中的一种特殊机制,用于帮助编译器避免分配运行时对象。 This is done by defining a subclass of AnyVal. The only parame…

《MySQL8.0.22:Lock(锁)知识总结以及源码分析》

目录1、关于锁的一些零碎知识,需要熟知事务加锁方式:Innodb事务隔离MVCC多版本并发控制常用语句 与 锁的关系意向锁行级锁2、锁的内存结构以及一些解释3、InnoDB的锁代码实现锁系统结构lock_sys_tlock_t 、lock_rec_t 、lock_table_tbitmap锁的基本模式的…

关于ORA-04021解决办法(timeout occurred while waiting to lock object)

某个应用正在锁定该表或者包 表为 select b.SID,b.SERIAL#,c.SQL_TEXT from v$locked_object a, v$session b, v$sqlarea c where a.SESSION_ID b.SID and b.SQL_ADDRESS c.ADDRESS and c.sql_text like %table_name% 包为 select B.SID,b.USERNAME,b.MACHINE FROM V$ACCESS …

HtmlAutoTestFrameWork

前段时间做的自动化测试的是Silverlight的,框架都已经搭好。突然测试发现这里还有一个要发送邮件的html页面,并且将另外启动浏览器,于是今天下午把这个html的也写出来。用法 : HtmlAutoTestFrameWork htf new HtmlAutoTestFrameW…

L8ER的完整形式是什么?

L8ER:稍后 (L8ER: Later) L8ER is an abbreviation of "Later". L8ER是“ Later”的缩写 。 It is an expression, which is commonly used in messaging or chatting on social media networking sites like Facebook, Yahoo Messenger, and Gmail, etc…

Randomize select algorithm 随机选择算法

从一个序列里面选择第k大的数在没有学习算法导论之前我想最通用的想法是给这个数组排序,然后按照排序结果返回第k大的数值。如果使用排序方法来做的话时间复杂度肯定至少为O(nlgn)。 问题是从序列中选择第k大的数完全没有必要来排序&#xff…

《Linux杂记:一》

目录CPU负载和CPU利用率CPU负载很高,利用率却很低的情况负载很低,利用率却很高常用linux命令常用的文件、目录命令常用的权限命令常用的压缩命令CPU负载和CPU利用率 可以通过 uptime , w 或者 top 命令看到CPU的平均负载。 Load Average :负载的3个数字,比如上图的0.57、0.4…

IOS Plist操作

代码:copy BUNDLE下的plist文件 到 library下面。 bundle下不支持些,library,doc路径支持读与写。 (void)copyUserpigListToLibrary {NSFileManager *fileManager [NSFileManager defaultManager];NSArray *paths NSSearchPathForDirector…

《线程管理:线程基本操作》

目录线程管理启动线程与(不)等待线程完成特殊情况下的等待(使用trycath或rall)后台运行线程线程管理 启动线程与(不)等待线程完成 提供的函数对象被复制到新的线程的存储空间中,函数对象的执行…

scala特质_Scala的特质

scala特质Scala特质 (Scala traits) Traits in Scala are like interfaces in Java. A trait can have fields and methods as members, these members can be abstract and non-abstract while creation of trait. Scala中的特性类似于Java中的接口 。 特征可以具有作为成员的…

优化PHP代码的40条建议(转)

优化PHP代码的40条建议 40 Tips for optimizing your php Code 原文地址:http://reinholdweber.com/?p3 英文版权归Reinhold Weber所有,中译文作者yangyang(aka davidkoree)。双语版可用于非商业传播,但须注明英文版作…

Iptables入门教程

转自:http://drops.wooyun.org/tips/1424 linux的包过滤功能,即linux防火墙,它由netfilter 和 iptables 两个组件组成。 netfilter 组件也称为内核空间,是内核的一部分,由一些信息包过滤表组成,这些表包含内…

《线程管理:传递参数、确定线程数量、线程标识》

参考《c Concurrency In Action 》第二章做的笔记 目录传递参数量产线程线程标识传递参数 thread构造函数的附加参数会拷贝至新线程的内存空间中,即使函数中的采纳数是引用类型,拷贝操作也会执行。如果我们期待传入一个引用,必须使用std::re…

手把手玩转win8开发系列课程(14)

这节的议程就是——添加appbar appbar是出现在哪儿了,出现在屏幕的底部。他能使用户能用手势或者使用鼠标操作程序。metro UI 重点是在主要的控件使用许多控件,使其用户使用win8电脑更加的方便。而appBar使其用户体验更好。在这节中,我将告诉…

No identities are available for signing 的解决办法

今天重新上传做好的app提交到app store,结果就出现标题上的错误。“No identities are available for signing”。 以后碰到这样的问题按照下面几个步骤来做: 进入Distribution -----下载发布证书 -----双击安装-----重启Xcode就能上传了 其他细节 如果再…

半连接反连接

半连接&反连接 1. 半连接 半连接返回左表中与右表至少匹配一次的数据行,通常体现为 EXISTS 或者 IN 子查询。左表驱动右表。只返回左表的数据,右表作为筛选条件。 可以用 EXISTS、 IN 或者 ANY 举例:表t1和表t2做半连接,t…