LJKのBlog

学无止境

bit

比特、位,量度信息的最小单位,状态为 0 和 1

B(Byte)

字节,1B = 8bit

KB

千字节,1KB = 1024B

MB

兆字节

为什么 32 位 cpu 只支持 4G 内存?

准确的说是 cpu 的地址总线位宽为 32 位的时候最大只能支持 4G 的寻址

寻址就是 CPU 最大能查找多大范围的地址。CPU 的寻址以字节为单位 (字节是最小可寻址单位)

32 位 CPU 一次只能处理 32 位的数据(就是 4 个字节),2 的 32 次方 bit=4G,所以 32 位 CPU 支持的最大寻址空间就是 4G(不考虑使用 PAE——物理地址扩展技术),如果插了一根 8G 的内存条,超过 4G 的内存部分跟不用不到

C 语言中各个变量占用的内存大小

https://blog.csdn.net/sinan1995/article/details/79577106

  • 16 位编译器

    1
    2
    3
    4
    5
    6
    7
    char/unsigned char :1字节
    char*:2字节
    short int:2字节
    int/unsigned int:2字节
    long int:4字节
    float:4字节
    double:8字节
  • 32 位编译器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char/unsigned char :1字节
    char*:4字节
    short int:2字节
    int/unsigned int:4字节
    long int:4字节
    float:4字节
    double:8字节
    long long:8字节
    long double:12字节
  • 64 位编译器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char/unsigned char :1字节
    char*:8字节
    short int:2字节
    int/unsigned int:4字节
    long int:8字节
    float:4字节
    double:8字节
    long long:8字节
    long double:16字节

int 占用四字节,32 位,范围 -2^31 \~ 2^31-1,而 long int 根据编译器不同,表示的范围从 32 位到 64 位,它可以保证至少 32 位。

long int 不是标准的 64 位整型,而long long 是,它在所有的编译器中都表示 64 位整型,表示范围 -2^63 ~ 2^63-1

关于 __int64:
在 C/C++中,64 为整型一直是一种没有确定规范的数据类型。现今主流的编译器中,对 64 为整型的支持也是标准不一,形态各异。一般来说,64 位整型的定义方式有 long long 和__int64 两种(VC 还支持_int64),而输出到标准输出方式有 printf(“%lld”,a),printf(“%I64d”,a),和 cout << a 三种方式。

关于 float 和 double 的表示范围和精度:

float: 1bit(符号) + 8bit(指数位) + 23bit(尾数位)
double: 1bit(符号) + 11bit(指数位) + 52bit(尾数位)

浮点数值 = (符号)(1.尾数) * (2)^(带符号的指数)
上述公式的 1 和 2 就是这么规定的,不必太在意

并发: 任务多于 cpu 核心数, 通过操作系统的各种任务调度算法, 实现多个任务”一起”执行(实际上总有一些任务不在执行, 因为切换任务的速度非常快, 看上去是一起执行而已)

并行: 指的是任务数小于等于 cpu 核心数, 即任务真的是一起执行

进程、线程、协程的区别

  • 进程是资源分配的单位
  • 线程是操作系统调度的单位
  • 进程切换需要的资源很大, 效率很低
  • 线程稍好
  • 协程最好
  • 多进程、线程根据 cpu 核数不一样可能是并行的,但是协程是在一个线程中,所以肯定是并发

线程 thread

  1. python 的 thread 模块是比较底层的模块, threading 模块对 thread 做了一些包装, 可以更加方便的被调用
  2. Python 的 threading.Thread 类有一个 run 方法, 用于定义线程的功能函数, 可以在自己的线程中覆盖该方法, 而创建自己的线程实例后, 通过 Thread 的 start 方法启动该线程, 交给 Python 虚拟机进行调度, 当该线程获得指定机会的时候, 就会调用 run 方法指定线程
  3. 多线程程序的执行顺序是不确定的

线程池

