模仿workerman的玩具

2018年7月25日 0 作者 筱枫

前两天写了个workerman的代码分析,现在就是自己模仿其写个类似的玩具,代码尽可能简单
首先,来写个最简单的socket程序,要求是启动后,用浏览器访问能够得到hello,world!即可
代码很简单,看下面

接着在命令行中用php xxx.php启动即可,注意端口8080如果被占用,可以换个其他的端口
之后使用浏览器直接访问 服务器ip:8080即可看到hello,world!
下面来一步步解释下代码,首先创建了一个套接字,接着设置地址可复用,其实不设置这个参数也是可以,但这样就会出现第一个程序关闭后再打开会出现address already use的错误
之后绑定这个套接字至ip和端口,注意,这里的ip写0.0.0.0表示通配所有,你如果只想本机连接,可以直接使用127.0.0.1
然后监听套接字

最后,使用socket_accept阻塞等待连接,如果有连接,这个套接字会立马返回,并且返回一个可以实际读取数据的套接字,注意!原始的sock是无法直接被读写的!
之后则是很常规的写入标准http的信息进去,这里要注意的一点是,所有的协议头前不要有换行符,如果你写成了

浏览器是不会解析的,因为Content-Type前面实际上有两个制表符符号,同时这里面的换行符也不能被省略
当然,你这么写也是可以的

一样没有任何问题,到结尾就是关闭套接字,很常规化的操作。

采用套接字(socket)开发,可以避免陷入复杂的tcp三次握手、重传等细节当中去
这样一个简单的服务器就搭建完成了,很简单,不是么?
这就是阻塞模型,最简单的方式,缺点是性能受影响,最大的问题在于socket_accept这里,当能够继续读取之后,就要开个新的线程去处理数据,当然在当前这个线程处理也是可以的

那我们来改写一下,采用select的方式
新的代码如下

代码跟之前相比没有太大的变化,将其中的http编码的东西提取出来了,可以复用
其他的主要就是设置为非阻塞模式,然后开始进行轮询

这行代码,主要是筛选传进去的$read是否是可读
如果之前有两个socket1和socket2,但只有socket1可读,那么这个函数执行后,$read里面就是socket1,因为它是引用传参,所以会改变其中的内容
例如 $read = [socket1, socket2];
执行后
$read = [socket1]

再下面就是模仿workerman进行处理,如果有新的连接,则接收新连接,并将新的套接字放入$read数组中等待读取
如果这个新连接可以读取,则直接读取,然后输出“你好,世界!”即可
代码总体还是非常简单,只是需要注意一些其中的细节。

接下来,我们给这个程序加上fork,用以创建子进程
我们的代码加在第十二行的地方

接着,我们让进程打出他们自己的进程号
然后现在来启动一下

如图,4个子线程,一个主线程,一共五个,其实我们也可以让主线程在最后直接退出即可,代码也很简单,直接判断$childrenNum === 0即可
接下来,让我们在打印消息的地方,也就是原本的34行前加一段代码,用于打印出pid,这样,我们就可以知道是哪个进程在处理这个socket连接

接着再重新启动,然后用浏览器访问下,这里我用了两个不同的浏览器访问,所以可以看到是不同的子进程在处理socket连接

当一个socket资源被不同的进程所持有时,由内核尽量公平的分配,这就是为什么会是不同的子进程处理的原因
读者可以多试试,当一个浏览器访问时,是由这个子进程处理,那么接下来,这个浏览器的请求都是由这个子进程处理了。

本文到这里便告一段落了,剩下的也没有什么太多深究的地方,无非就是些信号量,进程间的通讯问题了,与这些核心内容没有太多的关联,如果有兴趣,可以看看 “linux进程通信”之类的信息

最后附上个人的简单压测代码

测试环境为子进程设置为4,然后压测脚本通过php启动,一共开了5个
在我这台虚拟机上的测试结果
使用阻塞连接: 81秒
使用select+轮询的方式:50秒
可以看到,效率获得了巨大的提升,如果再使用libevent的话,效率还能更高