有趣的流
php常被提起的一个特性是流上下文. 这个可选的参数甚至在用户空间大多数流创建相关的函数中都可用, 它作为一个泛化的框架用于向给定包装器或流实现传入/传出额外的信息.
上下文
每个流的上下文包含两种内部消息类型. 首先最常用的是上下文选项. 这些值被安排在上下文中一个二维数组中, 通常用于改变流包装器的初始化行为. 还有一种则是上下文参数, 它对于包装器是未知的, 当前提供了一种方式用于在流包装层内部的事件通知.
php_stream_context *php_stream_context_alloc(void);
通过这个API调用可以创建一个上下文, 它将分配一些存储空间并初始化用于保存上下文选项和参数的HashTable. 还会自动的注册为一个请求终止后将被清理的资源.
设置选项
设置上下文选项的内部API和用户空间的API是等同的:
int php_stream_context_set_option(php_stream_context *context, const char *wrappername, const char *optionname, zval *optionvalue);
下面是用户空间的原型:
bool stream_context_set_option(resource $context, string $wrapper, string $optionname, mixed $value);
它们的不同仅仅是用户空间和内部需要的数据类型不同.下面的例子就是使用这两个API调用, 通过内建包装器发起一个HTTP请求, 并通过一个上下文选项覆写了user_agent设置.
php_stream *php_varstream_get_homepage(const char *alt_user_agent TSRMLS_DC) { php_stream_context *context; zval tmpval; context = php_stream_context_alloc(TSRMLS_C); ZVAL_STRING(&tmpval, alt_user_agent, 0); php_stream_context_set_option(context, "http", "user_agent", &tmpval); return php_stream_open_wrapper_ex("http://www.php.net", "rb", REPORT_ERRORS | ENFORCE_SAFE_MODE, NULL, context); }
译者使用的php-5.4.10中php_stream_context_alloc()增加了线程安全控制, 因此相应的对例子进行了修改, 请读者测试时注意.
这里要注意的是tmpval并没有分配任何持久性的存储空间, 它的字符串值是通过复制设置的. php_stream_context_set_option()会自动的对传入的zval内容进行一次拷贝.
取回选项
用于取回上下文选项的API调用正好是对应的设置API的镜像:
int php_stream_context_get_option(php_stream_context *context, const char *wrappername, const char *optionname, zval ***optionvalue);
回顾前面, 上下文选项存储在一个嵌套的HashTable中, 当从一个HashTable中取回值时, 一般的方法是传递一个指向zval **的指针给zend_hash_find(). 当然, 由于php_stream_context_get_option()是zend_hash_find()的一个特殊代理, 它们的语义是相同的.
下面是内建的http包装器使用php_stream_context_get_option()设置user_agent的简化版示例:
zval **ua_zval; char *user_agent = "PHP/5.1.0"; if (context && php_stream_context_get_option(context, "http", "user_agent", &ua_zval) == SUCCESS && Z_TYPE_PP(ua_zval) == IS_STRING) { user_agent = Z_STRVAL_PP(ua_zval); }
这种情况下, 非字符串值将会被丢弃, 因为对用户代理字符串而言, 数值是没有意义的. 其他的上下文选项, 比如max_redirects, 则需要数字值, 由于在字符串的zval中存储数字值并不通用, 所以需要执行一个类型转换以使设置合法.
不幸的是这些变量是上下文拥有的, 因此它们不能直接转换; 而需要首先进行隔离再进行转换, 最终如果需要还要进行销毁:
long max_redirects = 20; zval **tmpzval; if (context && php_stream_context_get_option(context, "http", "max_redirects", &tmpzval) == SUCCESS) { if (Z_TYPE_PP(tmpzval) == IS_LONG) { max_redirects = Z_LVAL_PP(tmpzval); } else { zval copyval = **tmpzval; zval_copy_ctor(©val); convert_to_long(©val); max_redirects = Z_LVAL(copyval); zval_dtor(©val); } }
实际上, 在这个例子中, zval_dtor()并不是必须的. IS_LONG的变量并不需要zval容器之外的存储空间, 因此zval_dtor()实际上不会有真正的操作. 在这个例子中包含它是为了完整性考虑, 对于字符串, 数组, 对象, 资源以及未来可能的其他类型, 就需要这个调用了.
参数
虽然用户空间API中看起来参数和上下文选项是类似的, 但实际上在语言内部的php_stream_context结构体中它们被定义为不同的成员.
目前只支持一个上下文参数: 通知器. php_stream_context结构体中的这个元素可以指向下面的php_stream_notifier结构体:
typedef struct { php_stream_notification_func func; void (*dtor)(php_stream_notifier *notifier); void *ptr; int mask; size_t progress, progress_max; } php_stream_notifier;
当将一个php_stream_notifier结构体赋值给context->notifier时, 它将提供一个回调函数func, 在特定的流上发生下表中的PHP_STREAM_NOTIFY_*代码表示的事件时被触发. 每个事件将会对应下面第二张表中的PHP_STREAM_NOTIFY_SEVERITY_*的级别:
事件代码 |
含义 |
RESOLVE |
主机地址解析完成.多数基于套接字的包装器将在连接之前执行这个查询. |
CONNECT |
套接字流连接到远程资源完成. |
AUTH_REQUIRED |
请求的资源不可用,原因是访问控制以及缺失授权 |
MIME_TYPE_IS |
远程资源的mime-type不可用 |
FILE_SIZE_IS |
远程资源当前可用大小 |
REDIRECTED |
原来的URL请求导致重定向到其他位置 |
PROGRESS |
由于额外数据的传输导致php_stream_notifier结构体的progress以及(可能的)progress_max元素被更新(进度信息,请参考php手册curl_setopt的CURLOPT_PROGRESSFUNCTION和CURLOPT_NOPROGRESS选项) |
COMPLETED |
流上没有更多的可用数据 |
FAILURE |
请求的URL资源不成功或未完成 |
AUTH_RESULT |
远程系统已经处理了授权认证 |
安全码 |
|
INFO |
信息更新.等价于一个E_NOTICE错误 |
WARN |
小的错误条件.等价于一个E_WARNING错误 |
ERR |
中断错误条件.等价于一个E_ERROR错误. |
通知器实现提供了一个便利指针*ptr用于存放额外数据. 这个指针指向的空间必须在上下文析构时被释放, 因此必须指定一个dtor函数, 在上下文的最后一个引用离开它的作用域时调用这个dtor进行释放.
mask元素允许事件触发限定特定的安全级别. 如果发生的事件没有包含在mask中, 则通知器函数不会被触发.
最后两个元素progress和progress_max可以由流实现设置, 然而, 通知器函数应该避免使用这两个值, 除非它接收到PHP_STREAM_NOTIFY_PROGRESS或PHP_STREAM_NOTIFY_FILE_SIZE_IS事件通知.
下面是一个php_stream_notification_func()回调原型的示例:
void php_sample6_notifier(php_stream_context *context, int notifycode, int severity, char *xmsg, int xcode, size_t bytes_sofar, size_t bytes_max, void *ptr TSRMLS_DC) { if (notifycode != PHP_STREAM_NOTIFY_FAILURE) { /* 忽略所有通知 */ return; } if (severity == PHP_STREAM_NOTIFY_SEVERITY_ERR) { /* 分发到错误处理函数 */ php_sample6_theskyisfalling(context, xcode, xmsg); return; } else if (severity == PHP_STREAM_NOTIFY_SEVERITY_WARN) { /* 日志记录潜在问题 */ php_sample6_logstrangeevent(context, xcode, xmsg); return; } }
默认上下文
在php5.0中, 当用户空间的流创建函数被调用时, 如果没有传递上下文参数, 请求一般会使用默认的上下文. 这个上下文变量存储在文件全局结构中: FG(default_context), 并且它可以和其他所有的php_stream_context变量一样访问. 当在用户空间脚本执行流的创建时, 更好的方式是允许用户指定一个上下文或者至少指定一个默认的上下文. 将用户空间的zval *解码得到php_stream_context可以使用php_steram_context_from_zval()宏完成, 比如下面改编自第14章"访问流"的例子:
PHP_FUNCTION(sample6_fopen) { php_stream *stream; char *path, *mode; int path_len, mode_len; int options = ENFORCE_SAFE_MODE | REPORT_ERRORS; zend_bool use_include_path = 0; zval *zcontext = NULL; php_stream_context *context; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|br", &path, &path_len, &mode, &mode_len, &use_include_path, &zcontext) == FAILURE) { return; } context = php_stream_context_from_zval(zcontext, 0); if (use_include_path) { options |= PHP_FILE_USE_INCLUDE_PATH; } stream = php_stream_open_wrapper_ex(path, mode, options, NULL, context); if (!stream) { RETURN_FALSE; } php_stream_to_zval(stream, return_value); }
如果zcontext包含一个用户空间的上下文资源, 通过ZEND_FETCH_RESOURCE()调用获取到它关联的指针设置到context中. 否则, 如果zcontext为NULL并且php_stream_context_from_zval()的第二个参数设置为非0值, 这个宏则直接返回NULL. 这个例子以及几乎所有的核心流创建的用户空间函数中, 第二个参数都被设置为0, 此时将使用FG(default_context)的值.
过滤器
过滤器作为读写操作的流内容传输过程中的附加阶段. 要注意的是直到php 4.3中才加入了流过滤器, 在php 5.0对流过滤器的API设计做过较大的调整. 本章的内容遵循的是php 5的流过滤器规范.
在流上应用已有的过滤器
在一个打开的流上应用一个已有的过滤器只需要几行代码即可:
php_stream *php_sample6_fopen_read_ucase(const char *path TSRMLS_DC) { php_stream_filter *filter; php_stream *stream; stream = php_stream_open_wrapper_ex(path, "r", REPORT_ERRORS | ENFORCE_SAFE_MODE, NULL, FG(default_context)); if (!stream) { return NULL; } filter = php_stream_filter_create("string.toupper", NULL, 0 TSRMLS_CC); if (!filter) { php_stream_close(stream); return NULL; } php_stream_filter_append(&stream->readfilters, filter); return stream; }
首先来看看这里引入的API函数以及它的兄弟函数:
php_stream_filter *php_stream_filter_create( const char *filtername, zval *filterparams, int persistent TSRMLS_DC); void php_stream_filter_prepend(php_stream_filter_chain *chain, php_stream_filter *filter); void php_stream_filter_append(php_stream_filter_chain *chain, php_stream_filter *filter);
php_stream_filter_create()的filterparams参数和用户空间对应的stream_filter_append()和stream_filter_prepend()函数的同名参数含义一致. 要注意, 所有传递到php_stream_filter_create()的zval *数据都不是过滤器所拥有的. 它们只是在过滤器创建期间被借用而已, 因此在调用作用域分配传入的所有内存空间都要手动释放.
如果过滤器要被应用到一个持久化流, 则必须设置persistent参数为非0值. 如果你不确认你要应用过滤器的流是否持久化的, 则可以使用php_stream_is_persistent()宏进行检查, 它只接受一个php_stream *类型的参数.
如在前面例子中看到的, 流过滤器被隔离到两个独立的链条中. 一个用于写操作中对php_stream_write()调用响应时的stream->ops->write()调用之前. 另外一个用于读操作中对stream->ops->read()取回的所有数据进行处理.
在这个例子中你使用&stream->readfilters指示读的链条. 如果你想要在写的链条上应用一个过滤器, 则可以使用&stream->writefilters.
定义一个过滤器实现
注册过滤器实现和注册包装器遵循相同的基础规则. 第一步是在MINIT阶段向php中引入你的过滤器, 与之匹配的是在MSHUTDOWN阶段移除它. 下面是需要调用的API原型以及两个注册过滤器工厂的示例:
int php_stream_filter_register_factory( const char *filterpattern, php_stream_filter_factory *factory TSRMLS_DC); int php_stream_filter_unregister_factory( const char *filterpattern TSRMLS_DC); PHP_MINIT_FUNCTION(sample6) { php_stream_filter_register_factory("sample6", &php_sample6_sample6_factory TSRMLS_CC); php_stream_filter_register_factory("sample.*", &php_sample6_samples_factory TSRMLS_CC); return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(sample6) { php_stream_filter_unregister_factory("sample6" TSRMLS_CC); php_stream_filter_unregister_factory("sample.*" TSRMLS_CC); return SUCCESS; }
这里注册的第一个工厂定义了一个具体的过滤器名sample6; 第二个则利用了流包装层内部的基本匹配规则. 为了进行演示, 下面的用户空间代码, 每行都将尝试通过不同的名字实例化php_sample6_samples_factory.
<?php stream_filter_append(STDERR, 'sample.one'); stream_filter_append(STDERR, 'sample.3'); stream_filter_append(STDERR, 'sample.filter.thingymabob'); stream_filter_append(STDERR, 'sample.whatever'); ?>
php_sample6_samples_factory的定义如下面代码, 你可以将这些代码放到你的MINIT块上面:
#include "ext/standard/php_string.h" typedef struct { char is_persistent; char *tr_from; char *tr_to; int tr_len; } php_sample6_filter_data; /* 过滤逻辑 */ static php_stream_filter_status_t php_sample6_filter( php_stream *stream, php_stream_filter *thisfilter, php_stream_bucket_brigade *buckets_in, php_stream_bucket_brigade *buckets_out, size_t *bytes_consumed, int flags TSRMLS_DC) { php_stream_bucket *bucket; php_sample6_filter_data *data = thisfilter->abstract; size_t consumed = 0; while ( buckets_in->head ) { bucket = php_stream_bucket_make_writeable(buckets_in->head TSRMLS_CC); php_strtr(bucket->buf, bucket->buflen, data->tr_from, data->tr_to, data->tr_len); consumed += bucket->buflen; php_stream_bucket_append(buckets_out, bucket TSRMLS_CC); } if ( bytes_consumed ) { *bytes_consumed = consumed; } return PSFS_PASS_ON; } /* 过滤器的释放 */ static void php_sample6_filter_dtor(php_stream_filter *thisfilter TSRMLS_DC) { php_sample6_filter_data *data = thisfilter->abstract; pefree(data, data->is_persistent); } /* 流过滤器操作表 */ static php_stream_filter_ops php_sample6_filter_ops = { php_sample6_filter, php_sample6_filter_dtor, "sample.*", }; /* 字符翻译使用的表 */ #define PHP_SAMPLE6_ALPHA_UCASE "ABCDEFGHIJKLMNOPQRSTUVWXYZ" #define PHP_SAMPLE6_ALPHA_LCASE "abcdefghijklmnopqrstuvwxyz" #define PHP_SAMPLE6_ROT13_UCASE "NOPQRSTUVWXYZABCDEFGHIJKLM" #define PHP_SAMPLE6_ROT13_LCASE "nopqrstuvwxyzabcdefghijklm" /* 创建流过滤器实例的过程 */ static php_stream_filter *php_sample6_filter_create( const char *name, zval *param, int persistent TSRMLS_DC) { php_sample6_filter_data *data; char *subname; /* 安全性检查 */ if ( strlen(name) < sizeof("sample.") || strncmp(name, "sample.", sizeof("sample.") - 1) ) { return NULL; } /* 分配流过滤器数据 */ data = pemalloc(sizeof(php_sample6_filter_data), persistent); if ( !data ) { return NULL; } /* 设置持久性 */ data->is_persistent = persistent; /* 根据调用时的名字, 对过滤器数据进行适当初始化 */ subname = (char *)name + sizeof("sample.") - 1; if ( strcmp(subname, "ucase") == 0 ) { data->tr_from = PHP_SAMPLE6_ALPHA_LCASE; data->tr_to = PHP_SAMPLE6_ALPHA_UCASE; } else if ( strcmp(subname, "lcase") == 0 ) { data->tr_from = PHP_SAMPLE6_ALPHA_UCASE; data->tr_to = PHP_SAMPLE6_ALPHA_LCASE; } else if ( strcmp(subname, "rot13") == 0 ) { data->tr_from = PHP_SAMPLE6_ALPHA_LCASE PHP_SAMPLE6_ALPHA_UCASE;; data->tr_to = PHP_SAMPLE6_ROT13_LCASE PHP_SAMPLE6_ROT13_UCASE; } else { /* 不支持 */ pefree(data, persistent); return NULL; } /* 节省未来使用时每次的计算 */ data->tr_len = strlen(data->tr_from); /* 分配一个php_stream_filter结构并按指定参数初始化 */ return php_stream_filter_alloc(&php_sample6_filter_ops, data, persistent); } /* 流过滤器工厂, 用于创建流过滤器实例(php_stream_filter_append/prepend的时候) */ static php_stream_filter_factory php_sample6_samples_factory = { php_sample6_filter_create };
译注: 下面是译者对整个流程的分析
1. MINIT阶段的register操作将在stream_filters_hash这个HashTable中注册一个php_stream_filter_factory结构, 它只有一个成员create_filter, 用来创建过滤器实例.
2. 用户空间代码stream_filter_append(STDERR, 'sapmple.one');在内部的实现是apply_filter_to_stream()函数(ext/standard/streamsfuncs.c中), 这里有两步操作, 首先创建过滤器, 然后将过滤器按照参数追加到流的readfilters/writefilters相应链中;
2.1 创建过滤器(php_stream_filter_create()): 首先直接按照传入的名字精确的从stream_filters_hash(或FG(stream_filters))中查找, 如果没有, 从右向左替换句点后面的内容为星号"*"进行查找, 直到找到注册的过滤器工厂或错误返回. 一旦找到注册的过滤器工厂, 就调用它的create_filter成员, 创建流过滤器实例.
2.2 直接按照参数描述放入流的readfilters/writefilters相应位置.
3. 用户向该流进行写入或读取操作时(以写为例): 此时内部将调用_php_stream_write(), 在这个函数中, 如果流的writefilters非空, 则调用流过滤器的fops->filter()执行过滤, 并根据返回状态做相应处理.
4. 当流的生命周期结束, 流被释放的时候, 将会检查流的readfilters/writefilters是否为空, 如果非空, 相应的调用php_stream_filter_remove()进行释放, 其中就调用了fops->fdtor对流过滤器进行释放.
上一章我们已经熟悉了流包装器的实现, 你可能能够识别这里的基本结构. 工厂函数(php_sample6_samples_filter_create)被调用分配一个过滤器实例, 并赋值给一个操作集合和抽象数据. 这上面的例子中, 你的工厂为所有的过滤器类型赋值了相同的ops结构, 但使用了不同的初始化数据.
调用作用域将得到这里分配的过滤器, 并将它赋值给流的readfilters链或writefilters链. 接着, 当流的读/写操作被调用时, 过滤器链将数据放入到一个或多个php_stream_bucket结构体, 并将这些bucket组织到一个队列php_stream_bucket_brigade中传递给过滤器.
这里, 你的过滤器实现是前面的php_sample6_filter, 它取出输入队列bucket中的数据, 使用php_sample6_filter_create中确定的字符表执行字符串翻译, 并将修改后的bucket放入到输出队列.
由于这个过滤器的实现并没有其他内部缓冲, 因此几乎不可能出错, 因此它总是返回PSFS_PASS_ON, 告诉流包装层有数据被过滤器存放到了输出队列中. 如果过滤器执行了内部缓冲消耗了所有的输入数据而没有产生输出, 就需要返回PSFS_FEED_ME标识过滤器循环周期在没有其他输入数据时暂时停止. 如果过滤器碰到了关键性的错误, 它应该返回PSFS_ERR_FATAL, 它将指示流包装层, 过滤器链处于不稳定状态. 这将导致流被关闭.
用于维护bucket和bucket队列的API函数如下:
php_stream_bucket *php_stream_bucket_new(php_stream *stream, char *buf, size_t buflen, int own_buf, int buf_persistent TSRMLS_DC);
创建一个php_stream_bucket用于存放到输出队列. 如果own_buf被设置为非0值, 流包装层可以并且通常都会修改它的内容或在某些点释放分配的内存. buf_persistent的非0值标识buf使用的内存是否持久分配的:
int php_stream_bucket_split(php_stream_bucket *in, php_stream_bucket **left, php_stream_bucket **right, size_t length TSRMLS_DC);
这个函数将in这个bucket的内容分离到两个独立的bucket对象中. left这个bucket将包含in中的前length个字符, 而right则包含剩下的所有字符.
void php_stream_bucket_delref(php_stream_bucket *bucket TSRMLS_DC); void php_stream_bucket_addref(php_stream_bucket *bucket);
Bucket使用和zval以及资源相同的引用计数系统. 通常, 一个bucket仅属于一个上下文, 也就是它依附的队列.
void php_stream_bucket_prepend( php_stream_bucket_brigade *brigade, php_stream_bucket *bucket TSRMLS_DC); void php_stream_bucket_append( php_stream_bucket_brigade *brigade, php_stream_bucket *bucket TSRMLS_DC);
这两个函数扮演了过滤器子系统的苦力, 用于附加bucket到队列的开始(prepend)或末尾(append)
void php_stream_bucket_unlink(php_stream_bucket *bucket TSRMLS_DC);
在过滤器逻辑应用处理完成后, 旧的bucket必须使用这个函数从它的输入队列删除(unlink).
php_stream_bucket *php_stream_bucket_make_writeable( php_stream_bucket *bucket TSRMLS_DC);
将一个bucket从它所依附的队列中移除, 并且如果需要, 赋值bucket->buf的内部缓冲区, 这样就使得它的内容可修改. 在某些情况下, 比如当输入bucket的引用计数大于1时, 返回的bucket将会是不同的实例, 而不是传入的实例. 因此, 我们要保证在调用作用域使用的是返回的bucket, 而不是传入的bucket.
小结
过滤器和上下文可以让普通的流类型行为被修改, 或通过INI设置影响整个请求, 而不需要直接的代码修改. 使用本章设计的计数, 你可以使你自己的包装器实现更加强大, 并且可以对其他包装器产生的数据进行改变.
接下来, 我们将离开PHPAPI背后的工作, 回到php构建系统的机制, 产生更加复杂的扩展链接到其他应用, 找到更加容易的方法, 使用工具集处理重复的工作.
以上就是[翻译][php扩展开发和嵌入式]第16章-有趣的流的内容,更多相关内容请关注PHP中文网(www.php.cn)!

使用数据库存储会话的主要优势包括持久性、可扩展性和安全性。1.持久性:即使服务器重启,会话数据也能保持不变。2.可扩展性:适用于分布式系统,确保会话数据在多服务器间同步。3.安全性:数据库提供加密存储,保护敏感信息。

在PHP中实现自定义会话处理可以通过实现SessionHandlerInterface接口来完成。具体步骤包括:1)创建实现SessionHandlerInterface的类,如CustomSessionHandler;2)重写接口中的方法(如open,close,read,write,destroy,gc)来定义会话数据的生命周期和存储方式;3)在PHP脚本中注册自定义会话处理器并启动会话。这样可以将数据存储在MySQL、Redis等介质中,提升性能、安全性和可扩展性。