关于设置线程池的进程数:
设 cpu 核心数为 N
IO 密集型: 线程池设置为 2N + 1
CPU 密集型: 线程池设置为 N + 1
如果不设置, 默认就是 N

总结:

  1. 每个线程都有一个默认的名字, python 自动为线程指定一个名字
  2. 当线程的 run 方法结束时, 该线程结束
  3. 无法控制线程调度程度, 但可以通过其他方式来影响线程调度方式

多线程 - 共享全局变量

  • 在一个进程内的所有线程共享全局变量, 很方便在多个线程间共享数据
  • 缺点就是: 线程对全局变量随意更改可能造成多线程之间对全局变量的混乱(即线程非安全)
  • 如果多个线程同时对一个全局变量操作, 会出现资源竞争问题, 从而数据结果会不正确

进程

进程池中的 pid 都是固定的, 设置了最大链接数, 就有多少个 pid
通过进程池调用进程, 进程池满了之后, 当前进程执行完, 后面排队的进程才会执行

1

进程间通信

使用 Queue

1
2
3
4
5
6
7
8
9
from multiprocessing import Queue
q = Queue(10) # 初始化一个Queue对象, 最多可以接受10条消息
# q.qsize() 返回当前队列包含的消息数量
# q.empty() 判断队列是否为空, 为空返回true
# q.full() 判断队列是否已满
# q.get([block[,timeout]) 获取队列中的一条消息, 然后将其从队列中移除, block默认为true, 表示如果队列中没有消息则等待timeout秒, 没有设置timeout的话就会一直等待下去
# q.get_nowait() 相当于q.get(false),表示队列为空,不等待马上抛出"Queue.Empty"异常
# q.put(item[,block[,timeout]]) 将item写入队列, block默认值是true, 表示如果队列已满, 则等待timeout秒, 不设置timeout的话就一直等待下去
# q.put_nowait(item) 相当于Queue.put(item,false), 如果队列已满, 直接抛出"Queue.Full"异常

进程池

当需要创建的子进程数量不多时,可以直接利用 multiprocessing 中的 Process 动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到 multiprocessing 模块提供的 Pool 方法。
初始化 Pool 时,可以指定一个最大进程数,当有新的请求提交到 Pool 中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from multiprocessing import Pool
def worker(msg):
print(msg)
po=Pool(3) #定义一个进程池,最大进程数3
for i in range(10):
po.apply_async(worker,(i))
print("----start----")
po.close() #关闭进程池,关闭后po不再接收新的请求
po.join() #等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")

# apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
# close():关闭Pool,使其不再接受新的任务;
# terminate():不管任务是否完成,立即终止;
# join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;

进程池中的进程之间的通信

如果要使用 Pool 创建进程,就需要使用 multiprocessing.Manager()中的 Queue(),而不是 multiprocessing.Queue(),否则会得到一条如下的错误信息:RuntimeError: Queue objects should only be shared between processes through inheritance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding:utf-8 -*-

# 修改import中的Queue为Manager
from multiprocessing import Manager,Pool
import os,time,random

def reader(q):
print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print("reader从Queue获取到消息:%s" % q.get(True))

def writer(q):
print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in "itcast":
q.put(i)

if __name__=="__main__":
print("(%s) start" % os.getpid())
q = Manager().Queue() # 使用Manager中的Queue
po = Pool()
# 使用阻塞模式创建进程,这样就不需要在reader中使用死循环了,可以让writer完全执行完成后,再用reader去读取
po.apply_async(writer, (q,))

time.sleep(1) # 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据

po.apply_async(reader, (q,))
po.close()
po.join()
print("(%s) End" % os.getpid())

GIL:

在 Python 多线程中, 每个线程的执行顺序:

  1. 获取 GIL
  2. 执行代码知道 sleep 或者是 python 虚拟机将其挂起
  3. 释放 GIL

