【面試】網易遊戲面試題目整理及答案(1)

網易遊戲面試題目整理及題目(1)

Python部分

  1. 迭代器生成器,生成器是如何實現迭代的?
    答:迭代器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)可判斷是否爲迭代器
    生成器generatorgenerator對象是一種特殊的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也可以極大的減少代碼量。

  2. list實現
    答:List的代碼示例:
    List Python
    在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  

在位置1插入一個元素
虛線框表示已經申請但是沒有使用的內存。申請了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)。
List的Pop操作
pop的彈出過程
我們發現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個了。
list空間壓縮
最後看一下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)
List中Remove移除元素
如何確定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編程》
  1. 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 中列出的東西

  1. 裝飾器實現
    答:裝飾器的功能當需要對一段寫好的代碼添加一段新的需求的時候的時候我們就可以用裝飾器實現
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)裝飾器帶參數,需要在閉包外層再定義一個函數,這個函數用來接受裝飾器的參數
類裝飾器帶參數

  1. 菱形繼承
    答:繼承是面向對象編程的一個重要的方式,通過繼承,子類就可以擴展父類的功能。在python中一個類能繼承自不止一個父類,這叫做python的多重繼承(Multiple Inheritance )。語法如下:
class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...):
    pass

在多層繼承和多繼承同時使用的情況下,就會出現複雜的繼承關係,多重多繼承。其中,就會出現菱形繼承。如下圖所示。
菱形繼承
在這種結構中,在調用順序上就出現了疑惑,調用順序究竟是以下哪一種順序呢
①D->B->A->C(深度優先)
②D->B->C->A(廣度優先)
示例代碼如下:
import調用順序
從輸出結果中看,調用順序爲:D->B->A->C->A。可以看到,B、C共同繼承於A,A被調用了兩次。A沒必要重複調用兩次。
其實,上面問題的根源都跟MRO有關,MRO(Method Resolution Order)也叫方法解析順序,主要用於在多重繼承時判斷調的屬性來自於哪個類,其使用了一種叫做C3的算法,其基本思想時在避免同一類被調用多次的前提下,使用廣度優先和從左到右的原則去尋找需要的屬性和方法。
那麼如何避免頂層父類中的某個方法被多次調用呢,此時就需要super()來發揮作用了,super本質上是一個類,內部記錄着MRO信息,由於C3算法確保同一個類只會被搜尋一次,這樣就避免了頂層父類中的方法被多次執行了,上面代碼可以改爲:
使用super關鍵字確保同一個類只會被搜尋一次
可以看出,此時的調用順序是D->B->C->A。即採用是
廣度優先
的遍歷方式。
補充內容:Python類分爲兩種,一種叫經典類,一種叫新式類。都支持多繼承,但繼承順序不同
1)新式類:從object繼承來的類。(如:class A(object)),採用廣度優先搜索的方式繼承(即先水平搜索,再向上搜索)。
2)經典類:不從object繼承來的類。(如:class A()),採用深度優先搜索的方式繼承(即先深入繼承樹的左側,再返回,再找右側)。

  1. 內存垃圾回收:分代回收細節
    答: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垃圾收集機制就會被觸發,把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活於整個系統的生命週期內

  1. 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協議等。

  2. uWSGI進程模型
    答:暫無

  3. 比較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一個值就表示將值傳遞給下一個執行的協程

  4. 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、B 和 C 三個進程
    等待隊列
    當進程 A 執行到創建 Socket 的語句時,操作系統會創建一個由文件系統管理的 Socket 對象(如下圖)。
    創建socket創建 Socket
    這個 Socket 對象包含了發送緩衝區、接收緩衝區與等待隊列等成員。等待隊列是個非常重要的結構,它指向所有需要等待該 Socket 事件的進程。
    當程序執行到 Recv 時,操作系統會將進程 A 從工作隊列移動到該 Socket 的等待隊列中(如下圖)。 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 接收到了數據的處理流程:
