PHP并不只是用来写网站和接口,借助 sockets 扩展它同样可以完成底层的网络编程。理解Socket通信的关键,在于把它看作操作系统开放的一扇“门”:应用进程通过这扇门,把数据交给内核协议栈,再由网卡发往网络另一端。无论服务端还是客户端,通信双方都要借助这扇门交换报文。

一、Socket与TCP、UDP的基础概念
Socket翻译为“套接字”,它不是某种协议,而是一种编程抽象。在PHP中,socket_create 函数会向操作系统申请一个Socket资源,你需要指定地址族(如 AF_INET 表示IPv4)、套接字类型(SOCK_STREAM 或 SOCK_DGRAM)以及协议(SOL_TCP 或 SOL_UDP)。其中类型决定了通信特征。
TCP对应的类型是 SOCK_STREAM,它是面向连接的字节流协议。通信前必须先完成三次握手,之后数据像水流一样连续传输,内核保证不丢失、不重复、按序到达。UDP对应的类型是 SOCK_DGRAM,它是无连接的报文协议,每次发送都是一个独立数据报,不保证送达,也不保证顺序,但开销小、延迟低。
初学者容易混淆“端口”和“连接”。端口只是进程在主机上的编号,一台机器上可以同时有TCP 8080和UDP 8080,它们互不冲突。TCP靠“四元组”(源IP、源端口、目的IP、目的端口)区分连接,而UDP每个数据报都自带对端地址,不需要维护连接状态。
1.1 阻塞与非阻塞模式
默认情况下PHP Socket是阻塞的,也就是说执行 socket_recv 时,如果对方没发数据,当前进程会一直停在那里等待,直到有数据或超时。这在命令行脚本里没问题,但在Web环境会卡死请求。通过 socket_set_nonblock 可切换为非阻塞,此时没有数据会立即返回 false,需要配合循环或 select 机制使用。
对于入门练习,建议先写阻塞版脚本,在终端用 php 命令运行,思路更清晰。等理解收发节奏后,再引入 socket_select 做多客户端并发,避免一开始就陷入事件循环的理解负担。
二、TCP通信入门示例
下面用一段最简单的TCP回显服务演示完整流程:创建Socket、绑定端口、监听、接收连接、循环读写。服务端代码如下:
<?php
// 创建TCP Socket
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($server === false) {
die('socket_create 失败: ' . socket_strerror(socket_last_error()));
}
// 绑定到本地9501端口
socket_bind($server, '0.0.0.0', 9501);
// 开始监听,等待连接
socket_listen($server, 5);
echo "TCP服务已启动,监听端口 9501n";
while (true) {
// 接受客户端连接,阻塞等待
$conn = socket_accept($server);
if ($conn === false) {
continue;
}
// 读取客户端发送的数据
$buf = socket_read($conn, 1024);
if ($buf !== false) {
// 将收到的内容原样返回
socket_write($conn, '你发送的是: ' . $buf);
}
socket_close($conn);
}
上面的代码每次只处理一个连接,处理完才接受下一个,属于串行模型。但它清楚展示了TCP服务端生命周期:socket_create 申请资源,socket_bind 绑定地址,socket_listen 进入被动监听,socket_accept 取出已完成握手的连接。
客户端可以用PHP自带函数快速验证,不必自己写Socket。比如使用 fsockopen 建立连接并 fwrite 发送字符串,再 fgets 读取返回。这样你能把精力放在服务端逻辑上,确认“发送-回显”闭环是否正常。
2.1 TCP字节流边界问题
TCP是流协议,没有消息边界。客户端分两次发送“hello”和“world”,服务端一次 socket_read 可能读到“helloworld”,也可能只读到“hel”。因此真实项目要自定义协议,例如在消息头用4字节声明长度,或者直接用换行符切割。入门阶段可在发送内容末尾加换行,服务端按行缓冲。
另一个坑是 socket_read 的第三个参数。传入 PHP_BINARY_READ 按字节读,传入 PHP_NORMAL_READ 遇到换行才返回。理解这两个模式的差异,能减少很多“为什么收不全”的疑惑。
三、UDP通信入门示例
UDP不需要listen和accept,服务端绑定后直接用 socket_recvfrom 收数据报,同时拿到对端地址,再用 socket_sendto 回包。下面是个最简UDP接收脚本:
<?php
// 创建UDP Socket
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($sock, '0.0.0.0', 9502);
echo "UDP服务已启动,监听端口 9502n";
while (true) {
// 接收数据报,并获取发送方地址
$from = '';
$port = 0;
socket_recvfrom($sock, $buf, 1024, 0, $from, $port);
echo "收到来自 {$from}:{$port} 的数据: {$buf}n";
// 回送确认
socket_sendto($sock, '已收到: ' . $buf, strlen('已收到: ' . $buf), 0, $from, $port);
}
这段代码没有连接概念,每次循环都是独立数据报。UDP适合高频小包场景,比如内部服务健康探测:每秒发一次ping,丢几个包也无所谓,总延迟比TCP握手重连低得多。
但要注意,UDP单次数据报大小受限于网络MTU,通常不建议超过1472字节,否则IP层分片,丢一个分片整包作废。如果业务必须传大块数据,还是老老实实用TCP,或自己在UDP上做可靠性重传,那基本等于造轮子。
3.1 如何选择TCP和UDP
简单判断:数据不能丢、要按顺序、量不大,选TCP;数据允许偶尔丢失、追求极低延迟、广播或多播,选UDP。实际系统常混合使用,例如游戏用UDP传坐标,用TCP传聊天和交易。
| 对比项 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 保证送达和顺序 | 不保证 |
| 速度 | 较慢,有握手开销 | 快,头部仅8字节 |
| 适用 | 支付、登录、文件 | 音视频、游戏、探测 |
四、常见错误与排查思路
绑定端口失败提示 address already in use,多半是上次脚本没正常退出,端口处于 TIME_WAIT。可以在 bind 前调用 socket_set_option 设置 SO_REUSEADDR 允许复用。另外用命令行运行PHP脚本时,记得用 Ctrl+C 结束,异常退出可用 netstat 或 lsof 查杀进程。
收不到数据先确认防火墙是否放行端口,再确认绑定的IP。写 127.0.0.1 只能本机连,写 0.0.0.0 才能外部访问。用 tcpdump 或 Wireshark 抓包,比盲目改代码更高效。掌握这些基础,PHP Socket编程就算真正入门了。
PHP_socketsocket_programmingTCP_UDP修改时间:2026-08-06 16:18:48