🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## s-socket ```php <?php /** * 学习 tcp/ip socket 能够让你了解计算机网络是如何工作的,掌握它就是掌握了进入互联网的钥匙 * 熟练掌握计算机网络原理能够让你的技术上一个新的台阶,而不再只局限于应用层的CURD了 * socket 是计算机程序通信的基础,有了套接字,不同的程序和不同的计算机就能够相互通信 * 从此站在底层看上层应用就会有一览众山小的感觉,如 redis mysql pdo ,http 请求、响应是什么原理? * 平时在业务中使用时有没有想过它们背后是怎么工作的呢,性能瓶颈在哪里呢? * 从底层去看,这些都了无秘密,这样优化上层应用的性能问题就很容易了,对各种眼花缭乱的中间件也不会再觉得望而生畏了 * 不过最为重要的是,学习这些设计背后的思想,思考为什么这样设计,并逐渐理解软件设计过程中的难处,以及如何平衡和取舍 * 最后忘记你学到的全部内容,剩下的就是你真正学会的了 */ /** * socket 五步曲 * 1. 创建 * 2. 绑定 * 3. 监听 * 4. 接受 * 5. 收发 */ // cmd: telnet 127.0.0.1 5000 function getSockErrorCode($sock = null) { return $sock ? socket_last_error($sock) : socket_last_error(); } function getSockError($sock = null) { $code = getSockErrorCode($sock); if ($code > 0) { } else { return ''; } $msg = socket_strerror($code); $msg = iconv("GBK", "utf-8", $msg); return "socket error: [{$code}] {$msg}"; } /** * 创建一个套接字资源 * 1. 规定了:通讯协议域簇、套接字类型、传输协议 */ $server_sock = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')); /** * 为套接字绑定一个端口 * * 1. 默认情况一个端口只能被一个进程绑定,重复绑定会报端口被占用的错误 * 2. 如果 内核支持 so_reuseport 参数时就可以实现多个进程绑定同一个端口 * socket_set_option($server_sock, SOL_SOCKET, 'so_reuseport', 1); * * https://blog.csdn.net/u010565545/article/details/99244959 * 127.0.0.1 本地回环地址(之一),本机ip,虚拟网卡的ip,如果你不知道本地外网ip的话就用这个 * 0.0.0.0 所有 本机ip */ socket_bind($server_sock, '127.0.0.1', 2347); /** * 开始监听此套接字 */ socket_listen($server_sock); /** * 套接字操作:连接、接受、接收、发送...... * * 当一个操作在一个阻塞的套接字上执行时,脚本将暂停执行,直到它收到一个信号或者它可以执行该操作。 * * socket_set_nonblock() 函数在由socket参数指定的socket上设置 O_NONBLOCK 标志。 * socket_set_block() 函数删除了由socket参数指定的套接字上的 O_NONBLOCK 标志。 * * 阻塞的 socket 上: * 1. 在套接字上进行操作时会被阻塞,直到收到信号或可操作时才会执行该操作 * 2. 信号即 socket 收到操作系统的信号,表示可以进行 可读、可写 等操作了 * 3. 套接字默认是阻塞模式的 * * 非阻塞的 socket 上: * 1. 在套接字上进行操作时不会被阻塞,但不一定调用成功 * 2. 只有在收到信号或可操作时才能执行该套接字操作,否则会导致调用失败,返回 false * 3. 注意,非阻塞会使阻塞性质(原本会导致阻塞)的套接字操作调用失败,返回 false * 所以需要判断当返回 false 时属于哪种情况,真有错,还是 非阻塞模式时调用了会导致阻塞的套接字操作 * */ // socket_set_nonblock($server_sock); // 将 server_sock 设为非阻塞 /* * 接受一个客户端的连接(每次接受一个客户端连接,返回值为一个 客户端 socket) * * 1. 可能会阻塞,取决于 server_sock 是否为阻塞的 * 2. 它会阻塞在等待客户端的连接上,直到有新的客户端连接 * 3. 如果该套接字上有多个排队的连接,将使用第一个。如果没有待处理的连接,socket_accept()将阻塞 * 4. 为非阻塞模式时,在没有客户端连接时,立即返回 false,所以非阻塞模式 应该使用 socket_select * 5. 返回 false 可能是 客户端 socket 已关闭等原因,也有可能是 非阻塞模式下 连接队列为空时,需要使用 socket_last_error socket_strerror 来判断具体情况(socket error: [10035] 无法立即完成一个非阻止性套接字操作。) * 6. 返回的 socket 实例不能被用来接受新的连接 */ $client_sock = socket_accept($server_sock); // echo getSockError($server_sock); // var_dump($client_sock);exit; // 客户端sock 还可以设置非阻塞 socket_set_nonblock($client_sock); /** * 从客户端 socket 上读取数据 * * 1. 可能会阻塞,取决于 server_sock 是否为阻塞的 * 2. 它会阻塞在读取内核 socket 可读缓冲区上,直到有可读数据,可读时返回读取到的数据 * 3. 为非阻塞模式时,在没有可读数据时,立即返回 false,有可读数据时,返回读取到的数据 * 4. 可设置每次最大读取字节,当然最终取决于实际可读的内容长度,或者也可以使用第三个参数 每次 \r, \n, \0 时结束 * 5. 建议设置最大读取长度,以控制每次从内核读取数据的大小,使内存消耗在可控范围内(建议 65535 一个数据包 最大的数据部分长度) * 6. 在读取错误时返回 false(并且会报错 Warning) 这可能是 客户端 socket 已关闭等原因,可使用 socket_last_error socket_strerror 来获取错误信息 * 7. 阻塞模式时,如果返回空串,可能时远端报错了,不可再读了 * * 同样的,返回 false 时要注意判断:socket error: [10035] 无法立即完成一个非阻止性套接字操作。 * * 可读吗,可读,但只能读一点点 */ // $buf = socket_read($client_sock, 1024); // echo getSockError($client_sock); // var_dump($buf);exit; socket_clear_error($client_sock); // socket_set_nonblock($client_sock); $msg = 'you input: ' . $buf; // socket_set_option($client_sock, SOL_SOCKET, SO_SNDBUF, 2); // // 65536 // $sndbuf = socket_get_option($client_sock, SOL_SOCKET, SO_SNDBUF); // echo 'sndbuf: ' . $sndbuf . PHP_EOL; $msg = str_repeat($msg, 1000000) . PHP_EOL . 'xiaobu' . PHP_EOL; echo 'msg len: ' . strlen($msg) . PHP_EOL; /** * 往客户端 socket 上写入数据 * * 1. 可能会阻塞,取决于 server_sock 是否为阻塞的 * 2. 它会阻塞在写入内核 socket 写入缓冲区上,直到有可写入空间 * 3. 为非阻塞模式时,在没有可写入空间时,立即返回 0或 false,可写入时,返回实际写入的数据字节数 * 4. 可设置每次最大写入字节,当然最终写入数据长度取决于缓冲区大小 * 5. 建议设置最大写入长度为要写入的内容长度,期望是能一次写入完毕 * 6. 在写入错误时 返回 返回 false 这可能是 客户端 socket 已关闭等原因,可使用 socket_last_error socket_strerror 来获取错误信息 * * socket_write()不一定会写入给定缓冲区中的所有字节。根据网络缓冲区等情况,虽然你的缓冲区更大,但只有一定数量的数据,甚至一个字节被写入,这是有效的。你必须注意,以免你无意中忘记传输其余的数据。 * * 可写吗,可写,但只能写一点点 */ $l = socket_write($client_sock, $msg, strlen($msg)); // 也可能阻塞,或者根本没有完全写完数据 echo getSockError($client_sock); var_dump($l); sleep(3); exit; /** * 与此客户端断开连接,通常是业务交互完毕了,并且不需要保持长连接 */ socket_close($client_sock); /** * 关闭服务端 socket,结束服务,这意味着服务关闭,通常在关闭服务器时才会这么做 */ socket_close($server_sock); // ================================= // 上面这样 很多操作是阻塞的,并且操作不是一次能完成的,这样写起来很麻烦 // 1. 如果能监听 socket 何时可操作就好了 // 2. 如果能知晓 一个 socket 何时可操作,提前将 操作设置成回调,这就是事件了 // 3. 如果不断的监听,并触发回调,就是事件循环了 // 监听 socket 状态 // socket_select( array|null &$read, array|null &$write, array|null &$except, int|null $seconds, int $microseconds = 0) : int|false $read = $write = $except = [$client_sock]; $ret = socket_select($read, $write, $except, 1, 0); // 打断一下,先抛出两个问题,慢慢思考 // 1. 客户端 与 服务端 收发数据的长度是不可控的,怎么判断收到了一个完整的内容呢,这就是粘包问题 // 答:每个内容末尾加一个特殊标记位,如 末尾\n,或者如 http 协议中的 Content-Length: xx 来确定内容长度进行区分 // 2. http 协议 如何 确定 请求-响应 之间的对应关系呢? // 答:有可能多个请求和响应是连续的,且顺序不可预测,那么只能通过请求标记与响应标记来确定关联性了 // 但是 http 协议没有规定这方面的内容,现在是由 浏览器实现的:要复用 tcp连接 只能一个 请求-响应 完毕才会发送第二个请求 // 所以也就不存在这个问题了。 // https://www.cnblogs.com/everlose/p/12779773.html // https://zhuanlan.zhihu.com/p/29609078 http2 才支持这个功能 // 3. 一个连接上能同时交替发送两个内容吗? // 答:不能,除非自己实现帧数据协议。没必要实现这样的功能,没意义,与其两个包都不能快点发送完,还不如早点让一个包发送完 // 感想:客户端的情况要简单些(虽然也可以做的复杂,但没必要只是徒增烦恼),因为不像服务端一样要考虑为多人服务(不能使用阻塞),所以可以粗暴一些,这样能简化和避免很多问题。如 可以使用阻塞的方式收发消息 // 这样减少了复杂度,更容易实现也更稳定。如 直接避免了并发交替发送数据包的情况。 // 客户端编码习惯了同步阻塞的方式,这样开发更简单直观,试想 你写的 curd 不都是同步操作 mysql 的 // onRead: 一次可读信号 // onMessage: 一个完整的消息内容接收完毕 // onWrite: 一次可写信号 // onSend: 一个完整的消息内容发送完毕 // ========================== // https://blog.csdn.net/wangjiben/article/details/40679587?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-4.no_search_link&spm=1001.2101.3001.4242.3 // 计算信息缓冲区大小 // Calculate message buffer size // socket_cmsg_space() // 向连接的套接字发送数据(数据已从缓冲区发出才返回) // Sends data to a connected socket // socket_send( Socket $socket, string $data, int $length, int $flags) : int|false // 向一个套接字发送消息,无论它是否连接。 // Sends a message to a socket, whether it is connected or not // socket_sendto( Socket $socket, string $data, int $length, int $flags, string $address, int|null $port = null) : int|false // 发送消息 // socket_sendmsg( Socket $socket, array $message, int $flags = 0) : int|false // 写入一个套接字 写到缓冲区(数据可能还没被网卡发出去) // Write to a socket // socket_write( Socket $socket, string $data, int|null $length = null) : int|false // ---- // 从一个套接字中读取最大长度的字节 // Reads a maximum of length bytes from a socket // socket_read( Socket $socket, int $length, int $mode = PHP_BINARY_READ) : string|false // 从已连接的socket接收数据 // 与 socket_read 类似 但可以 使用参数 flags 控制函数功能,如 指定至少读到某个字节长度,和 “重复读” // socket_recv( resource $socket, string &$buf, int $len, int $flags) : int // 从一个套接字接收数据,无论它是否面向连接。同 socket_recv 类似 // Receives data from a socket whether or not it is connection-oriented // socket_recvfrom( Socket $socket, string &$data, int $length, int $flags, string &$address, int &$port = null) : int|false // 读取消息 // Read a message // socket_recvmsg( Socket $socket, array &$message, int $flags = 0) : int|false // 读写超时控制 任何时候超时控制都是必不可少的,并且要特别小心 feof 之类的可能会陷入无限循环的情况 // https://blog.csdn.net/q1007729991/article/details/71078044 // stream_set_timeout // socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec'=>$sec, 'usec'=>$usec)); // socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array('sec'=>$sec, 'usec'=>$usec)); // 关闭一个套接字的接收、发送或两者都关闭。 // 相关的一个或多个缓冲区可能被清空,也可能不被清空。 // Shuts down a socket for receiving, sending, or both // socket_shutdown( Socket $socket, int $mode = 2) : bool // 服务端 // socket_write // socket_read // 客户端 // socket_send // socket_sendto // socket_sendmsg // socket_write // socket_read // socket_recv // socket_recvfrom // socket_recvmsg // https://blog.csdn.net/csdn_zhang99/article/details/81669793 // 异步架构程序设计原则 // 1、回调函数不可以执行过长时间,因为一个loop中可能包含其他事件,尤其是会影响一些准确度要求比较高的timer。 // 2、尽量采用库中所缓存的时间,有时候需要根据时间差来执行timeout之类的操作。当然能够利用库中的timer最好。 // 任务不要做复杂的事,不要在io上阻塞 ``` ---- [TCP-IP协议 · php笔记 · 看云](https://www.kancloud.cn/xiak/php-node/2545482) > 接收方 read buffer 满(说明应用程序处理能力过载),**不再接受数据, 不 ack 了,那么 发送方 write buffer 很快也就满了(等不到 ack 就不删除 write buffer)**,不能再发了。这算是一种 常规的TCP拥塞控制。 [libev_大张-CSDN博客_libev](https://blog.csdn.net/csdn_zhang99/article/details/81669793 ) ~~~ 异步架构程序设计原则 1、回调函数不可以执行过长时间,因为一个loop中可能包含其他事件,尤其是会影响一些准确度要求比较高的timer。 2、尽量采用库中所缓存的时间,有时候需要根据时间差来执行timeout之类的操作。当然能够利用库中的timer最好。 任务不要做复杂的事,不要在io上阻塞 ~~~ [HTTP 的前世今生:一次性搞懂 HTTP、HTTPS、SPDY、HTT_请求](https://www.sohu.com/a/275505518_497161) > SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。 [ReactPHP: Event-driven, non-blocking I/O with PHP - ReactPHP](https://reactphp.org/) > ReactPHP is non-blocking by default. Use workers for blocking I/O. > 大量的连接复用少数的进程,socket fd 事件循环是非阻塞的,但是 工人进程处理业务的部分是阻塞的。 [The Illustrated QUIC Connection: Every Byte Explained](https://quic.xargs.org/) [图解 QUIC](https://cangsdarm.github.io/illustrate/quic)