可见: 某个线程想要执行, 必须先拿到 GIL, 可以把 GIL 看作是”通行证”, 并且在一个 python 中, GIL 只有一个。拿不到通信证的线程,就不允许进入 CPU 执行。所以, python 中的多线程其实是”伪多线程”, 不管我们开多少线程, 都不能做到并行执行, 无法利用 cpu 的多核优势

总结 :  在 python 中, 对于多核心 CPU

  1. CPU 密集型任务适合多进程, 充分利用 CPU 多核心的计算优势, 多进程一般用于资源隔离, 或者弥补那些多核支持不好的语言。Python 中的多进程就属于后者
  2. IO 密集型任务适合多线程, 因为 IO 密集型代码瓶颈是 IO 不是 CPU, 用多线程比多进程适合, 多进程耗费的系统资源多, 切换速度慢

话说回来: CPU 密集型任务由于主要消耗 CPU 资源,因此,代码运行效率至关重要。Python 这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用 C 语言编写。

不过对于 IO 密集型任务,涉及到网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多,CPU 效率越高,但也有一个限度。常见的大部分任务都是 IO 密集型任务,比如 Web 应用。

IO 密集型任务执行期间,99%的时间都花在 IO 上,花在 CPU 上的时间很少,因此,用运行速度极快的 C 语言替换用 Python 这样运行速度极低的脚本语言,完全无法提升运行效率。对于 IO 密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C 语言最差。

协程

协程,又称微程序,纤程。英文名 Coroutine。

协程是 python 个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带 CPU 上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU 上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定, 比如子程序 A、B

1
2
3
4
5
6
7
8
def A():
print('1')
pirnt('2')
print('3')
def B():
print('a')
print('b')
print('c')

假设由协程执行,在执行 A 的过程中,可以随时中断,去执行 B,B 也可能在执行过程中中断再去执行 A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

但是在 A 中是没有调用 B 的,所以协程的调用比函数调用理解起来要难一些。
看起来 A、B 的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核 CPU 呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python 对协程的支持是通过 generator 实现的。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None) # 启动
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 生产n, 将n传递消费者
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

consumer 是一个 generator , 把一个 consumer 传入 produce 后:

  1. c.send(None) 启动生成器

  2. 一旦生产了东西, 通过 c.send(n)切换到 consumer 执行

  3. consumer 通过 yield 拿到消息, 处理, 又通过 yield 把结果传回

  4. produce 拿到 consumer 处理的结果, 继续生产下一条消息

  5. produce 决定不生产了, 通过 c.close()关闭 consumer, 整个过程结束

普通的 for 循环

1
2
3
4
5
6
7
8
9
>>>i = 0
>>> seq = ['one', 'two', 'three']
>>> for element in seq:
... print i, seq[i]
... i +=1
...
0 one
1 two
2 three

for 循环使用 enumerate

1
2
3
4
5
6
7
>>>seq = ['one', 'two', 'three']
>>> for i, element in enumerate(seq):
... print i, element
...
0 one
1 two
2 three

for 循环使用 range

range() 创建一个整数列表

1
2
3
4
5
6
7
for i in range(5):
print(i)
0
1
2
3
4

迭代器

迭代就是遍历,一个实现了__iter__方法和__next__方法的对象,就是迭代器。

1
手写一个迭代器: __init__初始化变量; __next__处理变量; __iter__返回self

生成器