Sock2接收到了數據,中斷程序喚起進程A
注:Recv 和 Select 的中斷回調可以設置成不同的內容。
所謂喚起進程,就是將進程從所有的等待隊列中移除,加入到工作隊列裏面,如下圖所示:
將進程A從所有等待隊列中移除,再加入到工作隊列裏面
將進程 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,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對象
eventpoll 對象也是文件系統中的一員,和 Socket 一樣,它也會有等待隊列。創建一個代表該 Epoll 的 eventpoll 對象是必須的,因爲內核要維護“就緒列表”等數據,“就緒列表”可以作爲 eventpoll 的成員。
維護監視列表
創建 Epoll 對象後,可以用 epoll_ctl 添加或刪除所要監聽的 Socket。以添加 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阻塞進程
epoll_wait 阻塞進程
如上圖所示,內核會將進程 A 放入 eventpoll 的等待隊列中,阻塞進程。當 Socket 接收到數據,中斷程序一方面修改 Rdlist,另一方面喚醒 eventpoll 等待隊列中的進程,進程 A 再次進入運行狀態(如下圖)
Epoll喚醒進程
Epoll 喚醒進程
也因爲 Rdlist 的存在,進程 A 可以知道哪些 Socket 發生了變化。
Epoll 的實現細節
至此,對 Epoll 的本質已經有一定的瞭解。但還需要知道 eventpoll 的數據結構是什麼樣子?此外,就緒隊列應該使用什麼數據結構?eventpoll 應使用什麼數據結構來管理通過 epoll_ctl 添加或刪除的 Socket?
Epoll原理示意圖
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。
Select、Poll與Epoll的對比

  1. Python裏的eval
    答:eval函數就是實現list、dict、tuple與str之間的轉化str函數把list,dict,tuple轉爲爲字符串,eval函數的用法就是把字符串對象轉換爲能夠具體的對象。

  2. 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
    ④缺陷:入門門檻較高
  1. 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影響。

  2. 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 代對象。垃圾收集主要流程如下

  3. 對於每一個容器對象,設置一個gc_refs值,並將其初始化爲該對象的引用計數值

  4. 對於每一個容器對象,找到所有其引用的對象,將被引用對象的gc_refs值減1

  5. 執行完步驟2以後所有gc_refs值還大於0的對象都被非容器對象引用着,至少存在一個非循環引用。因此不能釋放這些對象,將他們放入另一個集合

  6. 在步驟3中不能被釋放的對象,如果他們引用着某個對象,被引用的對象也是不能被釋放的, 因此將這些對象也放入另一個集合中

  7. 此時還剩下的對象都是無法到達(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. 虛擬內存與物理內存區別
    答:首先從進程訪問地址開始,進程開始要訪問一個地址,它可能會經歷下面的過程
    1.每次要訪問地址空間上的某一個地址,都需要把地址翻譯爲實際物理內存地址
    2.所有進程共享這整一塊物理內存,每個進程只把自己目前需要的虛擬地址空間映射到物理內存上
    3.進程需要知道哪些地址空間上的數據在物理內存上,哪些不在(可能這部分存儲在磁盤上),還有在物理內存上的哪裏,這就需要通過頁表來記錄
    4.頁表的每一個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(如果在的話)
    5.當進程訪問某個虛擬地址的時候,就會先去看頁表,如果發現對應的數據不在物理內存上,就會發生缺頁異常
    6.缺頁異常的處理過程,操作系統立即阻塞該進程,並將硬盤裏對應的頁換入內存,然後使該進程就緒,如果內存已經滿了,沒有空地方了,那就找一個頁覆蓋,至於具體覆蓋的哪個頁,就需要看操作系統的頁面置換算法是怎麼設計的了
    事實上,在每個進程創建加載時,內核只是爲進程“創建”了虛擬內存的佈局,具體就是初始化進程控制表中內存相關的鏈表,實際上並不立即就把虛擬內存對應位置的程序數據和代碼(比如.text .data段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射),等到運行到對應的程序時,纔會通過缺頁異常,來拷貝數據。還有進程運行過程中,要動態分配內存,比如malloc時,也只是分配了虛擬內存,即爲這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數據時,才引發缺頁異常
    虛擬存儲器涉及三個概念: 虛擬存儲空間磁盤空間內存空間
    虛擬存儲器
    可以認爲虛擬空間都被映射到了磁盤空間中,(事實上也是按需要映射到磁盤空間上,通過mmap),並且由頁表記錄映射位置,當訪問到某個地址的時候,通過頁表中的有效位,可以得知此數據是否在內存中,如果不是,則通過缺頁異常,將磁盤對應的數據拷貝到內存中,如果沒有空閒內存,則選擇犧牲頁面,替換其他頁面。
    mmap是用來建立從虛擬空間到磁盤空間的映射的,可以將一個虛擬空間地址映射到一個磁盤文件上,當不設置這個地址時,則由系統自動設置,函數返回對應的內存地址(虛擬地址),當訪問這個地址的時候,就需要把磁盤上的內容拷貝到內存了,然後就可以讀或者寫,最後通過manmap可以將內存上的數據換回到磁盤,也就是解除虛擬空間和內存空間的映射,這也是一種讀寫磁盤文件的方法,也是一種進程共享數據的方法共享內存
    物理內存與虛擬內存的關係
    物理內存和虛擬內存關係:物理內存和虛擬內存對應。除OS外任何程序都不會直接訪問物理內存而是訪問虛擬內存。可把虛擬內存等同於物理內存。以後就只說內存,不再區分物理內存和虛擬內存。
    頁面文件和虛擬內存關係:可把虛擬內存等同於物理內存。改變頁面文件大小可改變虛擬內存大小。詳細來說:頁面文件只是改變了物理內存的大小,當然也改變了虛擬內存的大小。(猜測:物理內存和虛擬內存的映射在大小上是1:1的。)可禁用頁面文件但不能禁用虛擬內存。
    虛擬地址空間和物理地址空間對應:虛擬地址空間指的是進程的可用地址空間範圍。而物理地址空間指的是實際可用的內存空間範圍。

  2. 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包頭 | 數據 ] 的內容。

