PHP 8.5.7 不自带协程引擎,所谓“协程支持”需依赖 Swow 或 Swoole 等外部扩展;原生 mail()/PHPMailer 同步调用仍会阻塞,必须用 Swow\Coroutine\Socket 替代 fsockopen 并手动封装 SMTP 流程,配合连接复用、限速重试与异常捕获方可实现高效并发发信。

PHP 8.5.7 自带 Swoole 协程支持(需编译启用)或可搭配 Swow 使用,但注意:**PHP 官方不内置协程引擎**,所谓“PHP 8.5.7 协程”实际指你在该版本上运行了兼容的协程扩展(如 Swow 1.4+ 或 Swoole 5.1+)。直接用原生 mail() 或 PHPMailer 同步发 1000 封,哪怕在 PHP 8.5.7 上也照样卡死——协程不是自动魔法,得你亲手调度。
用 Swow 启动并发 SMTP 连接,别碰 fsockopen 阻塞模式
常见错误是把旧式 fsockopen 套进协程里还设 stream_set_blocking($fp, 0),这在 Swow 下无效且易出 Broken pipe。正确做法是用 Swow\Coroutine\Socket 原生非阻塞套接字:
- 先确认已安装
swow扩展(php -m | grep swow),且版本 ≥ 1.4.0 -
SMTP连接必须复用Swow\Coroutine\Socket实例,每次connect()+send()+recv()都要yield - 避免在协程里调用
sleep()、usleep(),改用Swow\Coroutine::sleep() - 连接池不是必须,但单个
Socket并发发 5–10 封后再换新连接,比每封都新建更稳
PHPMailer 不能直接协程化,得包装成 Swow 友好接口
PHPMailer 内部大量使用同步 fsockopen 和 stream_get_contents(),直接 new PHPMailer() 在协程里会阻塞整个调度器。必须剥离其网络层:
- 复制
PHPMailer\SMTP类,把所有fsockopen/stream_socket_client替换为Swow\Coroutine\Socket调用 - 把
smtp->hello()、smtp->auth()、smtp->data()拆成独立协程函数,每个含yield - 别试图 patch
PHPMailer::send()全流程——只取它拼RAW邮件体的能力($mail->getSentMIMEMessage()),网络层全自己写 - 示例关键片段:
$socket = new Swow\Coroutine\Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); $socket->connect('smtp.example.com', 587); $socket->send("EHLO localhost\r\n"); $socket->recv(1024); // yield 自动发生
并发数别瞎设 100,真实瓶颈在 SMTP 服务端限流
本地跑 Swow 发 1000 封,设 go(function () { ... }) 100 次,并不等于 SMTP 服务器真能同时收 100 连接。Gmail、腾讯企业邮箱、阿里云 DirectMail 等普遍限制:
- 单 IP 每分钟新建连接 ≤ 10–20 个(非并发数)
- 单连接每分钟发信 ≤ 5–15 封(视认证等级而定)
- 未认证连接可能被直接 RST,或返回
421 Too many connections
所以实际策略是:用 5–8 个长期复用的协程连接,每连接串行发 100 封,中间加 Swow\Coroutine::sleep(0.3)——比盲目并发更稳、更快、更少被封。
立即学习“PHP免费学习笔记(深入)”;
失败重试和日志必须落盘,协程挂了任务就丢了
协程崩溃不会像 FPM 进程那样留下完整错误堆栈;Swow 的 Coroutine::create() 抛异常时,若没显式 try/catch,任务直接静默消失:
- 每封邮件发送逻辑外层包
try { ... } catch (\Throwable $e) { error_log("Mail #{$id} failed: " . $e->getMessage()); } - 成功/失败状态必须立刻写入数据库或 Redis(用
Swow\Coroutine\Redis,别用 phpredis 同步驱动) - 别依赖
register_shutdown_function清理——协程退出不触发它 - 临时文件(如附件)路径要绝对,且确保 CLI 用户有读权限;
Swow不继承 Web 服务器的 umask



















