정적 웹사이트의 콘텐츠는 서버에 직접 저장되는 간단한 정적 웹페이지로, 쉽게 놀라운 방문 수를 달성할 수 있습니다. 그러나 동적 웹사이트는 동적입니다. 즉, 사용자가 페이지를 방문할 때마다 서버는 데이터베이스 쿼리를 수행하고, 템플릿을 시작하고, 비즈니스 로직을 실행하고, 마지막으로 본 웹페이지를 생성해야 합니다. 이 모든 것이 동적으로 생성됩니다. 곧. 프로세서 리소스 관점에서 볼 때 이는 상대적으로 비용이 많이 듭니다.
대부분의 웹 애플리케이션에서는 과부하가 큰 문제가 되지 않습니다. 대부분의 웹 애플리케이션은 washingtopost.com이나 Slashdot이 아니기 때문에 일반적으로 작고 단순하거나 트래픽이 거의 없는 중간 규모의 사이트입니다. 하지만 중대형 트래픽이 있는 사이트의 경우 과부하 문제를 최대한 해결하는 것이 필요합니다. 이를 위해서는 캐시를 사용해야 합니다.
캐싱의 목적은 특히 시간과 리소스를 더 많이 소모하는 일부 계산의 경우 반복 계산을 방지하는 것입니다. 아래 코드는 동적 페이지의 결과를 캐시하는 방법을 보여줍니다.
given a URL, try finding that page in the cache if the page is in the cache: return the cached page else: generate the page save the generated page in the cache (for next time) return the generated page
이를 위해 Django는 동적 페이지의 결과를 캐시할 수 있는 안정적인 캐싱 시스템을 제공하므로 후속 동일한 요청이 불필요하게 반복되는 계산을 피하기 위해 캐시에 있는 데이터를 직접 사용할 수 있습니다. 또한 Django는 다양한 세부 단위로 데이터 캐싱을 제공합니다. 예를 들어 전체 페이지, 특정 부분 또는 전체 웹사이트를 캐시할 수 있습니다.
Django는 Squid(http://www.squid-cache.org) 및 브라우저 기반 캐시와 같은 "업스트림" 캐시에서도 잘 작동합니다. 캐싱 직접 제어할 수는 없지만 사이트의 어느 부분을 캐시해야 하는지, 어떻게 캐시해야 하는지에 대한 단서를 HTTP 헤더를 통해 제공할 수 있습니다.
계속 읽어보세요 Django의 캐싱 시스템을 사용하는 방법을 알아보세요. 귀하의 사이트가 Slashdot과 유사하다면 이 자료를 이해하셨다면 기쁠 것입니다
캐싱 설정
캐싱 시스템에는 약간의 설정 작업이 필요합니다. 즉, 캐시된 데이터가 데이터베이스, 파일 시스템 또는 메모리에 어디에 있는지 알려주어야 합니다. 이는 캐시 성능에 영향을 미치는 중요한 결정입니다. 예, 일부 캐시 유형은 다음과 같습니다. 다른 것보다 빠르며, 인 메모리 캐시는 일반적으로 파일 시스템이나 데이터베이스 캐시보다 빠릅니다. 전자는 파일 시스템이나 데이터베이스
캐시에 액세스하는 오버헤드가 없기 때문입니다. 설정 파일의 CACHE_BACKEND 설정 캐싱을 사용하지만 CACHE_BACKEND를 지정하지 않으면 Django는 기본적으로 simple:///로 설정됩니다. CACHE_BACKEND에 사용 가능한 모든 값은 아래에 설명되어 있습니다
메모리 캐싱
Django에서 사용할 수 있는 가장 빠르고 효율적인 캐시 유형은 원래 LiveJournal .com용으로 개발된 메모리 기반 캐싱 프레임워크인 Memcached입니다. 높은 부하를 처리했으며 이후 Danga
Interactive(http://www.danga.com)에서 오픈 소스화되었으며, Slashdot 및 Wikipedia와 같은 사이트에서 데이터베이스 액세스를 줄이고 사이트 성능을 대폭 향상시키는 데 사용됩니다
Memcached는 http://danga.com/memcached/에서 무료로 제공되며, 백그라운드 프로세스로 실행되고 지정된 양의 RAM을 할당하여 캐시에 매우 빠른 결과를 제공합니다. 임의의 데이터를 가능한 한 빨리 추가, 가져오기 및 삭제합니다. 모든 데이터는 메모리에 직접 저장되므로 데이터베이스 및 파일 시스템을 과도하게 사용하지 않습니다.
Memcached가 설치된 후 자체적으로 , Django와 직접 번들로 제공되지 않는 MemcachedPython 바인딩을 설치해야 합니다. 이러한 바인딩은 http://www.djangoproject.com/thirdparty/ python-memcached에서 사용할 수 있는 별도의 Python 모듈인 'memcache.py'에 있습니다. get
Django가 Memcached를 사용할 수 있도록 CACHE_BACKEND를 memcached://ip:port/로 설정합니다. 여기서 ip는 Memcached 백그라운드 프로세스의 IP 주소이고 포트는 다음과 같습니다. Memcached가 실행되는 포트
이 예에서 Memcached는 로컬 호스트(127.0.0.1)에서 실행되고 포트는 11211입니다.
CACHE_BACKEND = ' memcached://127.0.0.1:11211/'
Memcached의 뛰어난 기능 중 하나는 여러 서버에서 캐시를 공유하는 기능입니다. 즉, 여러 시스템에서 Memcached 프로세스를 실행할 수 있습니다. 머신 그룹은 각 머신의 캐시 값을 복사할 필요 없이 *단일* 캐시로 처리됩니다. Django가 이 기능을 활용하려면 세미콜론으로 구분된 CACHE_BACKEND에 모든 서버 주소를 포함해야 합니다.
이 예에서 캐시는 IP 주소 172.19.26.240 및 172.19.26.242와 포트 11211에서 실행되는 Memcached 인스턴스 간에 공유됩니다.
CACHE_BACKEND = 'memcached: //172.19.26.240:11211;172.19.26.242:11211/'
이 예에서 캐시는 172.19.26.240(포트 11211), 172.19.26.242(포트 11212), 172.19.26.2에서 실행됩니다. 44 ( 포트 11213) Memcached 인스턴스 간 공유:
CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11212;172.19.26.244:11213/'
Memcached의 마지막 점은 메모리 기반 캐싱에는 큰 단점이 있다는 점입니다. 캐시된 데이터는 메모리에만 저장되므로 서버가 충돌하면 메모리가 영구 데이터 저장을 위해 준비되지 않았으므로 데이터가 손실됩니다. Django에는 캐시가 없습니다. 백엔드는 저장소가 아닌 캐싱 솔루션입니다. 그러나 메모리 기반 캐싱은 특히 수명이 짧기 때문에 여기서 이를 지적합니다
.
데이터베이스 캐시
데이터베이스 테이블을 캐시 백엔드로 사용하려면 데이터베이스에 캐시 테이블을 생성하고 Django의 캐시를 가리켜야 합니다. 시스템을 해당 테이블에 추가
먼저 다음 명령문을 사용하여 캐시 데이터 테이블을 생성합니다.
python Manage.py createcachetable [cache_table_name]
여기서 [cache_table_name]은 데이터베이스 테이블 이름을 생성하는 것입니다. 이름은 데이터베이스에서 합법적이고 사용되지 않은 한 원하는 대로 지정할 수 있습니다. Django의 데이터베이스 캐싱 시스템에서 예상하는 형식입니다.
데이터베이스 테이블을 생성한 후 CACHE_BACKEND를 "db://tablename"으로 설정합니다. 여기서 tablename은 데이터베이스의 이름입니다. 데이터베이스 테이블(이 예에서는 캐시) 테이블 이름은 my_cache_table입니다:
CACHE_BACKEND = 'db://my_cache_table'
데이터베이스 캐시 백엔드는 설정 파일에 지정된 것과 동일한 데이터베이스를 사용합니다. 캐시 테이블에 다른 것을 사용할 수 없습니다.
파일 시스템 캐시
"file ://" 캐시 유형을 CACHE_BACKEND로 지정하고 파일 시스템 디렉터리를 지정하여 파일 시스템에 캐시 데이터를 저장합니다.
예를 들어 캐시 데이터를 /에 저장하려면 다음 설정을 사용합니다. var/tmp/django_cache:
CACHE_BACKEND = 'file:///var/tmp/django_cache'
예제 시작 부분에는 세 개의 슬래시가 있습니다. 처음 두 개는 file://, 세 번째는 디렉터리 경로입니다. 첫 번째 문자인 /var/tmp/django_cache는 Windows 시스템을 사용하는 경우 file:// 뒤에 드라이브 문자를 입력합니다. 'file://c:/ foo/bar'.
디렉터리 경로는 *절대* 경로여야 합니다. 즉, 파일 시스템의 루트에서 시작해야 하며, 설정 끝 부분에 슬래시가 있는지 여부를 확인하세요.
이 설정이 가리키는 디렉터리가 존재하고 시스템 사용자가 해당 디렉터리를 읽고 쓸 수 있는지 확인하세요. 위의 예를 계속해서, 서버가 사용자 apache로 실행 중이라면 /var/tmp/django_cache가 존재하고 사용자 apache가 /var/tmp/django_cache 디렉터리
CACHE_BACKEND = "locmem:///?timeout=60"
而在这个例子中,timeout设为30而max_entries为400:
CACHE_BACKEND = "locmem:///?timeout=30&max_entries=400"
其中,非法的参数与非法的参数值都将被忽略。
站点级 Cache
一旦你指定了”CACHE_BACKEND”,使用缓存的最简单的方法就是缓存你的整个网站。这意味着所有不包含GET或POST参数的页面在第一次被请求之后将被缓存指定好的一段时间。
要激活每个站点的cache,只要将``’django.middleware.cache.CacheMiddleware’``添加到MIDDLEWARE_CLASSES的设置里,就像下面这样:
MIDDLEWARE_CLASSES = ( 'django.middleware.cache.CacheMiddleware', 'django.middleware.common.CommonMiddleware', )
注意
关于MIDDLEWARE_CLASSES顺序的一些事情。请看本章节后面的MIDDLEWARE_CLASSES顺序部分。
然后,在你的Django settings文件里加入下面所需的设置:
CACHE_MIDDLEWARE_SECONDS:每个页面应该被缓存的秒数
§ “CACHE_MIDDLEWARE_KEY_PREFIX”:如果缓存被多个使用相同Django安装的网站所共享,那么把这个值设成当前网站名,或其他能代表这个Django实例的唯一字符串,以避免key发生冲突。如果你不在意的话可以设成空字符串。
缓存中间件缓存每个没有GET或者POST参数的页面,即如果用户请求页面并在查询字符串里传递GET参数或者POST参数,中间件将不会尝试得到缓存版本的页面,如果你打算使用整站缓存,设计你的程序时牢记这点,例如,不要使用拥有查询字符串的URLs,除非那些页面可以不缓存
缓存中间件( cache middleware)支持另外一种设置选项,CACHE_MIDDLEWARE_ANONYMOUS_ONLY。如果你把它设置为“True”,那么缓存中间件就只会对匿名请求进行缓存,匿名请求是指那些没有登录的用户发起的请求。如果想取消用户相关页面(user-specific
pages)的缓存,例如Djangos的管理界面,这是一种既简单又有效的方法。另外,如果你要使用CACHE_MIDDLEWARE_ANONYMOUS_ONLY选项,你必须先激活AuthenticationMiddleware才行,也就是在你的配置文件MIDDLEWARE_CLASSES的地方,AuthenticationMiddleware必须出现在CacheMiddleware前面。
最后,再提醒一下:CacheMiddleware在每个HttpResponse中都会自动设置一些头部信息(headers)
§ 当一个新(没缓存的)版本的页面被请求时设置Last-Modified头部为当前日期/时间
§ 设置Expires头部为当前日期/时间加上定义的CACHE_MIDDLEWARE_SECONDS
§ 设置Cache-Control头部来给页面一个最大的时间—再一次,根据CACHE_MIDDLEWARE_SECONDS设置
视图级缓存
更加颗粒级的缓存框架使用方法是对单个视图的输出进行缓存。这和整站级缓存有一样的效果(包括忽略对有 GET和 POST
参数的请求的缓存)。它应用于你所指定的视图,而不是整个站点。
完成这项工作的方式是使用修饰器,其作用是包裹视图函数,将其行为转换为使用缓存。视图缓存修饰器称为cache_page,位于django.views.decorators.cache模块中,例如:
from django.views.decorators.cache import cache_page def my_view(request, param): # ... my_view = cache_page(my_view, 60 * 15)
如果使用 Python 2.4或更高版本,
你也可以使用 decorator语法。这个例子和前面的那个是等同的:
from django.views.decorators.cache import cache_page @cache_page(60 * 15) def my_view(request, param): # ...
cache_page只接受一个参数:以秒计的缓存超时。在前例中, “my_view()”视图的结果将被缓存 15
分钟。(注意:为了提高可读性,该参数被书写为60 * 15。60 * 15将被计算为900,也就是说15分钟乘以每分钟
60 秒。)
和站点缓存一样,视图缓存与 URL无关。如果多个 URL
指向同一视图,每个视图将会分别缓存。继续my_view范例,如果 URLconf如下所示:
urlpatterns = ('', (r'^foo/(/d{1,2})/$', my_view), )
那么正如你所期待的那样,发送到/foo/1/和/foo/23/的请求将会分别缓存。但一旦发出了特定的请求(如:/foo/23/),之后再度发出的指向该
URL 的请求将使用缓存。
在 URLconf中指定视图缓存
前一节中的范例将视图硬编码为使用缓存,因为cache_page在适当的位置对my_view函数进行了转换。该方法将视图与缓存系统进行了耦合,从几个方面来说并不理想。例如,你可能想在某个无缓存的站点中重用该视图函数,或者你可能想将该视图发布给那些不想通过缓存使用它们的人。解决这些问题的方法是在
URLconf 中指定视图缓存,而不是紧挨着这些视图函数本身来指定。
完成这项工作非常简单:在 URLconf中用到这些视图函数的时候简单地包裹一个cache_page。以下是刚才用到过的
URLconf : urlpatterns = ('', (r'^foo/(/d{1,2})/$', my_view), ) 以下是同一个 URLconf,不过用cache_page包裹了my_view: from django.views.decorators.cache import cache_page urlpatterns = ('', (r'^foo/(/d{1,2})/$', cache_page(my_view, 60 * 15)), )
如果采取这种方法,不要忘记在 URLconf
中导入cache_page.
低层次缓存API
有些时候,对整个经解析的页面进行缓存并不会给你带来太多,事实上可能会过犹不及。
比如说,也许你的站点所包含的一个视图依赖几个费时的查询,每隔一段时间结果就会发生变化。在这种情况下,使用站点级缓存或者视图级缓存策略所提供的整页缓存并不是最理想的,因为你可能不会想对整个结果进行缓存(因为一些数据经常变化),但你仍然会想对很少变化的部分进行缓存。
在像这样的情形下, Django展示了一种位于django.core.cache模块中的简单、低层次的缓存
API。你可以使用这种低层次的缓存 API在缓存中以任何级别粒度进行对象储存。你可以对所有能够安全进行 pickle处理的
Python 对象进行缓存:字符串、字典和模型对象列表等等;查阅 Python文档可以了解到更多关于 pickling的信息。)
下面是如何导入这个 API :
>>> from django.core.cache import cache
基本的接口是set(key, value, timeout_seconds)和get(key):
>>> cache.set('my_key', 'hello, world!', 30) >>> cache.get('my_key') 'hello, world!'
timeout_seconds参数是可选的,并且默认为前面讲过的CACHE_BACKEND设置中的timeout参数.
如果对象在缓存中不存在,或者缓存后端是不可达的,cache.get()返回None:
# Wait 30 seconds for 'my_key' to expire... >>> cache.get('my_key') None >>> cache.get('some_unset_key') None
我们不建议在缓存中保存None常量,因为你将无法区分所保存的None变量及由返回值None所标识的缓存未中。
cache.get()接受一个缺省参数。其指定了当缓存中不存在该对象时所返回的值:
>>> cache.get('my_key', 'has expired')
'has expired'
要想一次获取多个缓存值,可以使用cache.get_many()。如果可能的话,对于给定的缓存后端,get_many()将只访问缓存一次,而不是对每个缓存键值都进行一次访问。get_many()所返回的字典包括了你所请求的存在于缓存中且未超时的所有键值。
>>> cache.set('a', 1) >>> cache.set('b', 2) >>> cache.set('c', 3) >>> cache.get_many(['a', 'b', 'c']) {'a': 1, 'b': 2, 'c': 3}
如果某个缓存关键字不存在或者已超时,它将不会被包含在字典中。下面是范例的延续:
>>> cache.get_many(['a', 'b', 'c', 'd'])
{'a': 1, 'b': 2, 'c': 3}
最后,你可以用cache.delete()显式地删除关键字。这是在缓存中清除特定对象的简单途径。
>>> cache.delete('a')
cache.delete()没有返回值,不管给定的缓存关键字对应的值存在与否,它都将以同样方式工作。
上游缓存
目前为止,本章的焦点一直是对你自己的数据进行缓存。但还有一种与 Web开发相关的缓存:由上游高速缓存执行的缓冲。有一些系统甚至在请求到达站点之前就为用户进行页面缓存。
下面是上游缓存的几个例子:
§ 你的 ISP (互联网服务商)可能会对特定的页面进行缓存,因此如果你向http://www.infocool.net/请求一个页面,你的
ISP 可能无需直接访问 www.infocool.net就能将页面发送给你。而 www.infocool.net的维护者们却无从得知这种缓存,ISP位于
www.infocool.net和你的网页浏览器之间,透明底处理所有的缓存。
§ 你的 Django网站可能位于某个代理缓存之后,例如
Squid网页代理缓存 (http://www.squid-cache.org/),该缓存为提高性能而对页面进行缓存。在此情况下,每个请求将首先由代理服务器进行处理,然后仅在需要的情况下才被传递至你的应用程序。
§ 你的网页浏览器也对页面进行缓存。如果某网页送出了相应的头部,你的浏览器将在为对该网页的后续的访问请求使用本地缓存的拷贝,甚至不会再次联系该网页查看是否发生了变化。
上游缓存将会产生非常明显的效率提升,但也存在一定风险。许多网页的内容依据身份验证以及许多其他变量的情况发生变化,缓存系统仅盲目地根据 URL保存页面,可能会向这些页面的后续访问者暴露不正确或者敏感的数据。
举个例子,假定你在使用网页电邮系统,显然收件箱页面的内容取决于登录的是哪个用户。如果 ISP盲目地缓存了该站点,那么第一个用户通过该 ISP登录之后,他(或她)的用户收件箱页面将会缓存给后续的访问者。这一点也不好玩。
幸运的是, HTTP提供了解决该问题的方案。已有一些 HTTP头标用于指引上游缓存根据指定变量来区分缓存内容,并通知缓存机制不对特定页面进行缓存。我们将在本节后续部分将对这些头标进行阐述。
使用 Vary头标
Vary头标定义了缓存机制在构建其缓存键值时应当将哪个请求头标考虑在内。例如,如果网页的内容取决于用户的语言偏好,该页面被称为根据语言而不同。
缺省情况下,Django的缓存系统使用所请求的路径(比如:"/stories/2005/jun/23/bank_robbed/")来创建其缓存键。这意味着对该
URL的每个请求都将使用同一个已缓存版本,而不考虑 cookies或语言偏好之类的 user-agent差别。然而,如果该页面基于请求头标的区别(例如
cookies、语言或者 user-agent)产生不同内容,你就不得不使用
Vary头标来通知缓存机制:该页面的输出取决与这些东西。
要在 Django完成这项工作,可使用便利的vary_on_headers视图修饰器,如下所示:
from django.views.decorators.vary import vary_on_headers # Python 2.3 syntax. def my_view(request): # ... my_view = vary_on_headers(my_view, 'User-Agent') # Python 2.4+ decorator syntax. @vary_on_headers('User-Agent') def my_view(request): # ...
在这种情况下,缓存装置(如 Django自己的缓存中间件)将会为每一个单独的用户浏览器缓存一个独立的页面版本。
使用vary_on_headers修饰器而不是手动设置Vary头标(使用像response['Vary']
= 'user-agent'之类的代码)的好处是修饰器在(可能已经存在的)Vary之上进行添加,而不是从零开始设置,且可能覆盖该处已经存在的设置。
你可以向vary_on_headers()传入多个头标:
@vary_on_headers('User-Agent', 'Cookie') def my_view(request): # ...
该段代码通知上游缓存对两者都进行不同操作,也就是说 user-agent和 cookie
的每种组合都应获取自己的缓存值。举例来说,使用Mozilla作为 user-agent而foo=bar作为
cookie值的请求应该和使用Mozilla作为 user-agent而foo=ham的请求应该被视为不同请求。
由于根据 cookie而区分对待是很常见的情况,因此有vary_on_cookie修饰器。以下两个视图是等效的:
@vary_on_cookie def my_view(request): # ... @vary_on_headers('Cookie') def my_view(request): # ...
传入vary_on_headers头标是大小写不敏感的;"User-Agent"与"user-agent"完全相同。
你也可以直接使用帮助函数:django.utils.cache.patch_vary_headers。该函数设置或增加Vary header,例如:
from django.utils.cache import patch_vary_headers def my_view(request): # ... response = render_to_response('template_name', context) patch_vary_headers(response, ['Cookie']) return response
patch_vary_headers以一个HttpResponse实例为第一个参数,以一个大小写不敏感的头标名称列表或元组为第二个参数。
其它缓存头标
关于缓存剩下的问题是数据的私隐性以及关于在级联缓存中数据应该在何处储存的问题。
通常用户将会面对两种缓存:他或她自己的浏览器缓存(私有缓存)以及他或她的提供者缓存(公共缓存)。公共缓存由多个用户使用,而受其他某人的控制。这就产生了你不想遇到的敏感数据的问题,比如说你的银行账号被存储在公众缓存中。因此,Web应用程序需要以某种方式告诉缓存那些数据是私有的,哪些是公共的。
解决方案是标示出某个页面缓存应当是私有的。要在 Django中完成此项工作,可使用cache_control视图修饰器:
from django.views.decorators.cache import cache_control @cache_control(private=True) def my_view(request): # ...
该修饰器负责在后台发送相应的 HTTP头标。
还有一些其他方法可以控制缓存参数。例如, HTTP允许应用程序执行如下操作:
§ 定义页面可以被缓存的最大次数。
§ 指定某个缓存是否总是检查较新版本,仅当无更新时才传递所缓存内容。(一些缓存即便在服务器页面发生变化的情况下都可能还会传送所缓存的内容,只因为缓存拷贝没有过期。)
在 Django中,可使用cache_control视图修饰器指定这些缓存参数。在本例中,cache_control告诉缓存对每次访问都重新验证缓存并在最长
3600 秒内保存所缓存版本:
from django.views.decorators.cache import cache_control @cache_control(must_revalidate=True, max_age=3600) def my_view(request): ...
在cache_control()中,任何有效Cache-ControlHTTP指令都是有效的。以下是一个完整的清单:
§ public=True § private=True § no_cache=True § no_transform=True § must_revalidate=True § proxy_revalidate=True § max_age=num_seconds § s_maxage=num_seconds
小提示
要了解有关Cache-ControlHTTP指令的相关解释,
可以查阅http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9的规范文档。
注意
缓存中间件已经使用CACHE_MIDDLEWARE_SETTINGS设置设定了缓存头标max-age。如果你在cache_control修饰器中使用了自定义的max_age,该修饰器将会取得优先权,该头标的值将被正确地被合并。)
其他优化
Django 带有一些其它中间件可帮助您优化应用程序的性能:
§ django.middleware.http.ConditionalGetMiddleware는 최신 브라우저에 대한 ETag 및 Last-Modified 헤더를 기반으로 하는 조건부 GET 응답에 대한 지원을 추가합니다.
§ django.middleware.gzip.GZipMiddleware는 모든 최신 브라우저의 응답 콘텐츠를 압축하여 대역폭과 전달 시간을 절약합니다.
MIDDLEWARE_CLASSES 순서
캐싱 미들웨어를 사용하는 경우 MIDDLEWARE_CLASSES의 올바른 위치에 배치해야 합니다. 왜냐하면 캐시 미들웨어는 다양한 캐시 저장소를 생성하는 데 사용되는 헤더를 알아야 하기 때문입니다.
다음 미들웨어를 포함하여 Vary 헤더에 콘텐츠를 추가할 수 있는 모든 미들웨어 뒤에 CacheMiddleware를 배치하세요.
§ Cookie의 SessionMiddleware 추가
§ Accept-Encoding의 GZipMiddleware 추가,
위는 Django 캐싱 메커니즘의 내용입니다. 더 많은 관련 내용을 주목해 주세요 PHP Chinese 홈페이지(www.php.cn)!