計算機網絡

  1. 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效率高,準確性低

  2. 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連接要建立三次連接
    答:爲了防止失效的連接請求又傳送到主機,因而產生錯誤如果使用的是兩次握手建立連接,假設有這樣一種場景,客戶端發送了第一個請求連接並且沒有丟失,只是因爲在網絡結點中滯留的時間太長了,由於TCP的客戶端遲遲沒有收到確認報文,以爲服務器沒有收到,此時重新向服務器發送這條報文,此後客戶端和服務器經過兩次握手完成連接,傳輸數據,然後關閉連接。此時此前滯留的那一次請求連接,網絡通暢了到達了服務器,這個報文本該是失效的,但是,兩次握手的機制將會讓客戶端和服務器再次建立連接,這將導致不必要的錯誤和資源的浪費。如果採用的是三次握手,就算是那一次失效的報文傳送過來了,服務端接受到了那條失效報文並且回覆了確認報文,但是客戶端不會再次發出確認。由於服務器收不到確認,就知道客戶端並沒有請求連接。

  • 如果一個客戶端不理會服務端發來的ack,一直重發syn怎麼辦?(我理解爲類似syn洪水攻擊)
  1. 擁塞控制 流量控制
    答:TCP/IP的流量控制:利用滑動窗口實現流量控制,如果發送方把數據發送得過快,接收方可能會來不及接收,這就會造成數據的丟失。所謂流量控制就是讓發送方的發送速率不要太快,要讓接收方來得及接收。TCP爲每一個連接設有一個持續計時器(persistence timer)。只要TCP連接的一方收到對方的零窗口通知,就啓動持續計時器。若持續計時器設置的時間到期,就發送一個零窗口控測報文段(攜1字節的數據),那麼收到這個報文段的一方就重新設置持續計時器。
    TCP擁塞控制:防止過多的數據注入到網絡中,這樣可以使網絡中的路由器或鏈路不致過載。擁塞控制所要做的都有一個前提:網絡能夠承受現有的網絡負荷。擁塞控制是一個全局性的過程,涉及到所有的主機、路由器,以及與降低網絡傳輸性能有關的所有因素。擁塞控制代價:需要獲得網絡內部流量分佈的信息。在實施擁塞控制之前,還需要在結點之間交換信息和各種命令,以便選擇控制的策略和實施控制。這樣就產生了額外的開銷。擁塞控制還需要將一些資源分配給各個用戶單獨使用,使得網絡資源不能更好地實現共享。

  2. http cookie具體所有相關內容
    答:Cookie總是保存在客戶端中,按在客戶端中的存儲位置,可分爲內存Cookie和硬盤Cookie。內存Cookie由瀏覽器維護,保存在內存中,瀏覽器關閉後就消失了,其存在時間是短暫的。硬盤Cookie保存在硬盤裏,有一個過期時間,除非用戶手工清理或到了過期時間,硬盤Cookie不會被刪除,其存在時間是長期的。所以,按存在時間,可分爲非持久Cookie和持久Cookie
    HTTP請求+cookie的交互流程如下圖:
    HTTP請求+cookie的交互流程
    如果步驟5攜帶的是過期的cookie或者是錯誤的cookie,那麼將認證失敗,返回至要求身份認證頁面。
    HTTP協議作爲無狀態協議,對於HTTP協議而言,無狀態同樣指每次request請求之前是相互獨立的,當前請求並不會記錄它的上一次請求信息。那麼問題來了,既然無狀態,那完成一套完整的業務邏輯,發送多次請求的情況數不勝數,使用http如何將上下文請求進行關聯呢?一種優化後的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左右。對於複雜的存儲需求來說是不夠用的。

  3. http傳輸一個二進制文件的所有過程
    答:HTTP協議是基於字符(ASCII)的,當Content-Type項爲text/xml,則內容是文本格式;當二進制格式時,Content-Type項爲image/gif。由上可知,http協議中content中可以是純二進制的。
    通常上的理解,http協議中請求、相應都是以ascii字符方式傳輸,如果要傳輸二進制需要經過BASE64或MIME等編碼(因爲HTTP協議pop3、smtp郵件協議都是針對文本的,而FTP支持傳輸二進制數據,即不需要經過編碼轉換成字符型數據)
    如果直接使用http傳輸二進制(不經過base64編碼),可能會造成一下問題

  1. 不知道傳輸字節的具體長度,如傳輸的int類型,將int類型之間轉爲char以後,丟失掉了長度的信息,如數字1234567,本來只有4個字節,但是轉化成文本的“1234567”是有7個字節。在int類型的時候固然好辦,但是一個數組的時候,經過轉化以後,在轉化回來就很麻煩了。
  2. 對於一些數字,二進制傳輸Server是沒法處理的。如int 1,二進制數據是0x00000001,按字節傳輸的時候,client能夠正常發送,但是libevent收到以後,在拋給libevent_http層是,會把數據截斷,前兩位0x00是字符串的停止符。
  1. 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

參考資料

  1. https://www.cnblogs.com/leohahah/p/10189281.html
  2. https://www.jianshu.com/p/J4U6rR
  3. https://www.cnblogs.com/Zzbj/p/9607462.html
  4. https://blog.csdn.net/gaifuxi9518/article/details/81038818
  5. https://blog.csdn.net/qq_34788903/article/details/84776831
  6. https://www.cnblogs.com/TM0831/p/10599716.html
  7. https://www.cnblogs.com/-wenli/p/10884168.html
  8. https://blog.csdn.net/yangxiaodong88/article/details/80942790
  9. https://blog.csdn.net/armlinuxww/article/details/92803381
  10. https://www.cnblogs.com/aylin/p/5702994.html
  11. https://blog.csdn.net/xc_zhou/article/details/80637714
  12. https://blog.csdn.net/fragmentalice/article/details/84983516
  13. https://blog.csdn.net/lvyibin890/article/details/82217193
  14. https://www.cnblogs.com/JenningsMao/p/9487465.html
  15. https://www.cnblogs.com/bq-med/p/8603664.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章