SessionID是网络应用程序中用来跟踪用户会话状态的机制。1.它是一个随机生成的字符串,用于在用户与服务器之间的多次交互中保持用户的身份信息。2.服务器生成并通过cookie或URL参数发送给客户端,帮助在用户的多次请求中识别和关联这些请求。3.生成通常使用随机算法保证唯一性和不可预测性。4.在实际开发中,可以使用内存数据库如Redis来存储session数据,提升性能和安全性。

在无状态环境如API中管理会话可以通过使用JWT或cookies来实现。1.JWT适合无状态和可扩展性,但大数据时体积大。2.Cookies更传统且易实现,但需谨慎配置以确保安全性。

要保护应用免受与会话相关的XSS攻击,需采取以下措施:1.设置HttpOnly和Secure标志保护会话cookie。2.对所有用户输入进行输出编码。3.实施内容安全策略(CSP)限制脚本来源。通过这些策略,可以有效防护会话相关的XSS攻击,确保用户数据安全。

优化PHP会话性能的方法包括:1.延迟会话启动,2.使用数据库存储会话,3.压缩会话数据,4.管理会话生命周期,5.实现会话共享。这些策略能显着提升应用在高并发环境下的效率。

thesession.gc_maxlifetimesettinginphpdeterminesthelifespanofsessiondata,setInSeconds.1)它'sconfiguredinphp.iniorviaini_set().2)abalanceIsiseededeedeedeedeedeedeedto to to avoidperformance andununununununexpectedLogOgouts.3)

在PHP中,可以使用session_name()函数配置会话名称。具体步骤如下:1.使用session_name()函数设置会话名称,例如session_name("my_session")。2.在设置会话名称后,调用session_start()启动会话。配置会话名称可以避免多应用间的会话数据冲突,并增强安全性,但需注意会话名称的唯一性、安全性、长度和设置时机。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

VSCode Windows 64位 下载
微软推出的免费、功能强大的一款IDE编辑器

Atom编辑器mac版下载
最流行的的开源编辑器

EditPlus 中文破解版
体积小,语法高亮,不支持代码提示功能

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 英文版
推荐:为Win版本,支持代码提示!