PHP利用socket发送邮件
学习背景:
在实际工作中遇到了发邮件需求。现有代码是通过访问JSP页面调用一个java的邮件类来实现。据说一开始也尝试过PHP的类,但好像用了一段时间就出问题了,所以就这样开始了“曲线救国”。按现有方式完成自己工作后,出于好奇,想要知道为什么直接使用PHP失败的原因。希望能够直接用PHP来完成功能,这样在效率上应该是更高的。
学习经过:
1、找寻现有的PHP邮件类2、使用PHP的mail()函数,但因为无法登陆只能发送内网邮箱。3、socket学习4、SMTP协议命令学习5、利用cmd,通过telnet连接服务器,发送SMTP命令发邮件6、利用socket,发送SMTP命令发邮件7、归纳总结data部分编码问题及邮件的附件格式(1)标题编码(2)正文分割(3)带附件的具体正文(4)具体实现8、总结9、附件
注:下文使用了三个邮箱服务器:1、公司旧服务器:mail.old.net2、公司新服务器:mail.new.net3、QQ邮箱:smtp.qq.com
1、找寻现有的PHP邮件类:
找到代码中原先的PHP类,还从网上找了一些别人写好的PHP类...代码少则几百多则上千行...以我目前的水平要啃完其中的原理再找出问题所在应该是很难的...还看到了安装PEAR后用邮件拓展的方式,但不知为什么我安装一直失败,也只能先放弃了。
2、使用PHP的mail()函数,但因为无法登陆只能发送内网邮箱:
想起PHP本身就有一个mail()函数...所以...为什么没人用?先试试看...
具体实现:
1、修改php.ini配置:
SMTP = mail.old.net //设置具体的邮件服务器地址smtp_port = 25 //设置端口,一般为25sendmail_from = "gs@old.net" //设置发送者的邮箱(win环境);sendmail_path = "" //unix环境?未测试过
2、php代码:mail($to, $subject, $message, $headers, $parameters)mail(收件人, 标题, 内容, 显示在邮件原文里的一些参数, 没用过不清楚)
$to = "gs@old.net";$subject = "=?UTF-8?B?".base64_encode("邮件标题")."?=";$message = "点击可以直接跳转";$headers = \r\nto:" \r\nHEADERS;$boolean = mail($to, $subject, $message, $headers); //返回是否发送成功
说明:
1、邮件的发件人与收件人:ini配置 --> 邮件原文Return-Path$to参数 --> 实际收件人(必须是内网的)$header参数 --> 显示发件人(可以随便添)$header参数 --> 显示收件人(可以随便填)2、发件范围:收件人必须是内网的人:测试情况:只能够发送同一服务器范围中的邮箱(@后的一致)。如果发送外网(如公司邮箱发送QQ邮箱等),错误码有554和550。推测:1、邮件服务器的设置问题。2、因为mail发邮件不需要登陆具体账号和密码,所以无法发送外网?注:界面上显示的内容可以因为to的设置而显示为别人。即:$to设置内网邮箱,该邮箱实际接受到此邮件。但该邮件所显示的收件人可以不是该内网邮箱,而是依据$headers所设定的。3、编码:对于内容:先用base64编码,然后再UTF-8。形如: =?UTF-8?B?".base64_encode("邮件标题")."?=4、正文使用HTML代码:在$headers中设置Content-type:text/html就可以了
总结:
如果只是内网进行邮件,mail()已经足够,但是如果需要和外网联系,则目前没找到mail()的解决办法。
3、socket学习
在网上找的一堆类中,发现了个代码量比较少的,使用的是socket方法。尝试先学习socket,然后将这个类理解。
socket大致意义:
socket服务器:开个端口,持续监听,收到信息,回复信息。socket客户端:发信息到具体端口,接受服务器回复的信息。
具体实现:
服务器代码:
set_time_limit(0); //网页执行防止超时,cmd中执行无需该行$socket = socket_create(AF_INET, SOCK_STREAM, 0); //创建socket资源socket_bind($socket, "127.0.0.1", 16161); //将socket资源绑定到具体端口socket_listen($socket, 3); //该端口开始监听,第二个参数是具体连接数(并发数?)?$receive_socket = socket_accept($socket); //接收到客户端的信息,一个新的socket资源$input = socket_read($receive_socket, 1024); //读取该socket资源的内容var_dump($input); //客户端发出的内容,一般都是字符串?$output = "服务器-->客户端 信息";socket_write($receive_socket, $output, strlen ($output)); //给客户端返回信息。注意:用的是接受到的socket资源,而不是原本自身的资源。socket_close($socket); //将两个资源关闭socket_close($receive_socket);
客户端代码:
$socket = socket_create(AF_INET, SOCK_STREAM, 0); //创建socket资源socket_connect($socket, "127.0.0.1", 16161); //连接具体端口$message = "客户端-->服务器 信息";socket_write($socket, $message, strlen($message)); //发送信息给服务器$result = socket_read ($socket, 1024); //读取到具体的信息var_dump($resule);socket_close($socket); //关闭socket资源
说明:
只测试了浏览器访问的方式,没有用cmd执行。(浏览器访问:先服务器,后客户端)目前客户端第一次访问后服务器就停止了,当第二次访问的时候就会报错。socket_close()注释掉 && socket_listen()第二个参数>1,依然无效。不知道如何让服务器持续监听。
总结:
初步明白了socket的意义及简单的使用。对于更多参数及方法的使用等以后再深入学习。
4、SMTP协议命令学习
SMTP常用命令(大小写无关):
每个命令都会返回状态码+简短说明,用来告知成功失败。
//连接成功,220helo xxx //这一步是必须的!xxx貌似自定义,250ehlo xxx //xxx貌似自定义,查看服务器支持哪些命令,返回250-xxx 250-xxxstarttls //QQ邮箱需要auth login //账号密码登陆,账号密码需要的是base64加密过的,mail from://发件人,250rept to: //收件人,可以重复执行发送给多个人,250data //具体邮件内容,显示的from-to、编码及附件都在这里传输,以单行.结束(\r\n.\r\n),354+250quit //退出连接,221
5、利用cmd,通过telnet连接服务器,发送SMTP命令发邮件
telnet使用:
连接服务器后输入具体命令,此时无法使用退格键进行更错
telnet host port //邮件通常端口为25 QQ邮箱需要使用587(SSL加密方式,官方465或587,但465我测试失败)
公司旧服务器:mail.old.net
无法auth login登陆
telnet mail.old.net 25 //220helo abc //250ehlo abc //返回250-xx 250-xxmail from://原文Return-Path,可外网邮箱,250rcpt to: //实际收件人(必须内网邮箱),250data //354 开始可以输入具体内容from: random@random //显示的发件人(可以随便填)to: random@random //显示的收件人(可以随便填)comment-comment //具体内容需要和上面的隔一个空行. //用单行.结束,250quit //221
公司新服务器:mail.new.net
不论是否登陆,都可以发送邮件。1、auth login不登陆
telnet mail.new.net 25 //220helo abc //250ehlo abc //返回250-xx 250-xxmail from://原文Return-Path,可外网邮箱,250rcpt to: //实际收件人(必须内网邮箱),250data //354 开始可以输入具体内容from:random@random //显示的发件人(可以随便填)to: random@random //显示的收件人(可以随便填)comment-comment //具体内容需要和上面的隔一个空行. //用单行.结束,250quit //221
2、auth login登陆无论端口是25还是587,在starttls后都无法进行账号登陆。另发现,如果 mail from 及 rcpt to 都是填写QQ邮箱,QQ邮箱会无效。
telnet mail.new.net 25 //220helo abc //250ehlo abc //返回250-xx 250-xxauth login //334base64abcdefg //@前的 base64编码,334base64abcdefg //密码 base64编码,235mail from://原文Return-Path,可外网邮箱,250rcpt to: //实际收件人,可外网邮箱,250data //354 开始可以输入具体内容from:random@random //显示的发件人(可以随便填)to: random@random //显示的收件人(可以随便填)comment-comment //具体内容需要和上面的隔一个空行. //用单行.结束,250quit //221
QQ邮箱服务器:smtp.qq.com 587
需要在QQ邮箱里设置,开启P0P3/SMTP。QQ邮箱必须开启starttls后再登陆,可以发送邮件到外网。
telnet smtp.qq.com 587 //220helo abc //250ehlo abc //返回250-xx 250-xxstarttls //220auth login //334base64abcdefg //@前的 base64编码,334base64abcdefg //授权码 base64编码,235mail from://原文Return-Path,必须为登陆账号,250rcpt to: //实际收件人,可外网邮箱,250data //354 开始可以输入具体内容from:random@random //显示的发件人,可以随便填to: random@random //显示的收件人,可以随便填comment-comment //具体内容需要和上面的隔一个空行. //用单行.结束,250quit //221
说明:
1、发送范围:公司旧邮箱:只能内网。公司新邮箱:不登陆只能内网,登陆了可以外网。QQ邮箱:先starttls然后登陆,可以外网。2、具体发件人收件人:登陆的账号 --> 实际发送人mail from --> 原文Return-Path的内容(如果与data-from设置不同,QQ邮箱会显示:由xxx代发)rcpt to --> 实际收件人data-from --> 显示的发送人(可以随便填)data-to --> 显示的收件人(可以随便填,与实际不同)3、其它:编码及附件未测试
总结:
与mail()一样,不登陆的情况下,只能在内网发送。不过telnet可以用auth login登陆账号,然后可以发送邮件到外网。
6、利用socket,发送SMTP命令发邮件
发送socket大致流程:
$socket = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); //创建socket资源socket_connect($socket, $mail_server_name, $mail_server_port); //连接端口$command = "helo abc \r\n"; // \r\n表示回车socket_write($socket, $command, strlen($command)); //发送命令给服务器$num = socket_read($socket, 1024); //接受状态码及短句
说明:
1、通过多次socket_write()来传送SMTP命令,完成整个发邮件的流程2、\r\n等同于在命令行中按回车3、QQ邮箱无法使用:在auth login命令后:显示错误: unable to read from socket [0]: 远程主机强迫关闭了一个现有的连接。可能是处于安全性的考虑??
7、归纳总结data部分编码问题及邮件的附件格式
查资料想找编码还有正文的格式,但大部分比较凌乱,不然就是RFC文件没勇气。后来想到自己给自己发邮件,然后用查看邮件原文这个功能,去自己总结。目前需求只是发送文本、html的a标签、附件就足够了,因此自己总结的不用太全面。
(1)标题编码:
$subject = "=?UTF-8?B?".base64_encode("标题")."?=";
(2)正文分割:
需要声明:Content-Type: multipart/mixed;其中,mixed表明是混合类型。规定boundary分割,然后按照具体情况分别写具体声明+内容。
boundary分割规则:
boundary是自定义的第一段开始:--boundary第n段开始: --boundary结束: --boundary--boundary是可以嵌套的:如果就文本和附件,一般无需嵌套。
整体类似于:
标题发件人收件人声明编码-类型-boundary声明 --boundary 声明编码-类型 内容 --boundary 声明编码-类型 内容 --boundary 声明编码-类型 --boundary--
(3)带附件的具体正文
声明与内容,内容与后文 都需要有空行,即2次\r\n
html:
$command =附件:
先用fopen()及fread() 读取二进制,然后将二进制内容进行发送
$file_name = iconv('UTF-8', 'GB18030', "文件名.txt" ); //对文件名编码$fp = fopen($file_name, 'rb'); //以二进制形式打开文件$comment = fread($fp, filesize($file_name)); //读取文件内容fclose($fp); //关闭资源然后声明编码-类型并传递数据
$file_name = "=?UTF-8?B?".base64_encode("文件名.txt")."?="; //对文件名编码$command = "Content-Type: application/octet-stream; \r\n"$command .= "name=\"" . $file_name . "\"\r\n"$command .= "Content-Disposition: attachment; filename=\"" . $file_name . "\"\r\n";$command .= "Content-Transfer-Encoding: base64 \r\n";$command .= "\r\n" . base64_encode($comment) . "\r\n\r\n"; //这里的$comment就是读出来的文件二进制数据说明:
1、文件名编码:对文件名的编码有两次,一次是为了打开文件,一次是为了邮件的显示。编码方式不同,不知道能否统一?2、开始的声明中,boundary前必须有一个Tab键的缩进:原因未知
"Mime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"".$boundary."\"\r\nContent-Transfer-Encoding: 8Bit\r\n";3、附件声明:看别人的代码,好像通过文件后缀来判断的类型。而自己只是笼统地用 application/octet-stream;
(4)具体实现:
send_email.php 文件 附件1
自己归纳的发邮件函数缺陷,未添加:1、没有抄送人、密送人。2、没有对命令发送是否成功进行判断(可以通过正则判断返回码)。3、没有选择是否text还是html,全部默认成为了html。4、没有选择是否starttls,如果需要使用到QQ邮箱,需自行添加。5、变量名及代码格式可能不太规范。
email.php 文件 附件2
网上已经封装好的类,通过socket发送邮件。
8、总结:
上述内容大约花了三天时间学习及测试,然后整理就花了一天多...以前总以为socket深不可测,现在起码有了一定的了解,没有那么害怕了。QQ邮箱必须登陆,而且mail from的发件人设置必须是登陆人,可防止有人故意隐藏自己真实地址。所以说,公司新邮箱不用登陆的情况,估计是属于安全漏洞?等PHP代码知识再熟悉之后,得去研究研究那上千行的PHP类...不知道具体实现原理究竟是怎么样的...
9、附件:
附件一:send_email.php
超链接";$file_name[0] = "啊.png";$file_name[1] = "哦.xlsx";$file_name[2] = "额.docx";$file_name[3] = "咦.pdf";send_email($mail_server_name, $username, $passward, $mail_from, $mail_to, $html_comment, $file_name, $subject);/** * 通过socket发送邮件 * @param $mail_server_name 邮件服务器地址 * @param $username 登陆账号 * @param $passward 登陆密码 * @param $mail_from 发件人地址 * @param $mail_to 收件人地址,数组格式 * @param $html_comment 文本内容 * @param array $file_name 附件路径,数组格式 * @param string $subject 标题,默认空 * @param string $mail_server_port 邮件服务器端口 默认25 * @param string $boundary 分割符 */function send_email($mail_server_name, $username, $passward, $mail_from, $mail_to, $html_comment, $file_name=array(), $subject="", $mail_server_port="25", $boundary="ABCDEFG"){ //创建一个socket连接 $socket = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); //连接邮件服务器,需要返回状态码 220 socket_connect($socket, $mail_server_name, $mail_server_port); //helo localhost,需要返回状态码 250 $command = "helo localhost\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //登陆账号,分别返回状态码 334 334 235 $command = "auth login\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //需要返回状态码 334 $command = base64_encode($username)."\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //需要返回状态码 235 $command = base64_encode($passward)."\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //设置邮件发送者,需要返回状态码 250 $command = "MAIL FROM:\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //设置邮件接受者,需要返回状态码 250 $mail_to_length = count($mail_to); for($i=0; $i\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); } //开始发送具体内容,需要返回状态码 354 $command = "DATA\r\n"; socket_write($socket, $command, strlen($command)); socket_read($socket, 1024); //发送具体内容:需要返回状态码 250 $command = "from: ".$mail_from."\r\n"; for($i=0; $i附件二:email.php
addAttachment("123.doc","啊.doc");$smtp->sendmail($to, $from, $subject, $body, $cc, $bcc);class smtp { /* Public Variables */ public $attachments = array(); /* Private Variables */ private $smtp_host; private $smtp_port; private $time_out; private $host_name; private $auth; private $user; private $pass; private $sock; /* Constractor */ public function smtp($smtp_host = null, $smtp_port = null, $user = null, $pass = null, $auth = true) { $this->smtp_host = (!empty($smtp_host)) ? $smtp_host : SMTP_HOST; $this->smtp_port = (!empty($smtp_port)) ? $smtp_port : SMTP_PORT; $this->user = (!empty($user)) ? $user : SMTP_PORT; $this->pass = (!empty($pass)) ? $pass : SMTP_PORT; $this->auth = $auth; $this->time_out = 15; # $this->host_name = "localhost"; $this->sock = FALSE; } /* Main Function */ public function sendmail($to, $from, $subject = "", $body = "", $cc = "", $bcc = "") { $bndp = md5(uniqid("")) . rand(1000, 9999); $bnd = md5(uniqid("")) . rand(1000, 9999); list ($msec, $sec) = explode(" ", microtime()); $mail_from = $this->strip_line_breaks($from); $mail_to = explode(",", $to); $body = preg_replace("/(^|(\r\n))(\\.)/", "", $body); if ($cc != "") $mail_to = array_merge($mail_to, explode(",", $cc)); if ($bcc != "") $mail_to = array_merge($mail_to, explode(",", $bcc)); $headers = "MIME-Version:1.0" . EOL; $headers .= "To: " . $to . EOL; if ($cc != "") { $headers .= "Cc: " . $cc . EOL; } $headers .= "From: $from" . EOL; $headers .= "Subject: " . $subject . EOL; $headers .= "Date: " . date("r") . EOL; $headers .= "X-Mailer: Webmail ver 1.0 (PHP Version/" . phpversion() . ")" . EOL; $headers .= "Message-ID: " . EOL; if (count($this->attachments) > 0) { $headers .= "Content-Type: multipart/mixed;" . EOL . chr(9) . " boundary=\"" . $bndp . "\"" . EOL . EOL; $headers .= '--'.$bndp . EOL; $headers .= 'Content-Type : multipart/alternative; boundary="' . $bnd . '"' . EOL . EOL; $headers .= '--' . $bnd . EOL; $headers .= 'Content-Type: text/plain; charset=utf-8' . EOL; $headers .= "Content-Transfer-Encoding: 8bit" . EOL . EOL; $headers .= $body . EOL; $headers .= '--' . $bnd . EOL; $headers .= 'Content-type: text/html; charset=utf-8' . EOL; $headers .= "Content-Transfer-Encoding: 8bit" . EOL . EOL; $headers .= $body . EOL; $headers .= '--' . $bnd . '--' . EOL; foreach ($this->attachments as $att) { $headers .= "--" . $bndp . EOL . $att; print_R($headers); } $headers .= '--' . $bndp . '--' . EOL; $this->clear_attachments(); } else { $headers .= 'Content-Type : multipart/alternative;boundary="'.$bnd.'"' . EOL . EOL; $headers .= '--'.$bnd . EOL; $headers .= 'Content-Type: text/plain; charset=utf-8' . EOL; $headers .= "Content-Transfer-Encoding: 8bit" . EOL . EOL; $headers .= $body . EOL; $headers .= '--'.$bnd . EOL; $headers .= 'Content-type: text/html; charset=utf-8' . EOL; $headers .= "Content-Transfer-Encoding: 8bit" . EOL . EOL; $headers .= $body . EOL; $headers .= '--'.$bnd.'--' . EOL; } $sent = TRUE; foreach ($mail_to as $rcpt_to) { $rcpt_to = $this->strip_line_breaks($rcpt_to); if (!$this->smtp_sockopen($rcpt_to)) { $this->log_write("Error: Cannot send email to " . $rcpt_to); $sent = FALSE; continue; } if ($this->smtp_send($this->host_name, $mail_from, $rcpt_to, $headers, $body)) { $this->log_write("E-mail has been sent to "); } else { $this->log_write("Error: Cannot send email to "); $sent = FALSE; } fclose($this->sock); } $this->log_write("{$mail_to} send over;"); return $sent; } public function addAttachment($file,$file_name="", $dispo = "attachment") { $file_data = (file_exists($file)) ? file_get_contents($file) : ""; if ($file_data != "") { $filename = basename($file); if(!$file_name) $file_name=$filename; $file_name="=?UTF-8?B?".base64_encode($file_name)."?="; $ext = pathinfo($filename, PATHINFO_EXTENSION); $chunks = chunk_split(base64_encode($file_data)); $parts = "Content-Type: application/$ext; name=\"" . $file_name . "\"" . EOL; $parts .= "Content-Transfer-Encoding: base64" . EOL; $parts .= "Content-Disposition: " . $dispo . "; filename=\"" . $file_name . "\"" . EOL . EOL; $parts .= $chunks . EOL . EOL; $this->attachments[] = $parts; } } private function clear_attachments() { unset($this->attachments); $this->attachments = array(); } /* Private Functions */ private function smtp_send($helo, $from, $to, $header, $body = "") { if (!$this->smtp_putcmd("HELO", $helo)) { //$this->log_write("Error: Error occurred while sending HELO command."); return FALSE; } #auth if ($this->auth) { if (!$this->smtp_putcmd("AUTH LOGIN", base64_encode($this->user))) { //$this->log_write("Error: Error occurred while sending HELO command."); return FALSE; } if (!$this->smtp_putcmd("", base64_encode($this->pass))) { //$this->log_write("Error: Error occurred while sending HELO command."); return FALSE; } } if (!$this->smtp_putcmd("MAIL", "FROM:")) { //$this->log_write("Error: Error occurred while sending MAIL FROM command."); return FALSE; } if (!$this->smtp_putcmd("RCPT", "TO:")) { //$this->log_write("Error: Error occurred while sending RCPT TO command."); return FALSE; } if (!$this->smtp_putcmd("DATA")) { //$this->log_write("Error: Error occurred while sending DATA command."); return FALSE; } if (!$this->smtp_message($header, $body)) { //$this->log_write("Error: Error occurred while sending message."); return FALSE; } if (!$this->smtp_eom()) { //$this->log_write("Error: Error occurred while sending. [EOM]."); return FALSE; } if (!$this->smtp_putcmd("QUIT")) { //$this->log_write("Error: Error occurred while sending QUIT command."); return FALSE; } return TRUE; } private function smtp_sockopen($address) { if ($this->smtp_host == "") { return $this->smtp_sockopen_mx($address); } else { return $this->smtp_sockopen_relay(); } } private function smtp_sockopen_relay() { $this->log_write("Trying to Connect " . $this->smtp_host . ":" . $this->smtp_port . "..."); $this->sock = @fsockopen($this->smtp_host, $this->smtp_port, $errno, $errstr, $this->time_out); if (!($this->sock && $this->smtp_ok())) { $this->log_write("Error: connenct error" . $errstr . " (" . $errno . ")"); return FALSE; } $this->log_write("Connected Ok"); return TRUE; } private function smtp_sockopen_mx($address) { $domain = preg_replace("/^.+@([^@]+)$/", "\1", $address); if (!@getmxrr($domain, $MXHOSTS)) { $this->log_write("Error: Cannot resolve MX \"" . $domain . "\""); return FALSE; } foreach ($MXHOSTS as $host) { $this->log_write("Trying to " . $host . ":" . $this->smtp_port); $this->sock = @fsockopen($host, $this->smtp_port, $errno, $errstr, $this->time_out); if (!($this->sock && $this->smtp_ok())) { $this->log_write("Connect Error ," . $errstr . " (" . $errno . ")"); continue; } $this->log_write("Connected to mx host " . $host); return TRUE; } $this->log_write("Error: Cannot connect to any mx hosts (" . implode(", ", $MXHOSTS) . ")"); return FALSE; } private function smtp_message($header, $body) { fputs($this->sock, $header . "\r\n" . $body); return TRUE; } private function smtp_eom() { fputs($this->sock, "\r\n.\r\n"); return $this->smtp_ok(); } private function smtp_ok() { $response = str_replace("\r\n", "", fgets($this->sock, 512)); if (!preg_match("/^[23]/", $response)) { fputs($this->sock, "QUIT\r\n"); fgets($this->sock, 512); $this->log_write("Error: Remote host returned \"" . $response . "\""); return FALSE; } return TRUE; } private function smtp_putcmd($cmd, $arg = "") { if ($arg != "") $cmd = ($cmd == "") ? $arg : ($cmd . " " . $arg); fputs($this->sock, $cmd . "\r\n"); return $this->smtp_ok(); } private function strip_line_breaks($address) { $address = preg_replace("/([\t\r\n])+/", "", $address); $address = preg_replace("/^.*.*$/", "", $address); return $address; } public function log_write($message) { $message = date("M d H:i:s ") . get_current_user() . "[" . getmypid() . "]: " . $message; file_put_contents(dirname(__FILE__) . '/mail.log', $message . PHP_EOL, FILE_APPEND | LOCK_EX); }}
上一篇: 解说PHP框架 (一) 基本概念