利用迭代器,我们可以在每次迭代获取数据(通过 next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合 next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器

创建生成器 1

要创建一个生成器, 方法有很多, 第一种方法很简单, 只要把列表的[]改成()就可以了

1
2
L = [x*2 for x in range(5)]
G = (x*2 for x in range(5))

创建生成器 2

简单的来说, 只要在 def 中有 yield 关键字的就称为生成器
函数会保存 yield 标记的变量, next()唤醒生成器, 获取到 yield 标记的变量
除了 next(), 还可以使用 send()唤醒, send()相比 next()的优势是可以传参, next()等价于 send(None)
但是第一次唤醒只能是 next()或者 send(None), 不能传一个非 none 值给一个未启动的生成器, 这点区别于 PHP

当执行 next()时,第 1 个 yield 到第二个 yield 之间的的语法被执行。然后返回第二个 yield 标记的值

上下文管理器

任何实现了 enter() 和 exit() 方法的对象都可称之为上下文管理器 ,上下文管理器对象可以使用 with 关键字, 显然, 文件(file)对象也实现了上下文管理器。

Python 还提供了一个 contextmanager 的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield 将函数分割成两部分,yield 之前的语句在 enter 方法中执行,yield 之后的语句在 exit 方法中执行。紧跟在 yield 后面的值是函数的返回值。

1
2
3
4
5
6
7
from contextlib import contextmanager

@contextmanager
def my_open(path, mode):
f = open(path, mode)
yield f
f.close()

调用:

1
2
with my_open('out.txt', 'w') as f:
f.write("hello , the simplest context manager")

总结

Python 提供了 with 语法用于简化资源操作的后续清除操作,是 try/finally 的替代方法,实现原理建立在上下文管理器之上。此外,Python 还提供了一个 contextmanager 装饰器,更进一步简化上下管理器的实现方式。

当粉丝使用微信, 向公众号发送一条消息时, 微信服务器会主动向开发者服务器发送一个请求. 并携带粉丝发送的消息. ( 以 post 的方式发送 xml 数据 )

$_POST 并不能接受所有以 post 方式发送的请求数据, $_POST 是用来接受 application/x-www-form-urlencoded 类型的请求类型.

application/x-www-form-urlencoded 类型的请求数据的格式 : key=val&key1=val1

而微信向我们开发者服务器发送的请求是 xml 格式, 显然就不是 application/x-www-form-urlencoded 类型. 所以使用$_POST 就不能获取到数据.

使用 php://input 来获取数据.

php 中使用 php://input 来获取内容, php://input 是一个只读信息流, 当请求方式是 post 的 enctype=”multipart/form-data” 的时候 php://input 是无效的。

和 $HTTP_RAW_POST_DATA 比起来,它给内存带来的压力较小,并且不需要任何特殊的 php.ini 设置。

获取 xml 数据:

1
$data = file_get_contents('php://input');

这个$data 就是 xml 格式的数据, 要获取其中的数据(ToUserName、CreateTime、MsgType…), 就要把 xml 进行解析。php 提供了 simplexml_load_string 函数, 将 xml 解析成对象格式的数据,作用类似于 json_decode.

使用 simplexml_load_string 将 xml 转成 php 对象:

1
$xml_obj = simplexml_load_string($data);

首先要安装 php 扩展 xdebug,这里不做介绍

1
2
3
4
5
6
[xdebug]
zend_extension=xdebug.so
xdebug.trace_output_dir=/tmp/xdebug
xdebug.profiler_output_dir = /tmp/xdebug
xdebug.profiler_enable = On
xdebug.profiler_enable_trigger = 1

创建 xdebug 目录

1
2
mkdir  /tmp/xdebug
chown www:www /tmp/xdebug

webgrind 安装

1
git clone https://github.com/jokkedk/webgrind.git

配置虚拟主机,确保能访问到 webgrind 目录

配置 webgrind 的 config.php

1
2
static $storageDir = '/tmp/webgrind';
static $profilerDir = '/tmp/xdebug';

创建 webgrind 目录

1
2
mkdir /tmp/webgrind
chown www.www /tmp/webgrind

分析

选择要分析的文件,然后电机 update 即可

  • Invocation Count:函数调用次数
  • Total Self Cost:函数本身花费的时间
  • Total Inclusive Cost:包含内部函数花费的时间

约定:

$pattern = 正则表达式
$subject = 匹配的目标数据

preg_match() 与 preg_match_all():

  • preg_match($pattern, $subject, [array &$matches])
  • preg_match_all($pattern, $subject, array &$matches)

preg_match_all 的第三个参数是必填的。

除了可以借助 redis

还可以使用 php 内置的 SPL

在 php 的面向对象编程中,总会遇到:

1
2
3
4
5
6
7
8
class test{
public static function test(){
self::func();
static::func();
}

public static function func(){}
}

可你知道 self 和 static 的区别么?

其实区别很简单,只需要写几个 demo 就能懂:

demo for self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car
{
public static function model(){
self::getModel(); // 注意:self
}

protected static function getModel(){
echo "This is a car model";
}
}
Car::model(); // This is a car model

Class Taxi extends Car
{
protected static function getModel(){
echo "This is a Taxi model";
}
}
Taxi::model(); // This is a car model

demo for static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car
{
public static function model(){
static::getModel(); // 注意:static
}

protected static function getModel(){
echo "This is a car model";
}
}
Car::model(); // This is a car model

Class Taxi extends Car
{
protected static function getModel(){
echo "This is a Taxi model";
}
}
Taxi::model(); // This is a Taxi model

总结

self 只能引用当前类中的方法,而 static 关键字允许函数能够在运行时动态绑定类中的方法,也叫延迟绑定

太空船操作符 <=>

用于比较两个表达式,返回-1、0、1

类型声明

以前函数传入参数和返回数据都是不需要限制类型的。

php7 中可以通过修改declare(strict_types=1);开启严格模式。开启后,如果规定了类型,就必须满足对应的类型,否则报错。

常量数组

常量数组是不可以数组

namespace 批量导入

throwable 接口

Closure::call()

intdiv

1
intdiv(10,3);  // 10除3,取整数位

list 方括号写法

1
2
3
$arr = [1,2,3];
list($a,$b,$c) = $arr;
var_dump($a,$b,$c);

已上写法等价于:

1
2
3
$arr = [1,2,3];
[$a,$b,$c] = $arr;
var_dump($a,$b,$c);

抽象语法树(AST)

PHP7 中 ?? 和 ?: 的区别

$a ?? $b 相当于

1
isset($a) ? $a : $b

$a ?: $b 相当于

1
$a ? $a : $b

另外

and 相当于 &&

or 相当于 ||

但是优先级不一样, and 和 or 的优先级非常的低, 比 = 还低

中文网站:https://pkg.phpcomposer.com/

composer.json 中记录开发者主动安装的包, composer.lock 中除了记录 composer.json 中记录的包之外, 还记录 composer.json 中记录的包的依赖和依赖的依赖等等; 所以说 composer.lock 中记录的是最全的

windows 下安装 composer

  1. 找到并进入 PHP 的安装目录

  2. 将 composer.phar 复制到 PHP 的安装目录下面,也就是和 php.exe 在同一级目录。

  3. 在 PHP 安装目录下新建一个 composer.bat 文件,并将下列代码保存到此文件中。

    • @php “%~dp0composer.phar” %*

命令

composer install

如果本地存在 composer.lock 文件, 则检查更新 composer.lock 中的包, 如果本地不存在 composer.json, 则检查更新 composer.json

composer update

检查更新 composer.json, 注意这个是检查更新所有包, 如果只想更新指定的包, 可以命令后面指定包, 例如:

1
composer update topthink/framework

不是很推荐这个方式, 推荐使用 require

composer require

安装装或者更新包

composer dumpautoload

更新 autoload 设置, 执行这条命令后, 可以查看 vender/composer/autoload_psr4.php 查看更改,然后只要保证引入 vender/autoload.php 即可。

对于某些包,作者可能没有发布证书版本,如果这个时候你需要使用这个包,例如

1
composer require easyswoole/oss

会报错

我们可以指定安装开发版本

1
composer require easyswoole/oss:dev-master

推荐使用阿里云的源

1
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

python 中的协程

协程,又称微线程。

1
2
3
4
5
6
7
8
9
def Fn():
i = 0
while i < 5:
yield i
i ++

f = Fn()
next(f)
next(f)

协程就是通过 yield 关键字返回数据,然后中断函数执行,处理返回的数据,之后使用 next()激活函数,继续执行。

函数每次执行都需要激活,第一次也不例外,除了可以使用 next()激活,还可以使用 send()激活,send()激活的好处就是可以传递参数。next() = f.send(None)

封装

greenlet:为了更好使用协程来完成多任务,python 中的 greenlet 模块对其封装,从而使得切换任务变的更加简单。

gevent:greenlet 已经实现了协程,但是这个还的人工切换,gevent 能够自动切换任务,其原理是当一个 greenlet 遇到 IO(指的是 input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的 greenlet,等到 IO 操作完成,再在适当的时候切换回来继续执行。

不过这两个封装协程的扩展我是没太看懂。

php 中的协程

和 python 中类似,只要函数中包含 yield 关键字,那么它就是协程。

与 python 的协程不同的是第一次激活协程,python 是统一使用 next(), 而 php 是单独定义了一个方法 rewind(), 第一次运行协程一定要通过 rewind()方法, 如果第一次使用 next()、send()、current(), 也会运行协程, 是因为他们已经隐式的运行了 rewind():

1
2
3
4
5
6
7
8
9
<?php
function gen()
{
yield 'foo';
yield 'bar';
}

$gen = gen();
$gen->next(); // 等价与 $gen->rewind(); $gen->next(); 同理: $gen->send('xxx'); 等价与 $gen->rewind(); $gen->send('xxx');

所以如果第一次就使用 next(), 得到的结果就是第二次 yield 的返回值了.

cURL 是一个利用 URL 语法规则来进行传输文件和数据的工具。PHP 也支持 cURL 库。

在 PHP 中建立 cURL 请求的基本步骤

  1. 初始化 curl_init()
  2. 设置变量 curl_setopt()
  3. 执行并获取结果 curl_exec()
  4. 释放 cURL 句柄 curl_close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param $url
* @param int $type 0 get 1 post
* @param array $data
*/
function doCurl($url, $type=0, $data=[]) {
$ch = curl_init(); // 初始化
// 设置选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER,0);
if($type == 1) {
// post
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
//执行并获取内容
$output = curl_exec($ch);
// 释放curl句柄
curl_close($ch);
return $output;
}

上述四个步骤中, 其中第二步最为关键, 可以设置一些高级选项 :

  • 设置请求地址:
    curl_setopt($ch, CURLOPT_URL, $url);

  • 由于 https 协议会对数据进行加密处理, 需要服务器配置证书. 设置 curl 不要对证书进行验证:
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CHRLOPT_SSL_VERIFYHOST, false);

  • curl_exec($ch)直接输出对应URL返回的数据, 字符串格式, 如果发生错误, 则返回false. (判断是否false使用===全等), 所以我们要设置获取的信息以文件流的形式返回, 而不是直接输出浏览器:
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)

  • 以 POST 的方式发起请求
    curl_setopt($ch, CURLOPT_POST, 1);

  • 发送请求, POST 格式, $data就是发送的数据
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

curl 通过 CURLFile 类实现文件数据的读取。注意: CURLFile 在 php5.5 版本以上使用

$file = new CURLFile(文件的绝对路径); 返回一个对象 :

1
object(CURLFile)#1 (3) { ["name"]=> string(15) "./黑天鹅.jpg" ["mime"]=> string(0) "" ["postname"]=> string(0) "" }

cURL 上传文件

PHP 使用 CURL 上传文件只需发送一个 POST 请求就可以了,在请求中设置某个字段为需要上传的文件全路径,并且以“@”开头,然后使用 CURL 把该变量以 POST 方式发送到服务器,在服务端即可以从超级全局变量$_FILES 中取到相应的上传文件信息。

如果使用了-F 参数,curl 会以 multipart/form-data 的方式发送 POST 请求。-F 以 key=value 的形式指定要上传的参数,如果是文件,则需要使用 key=@file 的形式

session 和 cookie 存在的目的是为了解决 http 协议的无状态

PHP 的 session 机制工作过程大致是这样的: 当客户端浏览器向服务器发起一个请求的时候,服务器会检查请求数据包头部中的“Cookie”字段是否包含名称为PHPSESSID的变量,

理解session和cookie1.png

这个名称可以通过 php.ini 中的 session.name 自定义,默认是 PHPSESSID,比如 https://nba.hupu.com/ 貌似就命名为_HUPUSSOID

理解session和cookie3.png

该变量的值即为 sessionid, 即:$_COOKIE[session.name]=sessionid。若不存在,PHP 会在 session.savepath 配置的路径目录下生成一个文件(session.save_path 也是 PHP 的配置文件 php.ini 中的一个配置项,配置服务器 session 文件放置的位置),同时生成一个 sessionid,这个文件的名称即为“sess”和 sessionid 拼凑而成。

理解session和cookie2.png然后在发送给浏览器的数据包头部中的 Set-Cookie 字段中指定 session 的名称和对应的 sessionid,浏览器则根据该字段的信息在内存中创建一个 Cookie,严格来说叫 SessionCookie。下一次浏览器再访问服务器的时候便会在数据包头部的 Cookie 字段中加入该 Cookie。若服务器发现浏览器请求包头部中“Cookie”字段包含了名为 session.name 的 sessionid,就会根据该 sessionid 到 session.savepath 指定的路径下找到名称为“sess”+sessionid 的文件,对文件进行读取或写入操作,开始了和客户端之间的会话。

由于 SessionCookie 是存在于浏览器内存中的,所以当浏览器关闭的时候,原来的 SessionCookie 也就消失了,下一次访问同一个服务器需要再次创建 session 文件和 Cookie,当然也有一些 cookie 是存储在客户端硬盘中的,即使关闭了浏览器,下一次打开浏览器访问相同网站的时候还是可以使用这个 cookie。当客户端的 cookie 过期之后,服务器中原来的 session 文件也就没有用处了。在 PHP 的配置文件 php.ini 中,使用配置项“session.gc_maxlifetime”来设置 session 文件的生存期,超过这个时间期限的文件数据都将被视为垃圾,并由垃圾回收程序处理。垃圾回收程序是在会话初始化时启动的,但并不是每一次都会启动,是有一定概率的,PHP 配置文件中使用“session.gc_probability”和“session.gc_divisor”这两个配置项来设置这个概率,计算公式为:session.gc_probability / session.gc_divisor。

以上讲述的是使用 Cookie 来存储 sessionid 的方法,这也是 PHP 默认的存储 sessionid 的方法。当客户端的 Cookie 出现问题的时候,如:用户设置浏览器禁用 cookie,session 就会受到影响了。不过我们可以通过其他方法来存储 sessionid,主要有以下三种方法:

  1. 设置文件 php.ini 中的 session.use_trans_sid = 1,可以让 PHP 自动跨页传递 sessionid(PHP 会自动把 sessionid 附着在 URL 末尾)。

  2. 手动通过 URL 传值,或通过隐藏表单字段传递 sessionid。

  3. 使用文件、数据库等形式保存 sessionid,在跨页过程中手动调用。

    注意,方法一配置 session.use_trans_sid=1 的同时也要配置 session.use_only_cookies=0,否则客户端禁用了 Cookie 之后即使置 session.use_trans_sid=1,session 还是无法使用。

在方法 2 中,第一次调用 session_start()函数时,PHP 会创建一个 session 文件并产生一个 sessionid,可以通过 session_id()函数获得该 sessionid,将 sessionid 拼接在 url 后面通过 get 方法传递(这种方法是存在很大的安全问题的),或者将 sessionid 作为表单的隐藏字段使用 post 方法传递,然后在下一个页面中可以把传过来的 sessionid 作为参数传递给 PHP 的 session_id()函数,指定要读写的 session 文件,然后调用 session_start()函数启动 session 会话,接下来就可以对 session 文件进行读取操作了。

必须说明的是,PHP 的 Session 机制并不是绝对安全的,攻击者如果能通过一定手段劫持、或者猜测出某个合法用户的 sessionid,那么属于这个用户的 session 数据都将暴露。所以,开发者应该设计一些方案来防御攻击者的攻击,提高 Session 机制的安全性,下面提供了一种利用请求数据包头部中的一些字段信息来加强 Session 安全性的方案思路。

将头部字段“User-Agent”和 sessionid 组合起来加密生成一个 token,并且让客户端在后续的请求中携带这个 token。为了更加保险,可使用两种不同的数据传递方式来 分别传递 sessionid 和 token,例如,通过 cookie 传递 sessionid,通过 get 方式传递 token。

二者都表明了本文件的绝对路径,区别在于:

$_SERVER[‘SCRIPT_FILENAME’]指向当前执行脚本的绝对路径;

__FILE__指向当前文件的绝对路径;也就是写在哪个文件里就是哪里。

示例:a.php 中引用了 b.php 文件:

b.php 文件中的$_SERVER[‘SCRIPT_FILENAME’]指向 a.php,__FILE__就还是指向 b.php

总结: 所有的情况都用$_SERVER[‘SCRIPT_FILENAME’]就行了

三个类: Register、Gateway、BusinessWorker

工作原理:

  1. Register、Gateway、BusinessWorker 三个进程启动
  2. Gateway、BusinessWorker 进程启动后向 Register 服务进程发起长链接注册自己
  3. Register 服务收到 Gateway 的注册后,把所有 Gateway 的通讯地址保存在内存中
  4. Register 服务收到 BusinessWorker 的注册后,把内存中所有的 Gateway 的通讯地址发给 BusinessWorker
  5. BusinessWorker 进程得到所有的 Gateway 内部通讯地址后尝试连接 Gateway
  6. 如果运行过程中有新的 Gateway 注册到 Register(一般是分布式部署加机器),则将新的 Gateway 内部通讯地址列表广播给所有 BusinessWoker,BusinessWorker 不在连接下线的 Gateway
  7. 如果有 Gateway 下线,则 Register 服务会收到通知,会将对应的内部通讯地址删除,然后广播新的内部通讯地址列表给所有 BusinessWorker,BusinessWorker 不再连接下线的 Gateway
  8. 至此 Gateway 与 BusinessWorker 通过 Register 建立起长链接
  9. 客户端的事件及数据全部都由 Gateway 转发给 BusinessWorker 处理,BusinessWorker 默认调用 Events.php 中的 onConnect、onMessage、onClose 处理业务逻辑
  10. BusinessWorker 的业务逻辑全部咋 Events.php 中

注: int 是有符号、unsigned int 是无符号;

gdb 调试数组的时候, nTableMask = 4294967294, 因为 nTableMask 是 unsigned int 类型, 所以使用(int)4294967294 转成 int 后结果是 2;

那为什么是 2 呢?

因为 unsigned int 和 int 之间的转化是按照实际存储的二进制进行转换的, 转换的时候二进制值不变。
所以, 如果最高位为 0 的, unsigned int 和 int 转换后值不变; 如果最高位为 1, 那么当有符号数转为无符号数时,最高位表示数值,而不是符号。反之最高位表示符号,而不是值。
重新根据补码规则运算。如果要找规律,如果 int 为 N 位(根据平台,N 为 16 或者 32,一般为 32)。
有符号转无符号,原本有符号值为 k,那么转换后的值为 2^N+k。
无符号转有符号,原本无符号值为 k,转换后的值为 k-2^N。
2^N 表示 2 的 N 次幂。

正数一般自动转换的数不变,但如果是负数可能会转换成一个很大的正数。