【面试】网易游戏面试题目整理及答案(1)
网易游戏面试题目整理及题目Python部分Java部分数据库部分算法部分计算机网络操作系统Linux部分框架其他参考资料Python部分迭代器生成器,生成器是如何实现迭代的?答:迭代器iterator:迭代器就是实现了迭代方式的容器,iterable对象一般只能按默认的正序方式进行迭代,你可以通过为其添加__next__()/next()方法来定制不同的迭代方式,这样通过next方法封装起来的迭代
Python部分
-
迭代器生成器,生成器是如何实现迭代的?
答:迭代器iterator:迭代器就是实现了迭代方式的容器,iterable对象一般只能按默认的正序方式进行迭代,你可以通过为其添加__next__()/next()方法来定制不同的迭代方式,这样通过next方法封装起来的迭代对象生成器就被称作迭代器。与iterable相比iterator多了一个next()方法,next()方法用于定制for循环时具体的返回值及返回顺序以及处理StopIteration异常等。
iterator必定是iterable的,因此其必然含有__iter__方法,此方法保证了iterator是可以迭代的,个人认为可以将__iter__()方法看做迭代器的入口,此入口告诉python对象是可for循环的,当你还为class定义了__next__方法时python的for循环会直接调用__next__()方法进行迭代,因此对于实现了__next__方法的迭代器来讲__iter__方法是一个不可或缺的鸡肋方法,不可或缺是因为他是可迭代的标识,鸡肋是因为他不会实质性的起作用,虽然他是迭代器的入口但却不是迭代的起始点,也因此iterator的__iter__方法可以随意返回self或者self.属性或者None。使用isinstance(实体名,Iterator)可判断是否为迭代器。
生成器generator:generator对象是一种特殊的iterator函数,它会在执行过程中保存执行的上下文环境,并在下次循环中从yield语句后继续执行,生成器的标志就是yield关键字。
generator不需要抛出StopIteration异常(可以看做yield已经在内部实现了StopIteration跳出循环),函数并没有将序列项一次生成,所以generator在实现上可以有无穷个元素,而不需要无穷的存储空间,这在内存优化方面很有用处。使用isinstance(实体名,Generator)可判断是否为生成器。生成器的创建办法有两种:①通过函数创建,称作generator function;②通过推导式创建,例如g=(x*2 for x in range(10)),称作generator expression.
补充知识:__iter__()和iter()
Python有一个built-in函数iter()用来从序列对象,如String, list, tuple中生成迭代器。
__iter__()方法是python的魔法方法,如果对象是iterator,那么for循环时python会直接调用__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
在python中,如果对象不含__next__方法,但是__iter__只返回self的话,尝试对对象使用for循环就会报“TypeError: iter() returned non-iterator of type [类名]”,针对这种错误要么加一个__next__()方法,要么__iter__()返回一个包含__next__()方法的迭代器对象。那么按理来说string、list这些iterable对象也是只含有__iter__不含__next__的,为何他们就可以for循环呢,这点在本文中的示例三中有演示,如果__iter__魔法方法调用了iter()方法,返回了一个迭代器对象,那么即便其不包含__next__也可以进行迭代
Python中使用迭代器生成器的库为collections,导入方式为:
from collections import Iterator,Iterable,Generator
补充知识:可迭代对象iterable
可迭代的对象的意思是就是说这个实体是可迭代的,例如字符、列表、元组、字典、迭代器等等,可以用for … in进行循环,可以使用for循环迭代的标志是内部实现了__iter__方法。
可迭代对象仅含有__iter__的内部方法,你可以通过封装next()方法(python3中为__next__())来将其做成一个迭代器,以生成器(generator,特殊的函数类型的迭代器)为例,你可以通过yield关键字来做一个迭代器,只不过名字被叫做generator,yield可以看做就是为对象添加了__iter__方法和指示下一次迭代的next()/next()方法。
使用isinstance(实体名,Iterable)可判断是否为可迭代对象。
示例代码如下:
问题1:既然可迭代对象也可以使用for循环遍历,为何还要使用迭代器呢?
答:一般情况下不需要将可迭代对象封装为迭代器。但是想象一种需要重复迭代的场景,在一个class中我们需要对输入数组进行正序、反序、正序step=1、正序step=2等等的多种重复遍历,那么我们完全可以针对每一种遍历方式写一个迭代容器,这样就不用每次需要遍历时都费劲心思的写一堆对应的for循环代码,只要调用相应名称的迭代器就能做到,针对每一种迭代器我们还可以加上类型判断及相应的处理,这使得我们可不必关注底层的迭代代码实现。
从这种角度来看,可以将迭代器看作可迭代对象的函数化,有一个非常流行的迭代器库itertools,其实就是如上所述,它为很多可迭代对象提前定义好了一些列的常见迭代对象,并封装为了迭代器,这样就可以很方便地直接调用此模块实现迭代。
此外,itertools还可以节省内存。
问题2:生成器(generator)如何节约内存?
答:generator的标志性关键字yield其实可以看作return,以及上文的generator_list()方法为例,generator_list(a)就是一个生成器。
生成器最大的好处在于:generator_list(a)并不会真正执行函数的代码,只有在被循环时才会去获取值,且每次循环都return一个值(即yield一个值),在处理完毕后下次循环时依然使用相同的内存(假设处理单元大小一样)来获取值并处理,这样在一次for循环中函数好像终端(yield)了无数次,每次都用相同大小的内存来存储被迭代的值。
yield与return的最大区别就是yield并不意味着函数的终止,而是意味着函数的一次中断,在未被迭代完毕之前yield意味着先返回一次迭代值并继续下一次函数的执行(起始位置是上一次yield语句结束),而return则基本意味着一个函数的彻底终止并返回一个全量的返回值。
因此,generator是为了节省内存,而且将函数写为一个生成器可以使函数变得可迭代,如果想遍历函数的返回值,不用再单独定义一个可迭代变量存储函数的返回值们,而是直接迭代生成器函数即可(除非函数本身返回一个全量的可迭代对象)。
同理,iterator的__iter__()方法只是一个可迭代的入口,每次调用__next__()时返回一个迭代值,同样以O(1)的空间复杂度完成了迭代。
问题3:iterator与generator的异同?
答:generator是iterator的一个子集,iterator也有节约内存的功能,generator也可以定制不同的迭代方式。官方解释为:Python’s generators provide a convenient way to implement the iterator protocol。其实说白了就是generator更加轻量,日常编程里可能常常使用它,而iterator一般使用系统提供的工具就可以了,极少会自己写一个。
示例代码2:
可以看到,只要调用相应名字的迭代器对象就可以直接进行for循环了,这种写法相比起每次都需要在for循环中重复一遍算法逻辑要简单,除此之外还可以为不同输入类型定制相同的迭代方式,这样就无需关注内部实现了。这就是迭代器的作用,为不同类型的输入封装相同的迭代功能,从而实现代码简化。Python中有一个非常有用的itertools module,提供了大量不同的迭代器,只要直接调用就可以实现对序列的各种操作,可以通过这个库加深对于迭代器的理解。
示例代码3:
因此这里可以对iterable对象做一个有别于开头的解释,非iterator的iterable对象其标志不仅仅是含有__iter__方法,它的__iter__方法还返回了一个迭代器对象(例如示例3中的iter(self.list)),但因为其本身不含__next__方法所以其可for循环但并不是iterator。
日常工作中使用generator处理大文件是比较常见的场景,因为可以不用一次性读取整个文件,使用generator也可以极大的减少代码量。 -
list实现
答:List的代码示例:
在Python中List是用下边的C语言的结构来表示的,其中的ob_item是用来保存元素的指针数组,allocated是ob_item预先分配的内存总容量:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
首先,List的初始化,即当初始化一个空的List的时候发生了什么L=[]
arguments: size of the list = 0
returns: list object = []
PyListNew:
nbytes = size * size of global Python object = 0
allocate new list object
allocate list of pointers (ob_item) of size nbytes = 0
clear ob_item
set list's allocated var to 0 = 0 slots
return list object
这里比较重要的是知道了list申请内存空间的大小(用allocated代替)和list实际存储元素所占空间的大小(ob_size)之间的关系,ob_size的大小和len(L)是一样的,而allocated的大小是在内存中已经申请空间大小。通常会看到allocated的值要比ob_size的值要大。这是为了避免每次有新元素加入list时都要调用realloc进行内存分配。
然后是当在list中追加一个整数:L.append(1),的时候会发生什么?调用了内部的C函数app1():
arguments: list object, new element
returns: 0 if OK, -1 if not
app1:
n = size of list
call list_resize() to resize the list to size n+1 = 0 + 1 = 1
list[n] = list[0] = new element
return 0
看下list_resize(),list_resize()会申请多余的空间以避免调用多次list_resize()函数,list增长的模型是:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …
arguments: list object, new size
returns: 0 if OK, -1 if not
list_resize:
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) = 3
new_allocated += newsize = 3 + 1 = 4
resize ob_item (list of pointers) to size new_allocated
return 0
开辟了四个内存空间来存放list中的元素,存放在第一个元素是1.如下图所示:
继续加入一个元素:L.append(2)。调用list_resize,同时n+1=2。但是因为allocated(已经申请的空间大小)是4。所以没有必要去申请新的内存空间。相同的事情发生在再次在list中添加两个元素的时候: L.append(3),L.append(4)。如下图所示。
然后在列表的第一个位置插入一个整数5,即L.insert(1,5),发现内部调用了ins1():
arguments: list object, where, new element
returns: 0 if OK, -1 if not
ins1:
resize list to size n+1 = 5 -> 4 more slots will be allocated
starting at the last element up to the offset where, right shift each element
set new element at offset where
return 0
虚线框表示已经申请但是没有使用的内存。申请了8个内存空间但是list实际用来存储元素只使用了其中5个内存空间。insert的时间复杂度是O(n)
当弹出list中的最后一个元素,即L.pop()。调用listpop()。list_resize在函数listpop()内部被调用,如果这时ob_size(弹出元素后)小于allocated(已经申请的内存空间)的一半。这时申请的内存空间将会缩小。
arguments: list object
returns: element popped
listpop:
if list empty:
return null
resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage
set list object size to 4
return last element
Pop的时间复杂度是O(1)。
我们发现4号内存空间指向还指向那个数值(弹出去的那个数值),但是很重要的是ob_size现在却成了4。然后再弹出一个元素。在list_resize内部,size – 1 = 4 – 1 = 3 比allocated(已经申请的空间)的一半还要小。所以list的申请空间缩小到6个,list的实际使用空间现在是3个(根据(newsize >> 3) + (newsize < 9 ? 3 : 6) = 3)。可以发现(下图)3号和4号内存空间还存储着一些整数,但是list的实际使用(存储元素)空间却只有3个了。
最后看一下Python中list对象移除一个指定元素(即调用listremove())的过程:
arguments: list object, element to remove
returns none if OK, null if not
listremove:
loop through each list element:
if correct element:
slice list between element's slot and element's slot + 1
return none
return null
切开list和删除元素,调用了list_ass_slice()(在上文slice list between element’s slot and element’s slot + 1被调用),来看下list_ass_slice()是如何工作的。在这里,低位为1 高位为2(传入的参数),我们移除在1号内存空间存储的数据5。
arguments: list object, low offset, high offset
returns: 0 if OK
list_ass_slice:
copy integer 5 to recycle list to dereference it
shift elements from slot 2 to slot 1
resize list to 5 slots
return 0
其中Remove的时间复杂度为O(n)
如何确定Python的list调整后的空间大小:
调整后大小 (new_allocated) = 新元素数量 (newsize) + 预留空间 (new_allocated)
调整后的空间肯定能存储 newsize 个元素。要关注的是预留空间的增长状况。
将预留算法改成 Python 版就更清楚了:(newsize // 8) + (newsize < 9 and 3 or 6)。
当 newsize >= allocated,自然按照这个新的长度 "扩容" 内存。
而如果 newsize < allocated,且利用率低于一半呢?
allocated newsize new_size + new_allocated
10 4 4 + 3
20 9 9 + 7
很显然,这个新长度小于原来的已分配空间长度,自然会导致 realloc 收缩内存。(不容易啊)
引自《深入Python编程》
- import一个包时过程是怎么样的?
答:简单地说,模块就是一个保存了Python代码的文件。模块能定义函数,类和变量,模块里也能包含可执行的代码。使用模块可以更加有逻辑地组织Python代码段,使代码更好用,更易懂。为了组织好模块,会将多个模块分为包。Python处理包也是相当方便的,简单来说,包就是文件夹,但该文件夹下必须存在__init__.py文件。最简单的情况下,init.py 为空文件即可,当然它也可以执行包的一些初始化代码。
注意:每个py文件被称之为模块,每个具有__init__.py文件的目录被称为包。只要模块或者包所在的目录在sys.path中,就可以使用import 模块或import包来使用。
在使用一个模块中的函数或类之前,首先要导入该模块。模块的导入使用import语句,格式如下:
import module_name
调用模块的函数或类时,需要以模块名作为前缀,如下:
module_name.func()
如果不想在程序中使用模块名前缀符,可以使用from import语句从模块导入函数,如下:
from module_name import func
func()
上面的例子全部基于当前程序能够找到 module_name 这个模块的假设,下面深入探究模块导入的机制。
Python模块导入机制
Python 提供了 import 语句来实现类库的引用,当我们执行一行 from package import module as mymodule 命令时,Python解释器会查找package 这个包的module模块,并将该模块作为 mymodule 引入到当前的工作空间。所以import语句主要是做了二件事:
①查找相应的module
②加载module到local namespace
在import的第一个阶段,主要是完成了查找要引入模块的功能。查找时首先检查 sys.modules (保存了之前import的类库的缓存),如果module没有被找到,则按照下面的搜索路径查找模块:
①.py 所在文件的目录
②PYTHONPATH 中的目录
③python安装目录,UNIX下,默认路径一般为/usr/local/lib/python/
④3.x 中.pth 文件内容
其大致过程可以简化为:
def import(module_name):
if module_name in sys.modules:
return sys.modules[module_name]
else:
module_path = find(module_name)
if module_path:
module = load(module_path)
sys.modules[module_name] = module
return module
else:
raise ImportError
模块导入错误
在导入模块方面,可能会出现下面的情况:
①循环导入(circular imports):如果创建两个模块,二者相互导入对方,那么有可能会出现循环导入。注意不是所有的互相导入都会引发 AttributeError。
②覆盖导入(Shadowed imports):当创建的模块与标准库中的模块同名时,如果导入这个模块,就会出现覆盖导入。
Tips
1)import 会生成 .pyc 文件,.pyc 文件的执行速度不比 .py 快,但是加载速度更快
2)重复 import 只会执行第一次 import
3)如果在 ipython 中 import 的模块发生改动,需要通过 reload 函数重新加载
4)import * 会导入除了以 _ 开头的所有变量,但是如果定义了 all,那么会导入 all 中列出的东西
- 装饰器实现
答:装饰器的功能:当需要对一段写好的代码添加一段新的需求的时候的时候我们就可以用装饰器实现。
def set_func(func):
def call_funct():
print("---这是权限验证1---")
print("---这是权限验证2——————")
func()
return call_funct
@set_func
def test_1():
print("----test1----")
test_1()
对test_1函数添加验证1和验证2的功能,需要设计一个闭包,闭包的外部参数传递的是函数的引用,在内部函数里面添加需要添加的功能。比如以上这段代码,我们在test_1函数前面添加上@set_func这个装饰器,在调用test_1函数的时候,我们就会按照call_funct函数里面的顺序执行。
装饰器的原理其实就是函数引用的传递,在闭包外部传递函数的引用,内部函数执行完“这是权限验证1”和“这是权限验证2”之后,就会把外部函数传递的函数引用参数拿过来执行
1)对有参数无返回值的函数进行修饰的时候,修饰的函数有几个参数,闭包的内部函数就需要有几个参数,函数地址传递给外部函数,函数实参传递给内部参数。
2)不定长参数的函数装饰器,可以直接在闭包的内部函数里写不定长参数,当然也可以按照调用函数的形参格式进行传递。
3)对带有返回值的函数进行装饰,通用装饰器
4)多个装饰器对同一个函数进行修饰:程序是可以执行的,但是执行顺序,简单来说,开启装饰器的顺序是从下到上,执行内部函数的时候,由上到下。
5)类的装饰器
6)装饰器带参数,需要在闭包外层再定义一个函数,这个函数用来接受装饰器的参数。
- 菱形继承
答:继承是面向对象编程的一个重要的方式,通过继承,子类就可以扩展父类的功能。在python中一个类能继承自不止一个父类,这叫做python的多重继承(Multiple Inheritance )。语法如下:
class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...):
pass
在多层继承和多继承同时使用的情况下,就会出现复杂的继承关系,多重多继承。其中,就会出现菱形继承。如下图所示。
在这种结构中,在调用顺序上就出现了疑惑,调用顺序究竟是以下哪一种顺序呢
①D->B->A->C(深度优先)
②D->B->C->A(广度优先)
示例代码如下:
从输出结果中看,调用顺序为:D->B->A->C->A。可以看到,B、C共同继承于A,A被调用了两次。A没必要重复调用两次。
其实,上面问题的根源都跟MRO有关,MRO(Method Resolution Order)也叫方法解析顺序,主要用于在多重继承时判断调的属性来自于哪个类,其使用了一种叫做C3的算法,其基本思想时在避免同一类被调用多次的前提下,使用广度优先和从左到右的原则去寻找需要的属性和方法。
那么如何避免顶层父类中的某个方法被多次调用呢,此时就需要super()来发挥作用了,super本质上是一个类,内部记录着MRO信息,由于C3算法确保同一个类只会被搜寻一次,这样就避免了顶层父类中的方法被多次执行了,上面代码可以改为:
可以看出,此时的调用顺序是D->B->C->A。即采用是广度优先的遍历方式。
补充内容:Python类分为两种,一种叫经典类,一种叫新式类。都支持多继承,但继承顺序不同。
1)新式类:从object继承来的类。(如:class A(object)),采用广度优先搜索的方式继承(即先水平搜索,再向上搜索)。
2)经典类:不从object继承来的类。(如:class A()),采用深度优先搜索的方式继承(即先深入继承树的左侧,再返回,再找右侧)。
- 内存垃圾回收:分代回收细节
答:Python一种面向对象的脚本语言,对象是Python中非常重要的一个概念。在Python中数字是对象,字符串是对象,任何事物都是对象,而它们的核心就是一个结构体–PyObject。
typedef struct_object{
int ob_refcnt;
struct_typeobject *ob_type;
}PyObject;
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。
垃圾回收机制作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来给其他对象使用。在Python中,垃圾回收机制主要是以引用计数为主要手段,以标记清除和分代回收机制作为辅助手段实现的。
1)引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少,当引用计数为0时,该对象生命就结束了。当发生以下四种情况的时候,该对象的引用计数+1
①对象被创建
这里实际上123这个对象并没有在内存中新建,因为在Python启动解释器的时候会创建一个小整数池,在-5~256之间的整数对象会被自动加载到内存中等待调用。因此a=123是对123这个整数对象增加了一次引用。而456是不在整数池里的,需要创建对象,那么最后的引用次数是2呢?因为sys.getrefcount(b)也是一次引用。
②对象被引用
每一次赋值操作都会增加数据的引用次数,要记住引用的变量a、b、c指向的是数据456,而不是变量本身。
③对象被作为参数,传到函数中
这里可以很明显看到在被传递到函数中后,引用计数增加了1。
④对象作为一个元素,存储在容器中 List={a,”a”,”b”,2}
这里我们在创建对象之后,把a分别添加到了一个列表和一个元组中,引用计数都增加了。
虽然引用计数必须在每次分配合释放内存的时候加入管理引用计数的操作,然而与其他垃圾回收技术相比,引用计数有一个最大的优点,那就是“实时性”,如果这个对象没有引用,内存就直接释放了,而其他垃圾回收技术必须在某种特殊条件下才能进行无效内存的回收。但是引用计数带来的维护引用计数的额外操作和Python中进行的内存分配和释放,引用的赋值次数成正比的。除此之外,引用计数机制的还有一个最大的软肋–无法解决循环引用带来的问题。循环引用可以使一种引用对象的引用计数不为0,然而这些对象实际上并没有被任何外部对象所引用,它们之间只是相互引用,这意味着这组对象所占用的内存空间是应该被回收的,但是由于循环引用导致的引用计数不为0,所以这组对象所占用的内存空间永远不会被释放。如下,list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。例如:
与上述情况相对应,当发生以下四种情况时,该对象的引用计数器-1
①当该对象的别名被显式销毁时:del a
②当该对象的引别名被赋予新的对象:a=26
③一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会-1(但是全局变量不会)
④将该元素从容器中删除时,或者容器被销毁时。
2)标记清除
标记清除(Mark—Sweep)算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。如下图所示:
在上图中,可以从程序变量直接访问块1,并且可以间接访问块2和3。程序无法访问块4和5。第一步将标记块1,并记住块2和3以供稍后处理。第二步将标记块2,第三步将标记块3,但不记得块2,因为它已被标记。扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5。
标记清除算法作为Python的辅助垃圾收集技术,主要处理的是一些容器对象,比如list、dict、tuple等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
3)分代回收。分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。
Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
-
WSGI
答:WSGI:Web Server Gateway Interface,WSGI不是服务器、python模块、框架、API或者任何软件,只是一种规范,描述web server如何与web application通信的规范。server和application的规范在PEP 3333中有具体描述。要实现WSGI协议,必须同时实现web server和web application,当前运行在WSGI协议之上的web框架有Torando,Flask,Diango。
WSGI协议主要包括server和application两部分:
1)Web server/gateway: 即 HTTP Server,处理 HTTP 协议,接受用户 HTTP 请求和提供并发,调用 web application 处理业务逻辑。通常采用 C/C++ 编写,代表:apache, nginx 和 IIS。WSGI server负责从客户端接收请求,将request转发给application,将application返回的response返回给客户端;
2)Python Web application/framework: WSGI application接收由server转发的request,处理请求,并将处理结果返回给server。application中可以包括多个栈式的中间件(middlewares),这些中间件需要同时实现server与application,因此可以在WSGI服务器与WSGI应用之间起调节作用:对服务器来说,中间件扮演应用程序,对应用程序来说,中间件扮演服务器。
WSGI协议其实是定义了一种server与application解耦的规范,即可以有多个实现WSGI server的服务器,也可以有多个实现WSGI application的框架,那么就可以选择任意的server和application组合实现自己的web应用。例如uWSGI和Gunicorn都是实现了WSGI server协议的服务器,Django,Flask是实现了WSGI application协议的web框架,可以根据项目实际情况搭配使用。
补充知识:uwsgi和uWSGI
1)uwsgi:与WSGI一样是一种通信协议,是uWSGI服务器的独占协议,用于定义传输信息的类型(type of information),每一个uwsgi packet前4byte为传输信息类型的描述,与WSGI协议是两种东西,据说该协议是fcgi协议的10倍快。
2)uWSGI:是一个web服务器,实现了WSGI协议、uwsgi协议、http协议等。 -
uWSGI进程模型
答:暂无 -
比较c语言和Python语言中的异步
答:同步:就是串行执行(按顺序,一个等一个),因为它就是函数的调用原理,栈机制的先进后出决定,速度慢,但是节约资源;异步:就是并行执行(没有顺序,同时做),它采用了开启多线程的方式实现同时做多件事情,速度快,但是会加大资源的开销;
在Python语言中,异步编程,就是以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序。(无序与有序的区别)同步/异步、阻塞/非阻塞并非水火不容,要看讨论的程序所处的封装级别。例如购物程序在处理多个用户的浏览请求可以是异步的,而更新库存时必须是同步的。在并发执行的异步模型中,许多任务被穿插在同一时间线上,所有的任务都由一个控制流执行(单一线程)。任务的执行可能被暂停或恢复,中间的这段时间线程将会去执行其他任务。
Python3的concurrent.future模块具有线程池和进程池, 管理并行编程任务, 处理非确定性的执行流程, 进程/线程同步等功能,此模块由以下部分组成:
①concurrent.futures.Executor: 这是一个虚拟基类,提供了异步执行的方法。
②submit(function, argument): 调度函数(可调用的对象)的执行,将 argument 作为参数传入。
③map(function, argument): 将 argument 作为参数执行函数,以 异步 的方式。
④shutdown(Wait=True): 发出让执行者释放所有资源的信号。
⑤concurrent.futures.Future: 其中包括函数的异步执行。Future对象是submit任务(即带有参数的functions)到executor的实例。
线程池或者进程池是用于在程序中优化和简化进程线程的使用。 通过池, 可以提交任务给executor。 池由两部分组成, 一部分是内部的队列, 存放着待执行的任务;另一部分是一系列的进程和线程, 用于执行这些任务。池的概念主要目的是为了重用: 让线程或者进程在生命周期内可以多次使用。 它减少了创建线程和进程的开销, 提高了程序的性能。 重用不是必须的规则, 但是它是程序员在应用中使用池的主要原因。
使用Asyncion管理时间循环:Python的Asyncion模块提供了管理事件, 协程, 任务和线程的方法, 以及编写并发代码原语。 此模块的组件和概念包括:
①事件循环: 在Asyncio模块中, 每一个进程都是一个事件循环。
②协程 :这是子程序的泛化概念。 协程可以在执行期间暂停。 这样就可以等待外部的处理 例如(IO)完成之后, 从之前的停止位置恢复执行。
③Futures : 定义了Future对象, 和concurrent.futures模块一样, 表示尚未完成的计算。
④Tasks:这是Asyncio 的子类, 用于封装和管理并行模式下的协程。
异步编程的上下文中, 事件是无比的重要, 因为事件的本质就是异步。
使用asyncio管理协程:当一个程序变得很大的时候而且复杂时候, 将其划分为子程序, 每一部分实现特定的任务是个不错的方案。 子程序不能单独执行, 只能在主程序的请求下执行,主程序负责协调各个主程序。 协程就是子程序的泛化。 和子程序一样的事, 协程只负责计算任务的一步, 和子程序不一样的是, 协程没有主程序来调度, 这是因为协程是通过管道连接在一起的, 没有监视函数负责调度它们。 在协程中执行点可以被挂起, 可以从之前被挂起的点恢复执行。通过协程池可以插入到计算中: 运行第一个任务, 直到它返回 yield 执行权, 然后运行下一个, 这样顺着执行下去。这种插入的控制组件就是事件循环。 它持续追踪所有协程并执行他们。协程的其他重要属性
1 协程可以有多个入口, 并可以yield多次
2 协程可以将执行权交给其他的协程
yield 表示协程暂停, 并且将执行权交给其他的协程, 因为协程可以将值与控制权一起传递给另一个协程, 所以yield一个值就表示将值传递给下一个执行的协程。 -
epoll原理
答:从网卡接收数据的流程讲起,串联起 CPU 中断、操作系统进程调度等知识;再一步步分析阻塞接收数据、Select 到 Epoll 的进化过程;最后探究 Epoll 的实现细节。
网卡接收数据的过程,通过硬件传输,网卡接收的数据存放到内存中,操作系统就可以去读取它们。
如何知道接收了数据?从 CPU 的角度来看数据接收。理解这个问题,要先了解一个概念:中断。计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时,它应立即去保存数据,保存数据的程序具有较高的优先级(电容可以保存少许电量,供 CPU 运行很短的一小段时间)。一般而言,由硬件产生的信号需要 CPU 立马做出回应,不然数据可能就丢失了,所以它的优先级很高。CPU 理应中断掉正在执行的程序,去做出响应;当 CPU 完成对硬件的响应后,再重新执行用户程序。它和函数调用差不多,只不过函数调用是事先定好位置,而中断的位置由“信号”决定。现在可以回答“如何知道接收了数据?”这个问题了:当网卡把数据写入到内存后,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
进程阻塞为什么不占用 CPU 资源?从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,Recv、Select 和 Epoll 都是阻塞方法。进程阻塞为什么不占用 CPU 资源?为简单起见,从普通的 Recv 接收开始分析。最基础的网络编程代码是先新建 Socket 对象,依次调用 Bind、Listen 与 Accept,最后调用 Recv 接收数据。Recv 是个阻塞方法,当程序运行到 Recv 时,它会一直等待,直到接收到数据才往下执行。那么阻塞的原理是什么?
工作队列
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得 CPU 使用权,正在执行代码的状态;等待状态是阻塞状态,比如程序运行到 Recv 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。下图的计算机中运行着 A、B 与 C 三个进程,其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
工作队列中有 A、B 和 C 三个进程
等待队列
当进程 A 执行到创建 Socket 的语句时,操作系统会创建一个由文件系统管理的 Socket 对象(如下图)。
创建 Socket
这个 Socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该 Socket 事件的进程。
当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中(如下图)。 Socket 的等待队列
由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。
注:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。
唤醒进程
当 Socket 接收到数据后,操作系统将该 Socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。同时由于 Socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。
内核接收网络数据全过程这一步,贯穿网卡、中断与进程调度的知识,叙述阻塞 Recv 下,内核接收数据的全过程。
如上图所示,进程在 Recv 阻塞期间:
①计算机收到了对端传送的数据
②数据经由网卡传送到内存
③然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序
此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中。
唤醒进程的过程如下图所示:
以上是内核接收数据全过程,这里我们可能会思考两个问题:
①操作系统如何知道网络数据对应于哪个 Socket?
②如何同时监视多个 Socket 的数据?
第一个问题:因为一个 Socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的 Socket。当然,为了提高处理速度,操作系统会维护端口号到 Socket 的索引结构,以快速读取。
第二个问题是多路复用的重中之重,也就是select和epoll的原理。
同时监视多个 Socket 的简单方法
服务端需要管理多个客户端连接,而 Recv 只能监视单个 Socket,这种矛盾下,人们开始寻找监视多个 Socket 的方法。Epoll 的要义就是高效地监视多个 Socket。
从历史发展角度看,必然先出现一种不太高效的方法,人们再加以改进,正如 Select 之于 Epoll。先理解不太高效的 Select,才能够更好地理解 Epoll 的本质。
假如能够预先传入一个 Socket 列表,如果列表中的 Socket 都没有数据,挂起进程,直到有一个 Socket 收到数据,唤醒进程。这种方法很直接,也是 Select 的设计思想。为方便理解,我们先复习 Select 的用法。在下边的代码中,先准备一个数组 FDS,让 FDS 存放着所有需要监视的 Socket。然后调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程。用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理。
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int fds[] = 存放需要监听的socket
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}
Select 的流程 Select 的实现思路很直接,假如程序同时监视 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。当任何一个 Socket 收到数据后,中断程序将唤起进程。下图展示了 Sock2 接收到了数据的处理流程:
注:Recv 和 Select 的中断回调可以设置成不同的内容。
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面,如下图所示:
将进程 A 从所有等待队列中移除,再加入到工作队列里面
经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:
①每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
正是因为遍历操作开销大,出于效率的考量,才会规定 Select 的最大监视数量,默认只能监视 1024 个 Socket。
②进程被唤醒后,程序并不知道哪些 Socket 收到数据,还需要遍历一次。
那么,有没有减少遍历的方法?有没有保存就绪 Socket 的方法?这两个问题便是 Epoll 技术要解决的。
补充说明:本节只解释了 Select 的一种情形。**当程序调用 Select 时,内核会先遍历一遍 Socket,如果有一个以上的 Socket 接收缓冲区有数据,那么 Select 直接返回,不会阻塞。**这也是为什么 Select 的返回值有可能大于 1 的原因之一。如果没有 Socket 有数据,进程才会阻塞。
Epoll 的设计思路
Epoll 是在 Select 出现 N 多年后才被发明的,是 Select 和 Poll(Poll 和 Select 基本一样,有少量改进)的增强版本。Epoll 通过以下一些措施来改进效率:
措施一:功能分离
Select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。
相比 Select,Epoll 拆分了功能,如上图所示,每次调用 Select 都需要这两步操作,然而大多数应用场景中,需要监视的 Socket 相对固定,并不需要每次都修改。Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。
为方便理解后续的内容,我们先了解一下 Epoll 的用法。如下的代码中,先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据:
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
功能分离,使得 Epoll 有了优化的可能。
措施二:就绪列表
Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。
就绪列表示意图
如上图所示,计算机共有三个 Socket,收到数据的 Sock2 和 Sock3 被就绪列表 Rdlist 所引用。当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据。
Epoll 的原理与工作流程
本节会以示例和图表来讲解 Epoll 的原理和工作流程。
1.)创建 Epoll 对象
如下图所示,当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 Epfd 所代表的对象)。
内核创建eventpoll对象
eventpoll 对象也是文件系统中的一员,和 Socket 一样,它也会有等待队列。创建一个代表该 Epoll 的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员。
维护监视列表
创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket。以添加 Socket 为例。
添加所要监听的 Socket
如上图,如果通过 epoll_ctl 添加 Sock1、Sock2 和 Sock3 的监视,内核会将 eventpoll 添加到这三个 Socket 的等待队列中。当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。
接收数据
当 Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用。
给就绪列表添加引用
如上图展示的是 Sock2 和 Sock3 收到数据后,中断程序让 Rdlist 引用这两个 Socket。eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。
阻塞和唤醒进程
假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。
epoll_wait 阻塞进程
如上图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)
Epoll 唤醒进程
也因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。
Epoll 的实现细节
至此,对 Epoll 的本质已经有一定的了解。但还需要知道 eventpoll 的数据结构是什么样子?此外,就绪队列应该使用什么数据结构?eventpoll 应使用什么数据结构来管理通过 epoll_ctl 添加或删除的 Socket?
Epoll 原理示意图,图片来源:《深入理解 Nginx:模块开发与架构解析(第二版)》,陶辉
如上图所示,eventpoll 包含了 Lock、MTX、WQ(等待队列)与 Rdlist 等成员,其中 Rdlist 和 RBR 是我们所关心的。
就绪列表的数据结构
就绪列表引用着就绪的 Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视 Socket,也可能随时删除。当删除时,若该 Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,**Epoll 使用双向链表来实现就绪队列(**对应上图的 Rdlist)。
索引结构
既然 Epoll 将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的 Socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是 O(log(N)),效率较好,Epoll 使用了红黑树作为索引结构(对应上图的 RBR)。
注:因为操作系统要兼顾多种功能,以及有更多需要保存的数据,Rdlist 并非直接引用 Socket,而是通过 Epitem 间接引用,红黑树的节点也是 Epitem 对象。同样,文件系统也并非直接引用着 Socket。为方便理解,本文中省略了一些间接结构。
总结
Epoll 在 Select 和 Poll 的基础上引入了 eventpoll 作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。这里也以表格形式简单对比一下 Select、Poll 与 Epoll。
-
Python里的eval
答:eval函数就是实现list、dict、tuple与str之间的转化;str函数把list,dict,tuple转为为字符串,eval函数的用法就是把字符串对象转换为能够具体的对象。 -
Tornado框架
答:Tornado框架简介如下:
1)tornado概述
Tornado就是我们在 FriendFeed 的 Web 服务器及其常用工具的开源版本。Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得益于其非阻塞的方式和对epoll的运用,Tornado 每秒可以处理数以千计的连接,因此 Tornado 是实时 Web 服务的一个理想框架。我们开发这个 Web 服务器的主要目的就是为了处理 FriendFeed 的实时功能 ——在 FriendFeed 的应用里每一个活动用户都会保持着一个服务器连接。
Tornado代表嵌入实时应用中最新一代的开发和执行环境。 Tornado 包含三个完整的部分:
①Tornado系列工具, 一套位于主机或目标机上强大的交互式开发工具和使用程序;
②VxWorks 系统, 目标板上高性能可扩展的实时操作系统;
③可选用的连接主机和目标机的通讯软件包,如以太网、串行线、在线仿真器或ROM仿真器。
2)tornado特点
Tornado的独特之处在于其所有开发工具能够使用在应用开发的任意阶段以及任何档次的硬件资源上。而且,完整集的Tornado工具可以使开发人员完全不用考虑与目标连接的策略或目标存储区大小。Tornado 结构的专门设计为开发人员和第三方工具厂商提供了一个开放环境。已有部分应用程序接口可以利用并附带参考书目,内容从开发环境接口到连接实现。Tornado包括强大的开发和调试工具,尤其适用于面对大量问题的嵌入式开发人员。这些工具包括C和C++源码级别的调试器,目标和工具管理,系统目标跟踪,内存使用分析和自动配置. 另外,所有工具能很方便地同时运行,很容易增加和交互式开发。
3)tornado模块索引
最重要的一个模块是web, 它就是包含了 Tornado 的大部分主要功能的 Web 框架。其它的模块都是工具性质的, 以便让 web 模块更加有用后面的 Tornado 攻略 详细讲解了 web 模块的使用方法。
主要模块:
- web - FriendFeed 使用的基础 Web 框架,包含了 Tornado 的大多数重要的功能
- escape - XHTML, JSON, URL 的编码/解码方法
- database - 对 MySQLdb 的简单封装,使其更容易使用
- template - 基于 Python 的 web 模板系统
- httpclient - 非阻塞式 HTTP 客户端,它被设计用来和 web 及 httpserver 协同工作
- auth - 第三方认证的实现(包括 Google OpenID/OAuth、Facebook Platform、Yahoo BBAuth、FriendFeed OpenID/OAuth、Twitter OAuth)
- locale - 针对本地化和翻译的支持
- options - 命令行和配置文件解析工具,针对服务器环境做了优化
底层模块 - httpserver - 服务于 web 模块的一个非常简单的 HTTP 服务器的实现
- iostream - 对非阻塞式的 socket 的简单封装,以方便常用读写操作
- ioloop - 核心的 I/O 循环
4)Tornado的优势 - 轻量级web框架
- 异步非阻塞IO处理方式
- 出色的抗负载能力
- 优异的处理性能,不依赖多进程/多线程,一定程度上解决C10K问题
- WSGI全栈替代产品,推荐同时使用其web框架和HTTP服务器
5)Tornado VS Django - Django:重量级web框架,功能大而全,注重高效开发
①内置管理后台
②内置封装完善的ORM操作
③session功能
④后台管理
⑤缺陷:高耦合 - Tornado:轻量级web框架,功能少而精,注重性能优越
①HTTP服务器
②异步编程
③WebSocket
④缺陷:入门门槛较高
-
PythonGIL锁
答:GIL锁,即全局解释锁(GIL),简单来说就是一个互斥体(或者说锁),这样的机制只允许一个线程来控制Python解释器。这就意味着在任何一个时间点只有一个线程处于执行状态。GIL对执行单线程任务的程序员们来说并没什么显著影响,但是它成为了计算密集型(CPU-bound)和多线程任务的性能瓶颈。
GIL解决了Python中的什么问题?
Python利用引用计数来进行内存管理,这就意味着在Python中创建的对象都有一个引用计数变量来追踪指向该对象的引用数量。当数量为0时,该对象占用的内存即被释放。
回到GIL本身,问题在于,这个引用计数变量需要在两个线程同时增加或减少时从竞争条件中得到保护。如果发生了这种情况,可能会导致泄露的内存永远不会被释放,抑或更严重的是当一个对象的引用仍然存在的情况下错误地释放内存。这可能会导致Python程序崩溃或带来各种诡异的bug。通过对跨线程分享的数据结构添加锁定以至于数据不会不一致地被修改,这样做可以很好的保证引用计数变量的安全。但是对每一个对象或者对象组添加锁意味着会存在多个锁这也就导致了另外一个问题——死锁(只有当存在多个锁时才会发生)。而另一个副作用是由于重复获取和释放锁而导致的性能下降。
GIL是解释器本身的一个单一锁,它增加的一条规则表明任何Python字节码的执行都需要获取解释锁。这有效地防止了死锁(因为只存在一个锁)并且不会带来太多的性能开销。但是这的确使每一个计算密集型任务变成了单线程。GIL虽然也被其他语言解释器使用(如Ruby),但是这不是解决这个问题的唯一办法。一些编程语言通过使用除引用计数以外的方法(如垃圾收集)来避免GIL对线程安全内存管理的请求。从另一方面来看,这也意味着这些语言通常需要添加其他性能提升功能(如JIT编译器)来弥补GIL单线程性能优势的损失。
为什么选取GIL作为解决方案?
那么为什么在Python中使用了这样一种看似绊脚石的技术呢?这是Python开发人员的一个错误决定么?正如Larry Hasting所说,GIL的设计决定是Python如今受到火热追捧的重要原因之一。当操作系统还没有线程的概念的时候Python就一直存在着。Python设计的初衷是易于使用以便更快捷地开发,这也使得越来越多的程序员开始使用Python。人们针对于C库中那些被Python所需的功能写了许多扩展,为了防止不一致变化,这些C扩展需要线程安全内存管理,而这些正是GIL所提供的。 GIL是非常容易实现而且很容易添加到Python中。因为只需要管理一个锁所以对于单线程任务来说带来了性能提升。非线程安全的C库变得更容易集成,而这些C扩展则成为Python被不同社区所接受的原因之一。正如所看到的,GIL是CPython开发者在早期Python生涯中面对困难问题的一种实用解决方案。
对多线程Python程序的影响
在多线程版本中GIL阻止了计算密集型任务线程并行执行。GIL对I/O密集型任务多线程程序的性能没有太大的影响,因为在等待I/O时锁可以在多线程之间共享。但是对于一个线程是完全计算密集型的任务来说(例如,利用线程进行部分图像处理)不仅会由于锁而变成单线程任务而且还会明显的增加执行时间。这种执行时间的增加是由于锁带来的获取和释放开销。
为什么GIL没有被删除?
删除GIL会使得Python 3在处理单线程任务方面比Python 2慢,可以想像会产生什么结果。不能否认GIL带来的单线程性能优势,这也就是为什么Python 3中仍然还有GIL。
对于那些一部分线程是计算密集型一部分线程是I/O密集型的程序来说会怎么样呢? 在这样的程序中,Python的GIL通过不让I/O密集型线程从计算密集型线程获取GIL而使I/O密集型线程陷入瘫痪。这是因为Python中内嵌了一种机制,这个机制在固定连续使用时间后强迫线程释放GIL,并且如果没人获取这个GIL,那么同一线程可以继续使用。这个机制面临的问题是大多数计算密集型线程会在别的线程获取GIL之前再次获取GIL。
如何处理Python中的GIL?
多进程vs多线程:最流行的方法是应用多进程方法,在这个方法中你使用多个进程而不是多个线程。每一个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。Python拥有一个multiprocessing模块可以帮助我们轻松创建多进程,进程管理有自己的开销。多进程比多线程更“重”,因此请记住,这可能成为规模瓶颈。
替代Python解释器:Python中有多个解释器实现办法,分别用C,Java,C#和Python编写的CPython,JPython,IronPython和PyPy是最受欢迎的。GIL只存在于传统的Python实现方法如CPython中。
只有当正在编写C扩展或者您的程序中有计算密集型的多线程任务时才会被GIL影响。 -
Python垃圾回收与内存泄露
答:Python解释器内核采用内存池方式管理物理内存,当创建新对象时,解释器在预先申请的物理内存块上分配相应的空间给对象使用,这样可以避免频繁的分配和释放物理内存。那么这些内存在什么时候释放呢?这涉及到Python对象的引用计数和垃圾回收。
什么是垃圾?在python解释器内部无任何地方引用的对象,这些对象也就是所谓的内存垃圾,python解释器有一套垃圾回收机制,确保内存中无用对象及其空间及时被清理。
什么是垃圾回收?Python垃圾回收是指内存不再使用时的释放和回收过程。Python通过两种机制实现垃圾回收:引用计数、能解决循环引用问题的垃圾收集器。
引用计数:引用计数是每个python对象的一个属性,该属性记录着有多少变量引用(指向)了该对象,该属性就称之为引用计数。将一个对象直接或者间接赋值给一个变量时,对象的计数器会加1 ;当变量被del删除,或者离开变量所在作用域时,对象的引用计数器会减 1。当引用计数归零时,代表无任何地方引用该对象,解释器将该对象安全的销毁。可以通过sys模块getrefcount()函数获取对象当前的引用计数。
垃圾收集器:引用计数存在一个比较严重的缺陷是,无法及时回收存在循环引用对象。只有容器对象才会形成循环引用,比如list、class、deque、dict、set等都属于容器类型,那么什么是循环引用?循环引用即两个对象互相引用对方,循环引用可能带来内存泄露问题。对于循环引用带来的问题,python解释器提供了垃圾收集器(gc)模块,gc使用分代回收算法回收垃圾。
所谓分代回收,是一种以空间换时间的操作方式。Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了三个generation(代),分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在第 0 代,年轻代链表的总数达到设定阈值时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推。老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。
分代回收在实现上,支持垃圾收集的对象(主要是容器对象),其内核的PyTypeObject结构体对象的tp_flags变量的Py_TYFLAGS_HAVE_GC位为1。凡是该标记位为1的对象,其底层物理内存的分配使用_PyObject_GC_Malloc函数,其他使用PyObject_Malloc函数。_PyObject_GC_Malloc本质上也是调用PyObject_Malloc函数在内存池上分配内存,但是会多分配PyGC_Head结构体大小的内存,该PyGC_Head位于对象实际内存的前面。PyGC_Head有一个gc_refs属性,垃圾收集器通过判断gc_refs值来实现垃圾回收。
所有支持垃圾收集的对象,在创建时都会被添加到一个gc双向链表,也就是前面所说的第 0 代的链表头部(解释器c源码中的_PyGC_generation0)。另外还有两个gc双向链表,存储了第 1 代和第 2 代对象。垃圾收集主要流程如下: -
对于每一个容器对象,设置一个gc_refs值,并将其初始化为该对象的引用计数值。
-
对于每一个容器对象,找到所有其引用的对象,将被引用对象的gc_refs值减1。
-
执行完步骤2以后所有gc_refs值还大于0的对象都被非容器对象引用着,至少存在一个非循环引用。因此不能释放这些对象,将他们放入另一个集合。
-
在步骤3中不能被释放的对象,如果他们引用着某个对象,被引用的对象也是不能被释放的, 因此将这些对象也放入另一个集合中。
-
此时还剩下的对象都是无法到达(unreachable)的对象, 现在可以释放这些对象了。
在循环引用中,对于unreachable、但collectable的对象,Python的gc垃圾回收机制能够定时自动回收这些对象。但是如果对象定义了__del__方法,这些对象变为uncollectable,垃圾回收机制无法收集这些对象,这也就是上面代码发生内存泄露的原因。
Python解释器标准库对外暴露的gc模块,提供了对内部垃圾收集的访问及配置等接口,比如开启或关闭gc、设置回收阈值、获取对象的引用对象等。在需要的地方,我们可以手动执行垃圾回收,及时清理不必要的内存对象。
解决内存泄露。如果存在循环引用,并且被循环引用的对象定义了__del__方法,就会发生内存泄露。如果我们的代码无法避免循环引用,但只要没有定义__del__方法,并且保证gc模块被打开,就不会发生内存泄露。但是由于gc垃圾收集机制,要遍历所有被垃圾收集器管理的python对象(包括垃圾和非垃圾对象),该过程比较耗时可能会造成程序卡顿,会对某些对内存、cpu要求较高的场景造成性能影响。那怎么才能优雅地避免内存泄露呢?
如果被循环引用的对象定义了__del__方法,但是只要编写足够安全的代码,也可以保证不发生内存泄露。比如对于发生内存泄露的函数,在函数结束前解除循环引用,即可解决内存泄露问题。例如:
def cycle_ref():
a1 = A()
a2 = A()
a1.child = a2
a2.child = a1
# 解除循环引用,避免内存泄露
a1.child = None
a2.child = None
对于上述方法,有可能会忘记那一两行无关紧要的代码而造成灾难性后果。那怎么办?Python已经考虑到这点:弱引用。
弱引用: Python标准库提供了weakref模块,弱引用不会在引用计数中计数,其主要目的是解决循环引用。并非所有的对象都支持weakref,例如list和dict就不支持。下面是weakref比较常用的方法:
1.class weakref.ref(object[, callback]) :创建一个弱引用对象,object是被引用的对象,callback是回调函数(当被引用对象被删除时,调用该回调函数)
2.weakref.proxy(object[, callback]):创建一个用弱引用实现的代理对象,参数同上
3.weakref.getweakrefcount(object) :获取对象object关联的弱引用对象数
4.weakref.getweakrefs(object):获取object关联的弱引用对象列表
5.class weakref.WeakKeyDictionary([dict]):创建key为弱引用对象的字典
6.class weakref.WeakValueDictionary([dict]):创建value为弱引用对象的字典
7.class weakref.WeakSet([elements]):创建成员为弱引用对象的集合对象
同样对于上面发生内存泄露的cycle_ref函数,使用weakref稍加改造,便可更安全地解决内存泄露问题:
# -*- coding: utf8 -*-
import weakref
class A(object):
def __init__(self):
self.data = [x for x in range(100000)]
self.child = None
def __del__(self):
pass
def cycle_ref():
a1 = A()
a2 = A()
a1.child = weakref.proxy(a2)
a2.child = weakref.proxy(a1)
if __name__ == '__main__':
import time
while True:
time.sleep(0.5)
cycle_ref()
-
虚拟内存与物理内存区别
答:首先从进程访问地址开始,进程开始要访问一个地址,它可能会经历下面的过程:
1.每次要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
2.所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
3.进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
4.页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
5.当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
6.缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。
事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
虚拟存储器涉及三个概念: 虚拟存储空间,磁盘空间,内存空间
可以认为虚拟空间都被映射到了磁盘空间中,(事实上也是按需要映射到磁盘空间上,通过mmap),并且由页表记录映射位置,当访问到某个地址的时候,通过页表中的有效位,可以得知此数据是否在内存中,如果不是,则通过缺页异常,将磁盘对应的数据拷贝到内存中,如果没有空闲内存,则选择牺牲页面,替换其他页面。
mmap是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间地址映射到一个磁盘文件上,当不设置这个地址时,则由系统自动设置,函数返回对应的内存地址(虚拟地址),当访问这个地址的时候,就需要把磁盘上的内容拷贝到内存了,然后就可以读或者写,最后通过manmap可以将内存上的数据换回到磁盘,也就是解除虚拟空间和内存空间的映射,这也是一种读写磁盘文件的方法,也是一种进程共享数据的方法 :共享内存
物理内存和虚拟内存关系:物理内存和虚拟内存对应。除OS外任何程序都不会直接访问物理内存而是访问虚拟内存。可把虚拟内存等同于物理内存。以后就只说内存,不再区分物理内存和虚拟内存。
页面文件和虚拟内存关系:可把虚拟内存等同于物理内存。改变页面文件大小可改变虚拟内存大小。详细来说:页面文件只是改变了物理内存的大小,当然也改变了虚拟内存的大小。(猜测:物理内存和虚拟内存的映射在大小上是1:1的。)可禁用页面文件但不能禁用虚拟内存。
虚拟地址空间和物理地址空间对应:虚拟地址空间指的是进程的可用地址空间范围。而物理地址空间指的是实际可用的内存空间范围。 -
Socket编程:raw_socket
答:raw_socket,即原始套接字。首先深入了解一下 python socket 通信,涉及到的函数:import socket、socket()、setsockopt()、sendto()、recvfrom()。因为使用的是原始套接字,所以我们不使用bind/connect函数:
bind 函数仅仅设置本地地址。就输出而言,调用bind函数设置的是将用于从这个原始套接字发送的所有数据报的源IP地址。如果不调用bind,内核就吧源IP地址设置为外出接口的主IP地址。
connect函数仅仅设置外地地址,同样因为原始套接字不存在端口号的概念。就输出而言,调用connect之后我们可以把sendto调用改为write或者send调用,因为目的IP地址已经指定了。connect函数也是三次握手的发生过程。
套接字参数:socket.socket([family[, type[, proto]]])
参数说明:
family:协议簇/地址簇。最常用的就是 socket.AF_INET 了,TCP/UDP 通信均属于此类型。除此之外常用的还有 AF_UNIX/AF_LOCAL ,代表UNIX域协议,属于IPC(进程间通信)的一种方式;AF_INET6 ,IPv6 通信。
socket.AF_UNIX :只能够用于单一的Unix系统进程间通信
socket.AF_INET :服务器之间网络通信
socket.AF_INET6 :IPv6
type:socket的类型,官网给出的列表如下:
socket.SOCK_STREAM
socket.SOCK_DGRAM
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
还有两种就是 socket.SOCK_RDM 与 socket.SOCK_SEQPACKET,基本没见过用,前两种分别代表:面向流(TCP)和面向数据报(UDP)的socket通信。
proto: 协议类型,常见的为
IPPROTO_ICMP = 1
IPPROTO_IP = 0
IPPROTO_RAW = 255
IPPROTO_TCP = 6
IPPROTO_UDP = 17
设置套接字选项
setsockopt:设置套接字选项
socket.setsockopt(level, optname, value)
具体参数
level:参数作用范围,常见的包括:
SOL_SOCKET SOL应该是指的 SOck Level ,意为套接字层选项,常见的有 SO_REUSEADDR ,可以服用处于 Time_wait 状态的端口。
IPPROTO_IP IP数据包选项,一个将要用到的是 IP_HDRINCL ,如果是TRUE,IP头就会随即将发送的数据一起提交,并从读取的数据中返回。
还有 IPPROTO_TCP 等,此处不多做介绍。
Socket 的作用就是封装了各种不同的底层协议,为我们提供一个统一的操作接口。使用socket通信的时候,我们只需要根据协议类型来初始化相应的socket,然后将我们需要写入的数据传入该socket即可。因此,在初始化之后,socket为我们做了这么几件事情:
①对于面向流的连接如TCP,可以帮助我们自动完成三次握手(connect函数)和四次挥手(close函数)的过程
②在我们每次发送数据的时候,将我们要发送的数据根据默认或者你设置的选项包裹好包头,将其交给网卡的发送缓冲区
③接受数据的时候,帮助我们去掉包头
由于不同协议都可以使用同样的接口进行发送和接受数据,因此,区分不同包头的过程都是在socket()函数中完成的。
包结构图:
1)创建四层以上的套接字
直接使用 socket.socket(socket.AF_INET,socket.SOCK_STREAM/socket.SOCK_DGRAM , socket.IPPROTO_TCP)即可,proto 可以自动推断(等价于IPPROTO_IP),也可以直接简写为s = socket.socket(),意味着我们需要填充的内容仅仅是包结构图中的 [ 数据 ] 部分的内容。
2)创建三层套接字
①自行填充TCP头/UDP头,IP头部交给内核填充,意味着我们需要填充的是包结构图中的 [ TCP包头 | 数据 ],因此,我们就需要传入 socket 函数的第三个参数。例如要自己构造TCP包,可以用 socket.socket(socket.AF_INET,socket.SOCK_RAW , socket.IPPROTO_TCP )
②自行填充 四层协议头部和IP头部(限定是IP协议),意味着我们需要填充的是包结构图中的 [ IP包头 | TCP包头 | 数据 ] 的内容。这个和上面那个差不多,只不过我们可以修改IP头部,一种方式是:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # 设置 IP 头部自己发送
另外一种方式是:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
这两种方式应该都是仅仅限于发送IP协议,所以 Ethernet 头部的协议字段不用我们填充。
3)创建二层套接字
方式1:
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
自行填充 以太网包头,意味着我们需要填充的是上图中的 [ MAC包头 | IP包头 | TCP包头 | 数据 ] 的内容。
方式2:
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
使用SOCK_RAW发送的数据必须包含链路层的协议头,接受得到的数据包,包含链路层协议头。而使用SOCK_DGRAM则都不含链路层的协议头。也即是说,需要填充的是上图中的 [ IP包头 | TCP包头 | 数据 ] 的内容。
计算机网络
-
TCP与UDP的区别
答:从9个方面比较:
1.连接: TCP面向连接,UDP面向非连接
2.可靠性: TCP可靠, UDP非可靠
3.有序性: TCP有序, UDP不保证有序
4.速度: TCP慢,UDP快
5.量级: TCP重量级, UDP轻量级
6.拥塞控制或流量控制: TCP有, UDP没有
7 TCP面向字节流,无记录边界; UDP面向报文,有记录边界
8 TCP只能单播; UDP可以广播或组播
9.应用场景: TCP效率低,准确性高; UDP效率高,准确性低 -
TCP 三次握手
答:Server处于Listen状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了:1)当Client端socket执行connect连接时,首先发送SVN报文到Server,进入SVN_SENT状态,等待Server发送ACK;2)Server接受到SVN进入SVN_RCVD状态,(很短暂,一般查询不到),发送SVN+ACK给Client端;3)Client端接受到Server的ACK,发送ACK给Server,Server接收到后进入established状态,Client也进入established状态。如下图所示:
补充问题:为什么TCP连接要建立三次连接?
答:为了防止失效的连接请求又传送到主机,因而产生错误。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
- 如果一个客户端不理会服务端发来的ack,一直重发syn怎么办?(我理解为类似syn洪水攻击)
-
拥塞控制 流量控制
答:TCP/IP的流量控制:利用滑动窗口实现流量控制,如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。
TCP拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。拥塞控制代价:需要获得网络内部流量分布的信息。在实施拥塞控制之前,还需要在结点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样就产生了额外的开销。拥塞控制还需要将一些资源分配给各个用户单独使用,使得网络资源不能更好地实现共享。 -
http cookie具体所有相关内容
答:Cookie总是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie和硬盘Cookie。内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。硬盘Cookie保存在硬盘里,有一个过期时间,除非用户手工清理或到了过期时间,硬盘Cookie不会被删除,其存在时间是长期的。所以,按存在时间,可分为非持久Cookie和持久Cookie。
HTTP请求+cookie的交互流程如下图:
如果步骤5携带的是过期的cookie或者是错误的cookie,那么将认证失败,返回至要求身份认证页面。
HTTP协议作为无状态协议,对于HTTP协议而言,无状态同样指每次request请求之前是相互独立的,当前请求并不会记录它的上一次请求信息。那么问题来了,既然无状态,那完成一套完整的业务逻辑,发送多次请求的情况数不胜数,使用http如何将上下文请求进行关联呢?一种优化后的HTTP请求方式如下:
1)浏览器发送request请求到服务器,服务器除了返回请求的response之外,还给请求分配一个唯一标识ID,协同response一并返回给浏览器。
2)同时服务器在本地创建一个MAP结构,专门以key-value(请求ID-会话内容)形式将每个request进行存储
3)此时浏览器的request已经被赋予了一个ID,第二次访问时,服务器先从request中查找该ID,根据ID查找维护会话的content内容,该内容中记录了上一次request的信息状态。
4)根据查找出的request信息生成基于这些信息的response内容,再次返回给浏览器。如果有需要会再次更新会话内容,为下一次请求提供准备。
所以根据这个会话ID,以建立多次请求-响应模式的关联数据传递。说到这里可能已经唤起了大家许多共鸣。这就是cookie和session对无状态的http协议的强大作用。服务端生成这个全局的唯一标识,传递给客户端用于唯一标记这次请求,也就是cookie;而服务器创建的那个map结构就是session。所以,cookies由服务端生成,用于标记客户端的唯一标识,无特定含义,在每次网络请求中,都会被传送。session服务端自己维护的一个map数据结构,记录key-content上下文内容状态。
一般cookie所具有的属性,包括:
1)Domain:域,表示当前cookie所属于哪个域或子域下面。
对于服务器返回的Set-Cookie中,如果没有指定Domain的值,那么其Domain的值是默认为当前所提交的http的请求所对应的主域名的。比如访问 http://www.example.com,返回一个cookie,没有指名domain值,那么其为值为默认的www.example.com。
2)Path:表示cookie的所属路径。
3)Expire time/Max-age:表示了cookie的有效期。expire的值,是一个时间,过了这个时间,该cookie就失效了。或者是用max-age指定当前cookie是在多长时间之后而失效。如果服务器返回的一个cookie,没有指定其expire time,那么表明此cookie有效期只是当前的session,即是session cookie,当前session会话结束后,就过期了。对应的,当关闭(浏览器中)该页面的时候,此cookie就应该被浏览器所删除了。
4)secure:表示该cookie只能用https传输。一般用于包含认证信息的cookie,要求传输此cookie的时候,必须用https传输。
5)httponly:表示此cookie必须用于http或https传输。这意味着,浏览器脚本,比如javascript中,是不允许访问操作此cookie的。
从服务器端,发送cookie给客户端,是对应的Set-Cookie。包括了对应的cookie的名称,值,以及各个属性。
从客户端发送cookie给服务器的时候,是不发送cookie的各个属性的,而只是发送对应的名称和值。
除了服务器发送给客户端(浏览器)的时候,通过Set-Cookie,创建或更新对应的cookie之外,还可以通过浏览器内置的一些脚本,比如javascript,去设置对应的cookie,对应实现是操作js中的document.cookie。
Cookie的缺陷
①cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
②由于在HTTP请求中的cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
③Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。 -
http传输一个二进制文件的所有过程
答:HTTP协议是基于字符(ASCII)的,当Content-Type项为text/xml,则内容是文本格式;当二进制格式时,Content-Type项为image/gif。由上可知,http协议中content中可以是纯二进制的。
通常上的理解,http协议中请求、相应都是以ascii字符方式传输,如果要传输二进制需要经过BASE64或MIME等编码(因为HTTP协议pop3、smtp邮件协议都是针对文本的,而FTP支持传输二进制数据,即不需要经过编码转换成字符型数据)
如果直接使用http传输二进制(不经过base64编码),可能会造成一下问题:
- 不知道传输字节的具体长度,如传输的int类型,将int类型之间转为char以后,丢失掉了长度的信息,如数字1234567,本来只有4个字节,但是转化成文本的“1234567”是有7个字节。在int类型的时候固然好办,但是一个数组的时候,经过转化以后,在转化回来就很麻烦了。
- 对于一些数字,二进制传输Server是没法处理的。如int 1,二进制数据是0x00000001,按字节传输的时候,client能够正常发送,但是libevent收到以后,在抛给libevent_http层是,会把数据截断,前两位0x00是字符串的停止符。
- post和get的区别
答:POST和GET是HTTP请求的两种方式,都可实现将数据从浏览器向服务器发送带参数的请求。他们的区别为:
1)POST可以改变服务器上的资源的请求,GET不可以;
2)GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连。POST把提交的数据则放置在是HTTP包的包体中。
3)GET方式提交的数据最多只能是1024字节,理论上POST没有限制,可传较大量的数据。
4)POST的安全性要比GET的安全性高。
GET和POST是最常用的HTTP请求方式。还有其他的请求方式,例如PUT、HEAD、DELETE
参考资料
- https://www.cnblogs.com/leohahah/p/10189281.html
- https://www.jianshu.com/p/J4U6rR
- https://www.cnblogs.com/Zzbj/p/9607462.html
- https://blog.csdn.net/gaifuxi9518/article/details/81038818
- https://blog.csdn.net/qq_34788903/article/details/84776831
- https://www.cnblogs.com/TM0831/p/10599716.html
- https://www.cnblogs.com/-wenli/p/10884168.html
- https://blog.csdn.net/yangxiaodong88/article/details/80942790
- https://blog.csdn.net/armlinuxww/article/details/92803381
- https://www.cnblogs.com/aylin/p/5702994.html
- https://blog.csdn.net/xc_zhou/article/details/80637714
- https://blog.csdn.net/fragmentalice/article/details/84983516
- https://blog.csdn.net/lvyibin890/article/details/82217193
- https://www.cnblogs.com/JenningsMao/p/9487465.html
- https://www.cnblogs.com/bq-med/p/8603664.html
更多推荐
所有评论(0)