關於socket的一些初步研究

2021-06-07 18:59:15 字數 2852 閱讀 7398

這些天在研究tornado的原始碼,說實話它的**過於艱深了,需要繞很多彎才能弄清。

我想其中的問題主要是我不太懂socket,於是就花了些時間學習socket,算是有了些收穫,順便記錄在此。

首先是socket的概念。實

客戶端則也通過socket()建立socket;再呼叫connect()連線到伺服器端bind的那個埠。這時候如果等待連線的客戶端超過listen()的設定,就會被拒絕訪問;否則伺服器端會accept()這個請求,並建立起連線。

而accept()會返回新的socket,伺服器和客戶端就可以通過這個socket,呼叫send()/recv()或write()/read()進行通訊。

最後在需要結束通訊時,呼叫shutdown()或close()來關閉這個socket即可。

總結一下的話,就是建立、監聽、連線、接受、收發和斷開的過程。

再介紹一下tornado,它本身是個無阻塞的socket伺服器。

說到阻塞這個概念,cpu的速度是很快的,暫存器、cpu快取和記憶體還能跟上這個速率,訪問時間都在納秒級左右;但如果換成硬碟、網路,訪問時間在毫秒級以上,這就造成了cpu長時間的空閒等待,於是就被稱為阻塞了。

傳統的解決方式是採用多執行緒模型,讓某些執行緒去做這些事,當阻塞發生時,其他執行緒還能繼續執行。這種方式需要執行緒同步,實現比較複雜,而且記憶體和執行緒切換的開銷都很大。

如果在這些i/o訪問時,cpu在發出指令後就立刻返回,繼續處理其他事情;而裝置則自行去完成連線、傳輸等任務,並在結束後讓cpu完成善後工作;這樣就既不會阻塞cpu,也沒有多執行緒的複雜性和開銷了。

在c語言中可以通過fcntl()和ioctl()來將socket設為無阻塞,python中則很直觀地呼叫setblocking(0)即可。

當socket變成無阻塞後,伺服器就需要能非同步處理這些i/o事件了。也

就是說,在呼叫accept()、send()和recv()的時候,函式會立刻返回;而在將來的某個時刻,cpu還需要去維護接受的socket和處理讀取的資料等。

最簡單的方式就是在死迴圈中不斷遍歷所有的socket,檢查它們是否已經就緒;但是這樣的效率顯然不高,因為i/o是很耗時的操作,因此有很大概率是尚未就緒的。

與此類似的還有select()和poll(),它們會返回一類socket的集合。雖然和上一種方式的效率差不多,但因為可以設定超時時間(例如每秒1次),也就不會出現忙等時cpu佔用率仍然100%的情況。

更先進的方式當然就是使用事件。linux中提供了epoll,bsd(含mac os x和ios)中提供了kqueue。我對kqueue還不熟,但epoll的行為是只返回有事件發生的socket,這樣就能保證cpu不會浪費時間去處理未就緒的socket了。

而epoll的事件還存在2種觸發方式:level-triggered和edge-triggered。lt指的是通過狀態來表示事件發生了,例如讀了緩衝區,那麼這個socket就會被epoll_wait()返回,它的事件包含epollin。et則是通過狀態的變化來表示事件的發生,例如讀取緩衝區時,如果沒讀完,那麼狀態一直是可讀的;只有把緩衝區中的資料全部讀完,狀態變為不可讀(eagain)以後,再等下次緩衝區狀態變為可讀時,才會引起epollin事件,並被epoll_wait()返回。

事實上,tornado就是依靠這種無阻塞的機制來與客戶端(瀏覽器)連線和通訊,所以它可以用單執行緒支撐大量併發。

但這只能保證tornado與客戶端之間的通訊是無阻塞的,乙個web伺服器還不可避免地需要與磁碟檔案、資料庫等打交道,只有當這些i/o都是無阻塞時,整個服務才是真正無阻塞的。

然而要做到後者比較複雜(有的socket必須阻塞),所以有時不得不用多執行緒模型來實現。而且因為幾乎所有請求都要訪問資料庫,當併發的請求數不能填滿cpu的負荷時,總會遇到需要等待i/o的時候,此時的無阻塞也就變得無意義了。

因此tornado推薦採用多程序的方式,當乙個程序被阻塞時,其他程序繼續處理其他請求。這樣雖然單個請求會花更長的時間,但總體的效率還能保持在乙個很高的水平。

然後測試下無阻塞+epoll方式的效率。這

次我直接在網上找到了一篇《python下epoll的使用與效能測試》,借用了它的**。

其中稍作了一些改進:輸出的header中增加了「connection: close」,epoll_wait()和poll()設定了1秒的超時時間,以及將send()前的sprintf()移出迴圈。

在我的虛擬機器上分別跑了一下,1000併發時,c和python的版本分別約為2300+ qps和2100+ qps。接著試了下《實現了乙個比nginx速度更快的http伺服器》這篇文章中的**,發現居然有2900+ qps,而且還是用檔案傳遞(sendfile)方式。

比較了半天後,clowwindy說可能是因為他對accept出來的socket呼叫了setsockopt ( infd, sol_tcp, tcp_cork, &on, sizeof ( on ) )。這個tcp_cork和tcp_nodelay是相對的,前者表示將小的資料報合併在一起傳送,後者則是不等待更多包就直接傳送。因為前者可以少傳遞一些資料報,所以在高併發的情況下,這個優勢也就被放大了。

於是我馬上加上了這行**,速度就增加到3000+ qps了。至於python,直接使用con.setsockopt(socket.ipproto_tcp, socket.tcp_cork, 1)即可(mac os x的python下沒有socket.tcp_cork,應該可以直接用常量3代替)。效果也很明顯,增大到2700+ qps了,約為c的90%。

最後還是那句話,在效能要求並非苛刻的時候,採用什麼語言並沒多大影響。

一台好的伺服器,每秒跑1萬hello world應該沒問題,每個請求平均下來只需要0.1 ms的處理時間。而這和資料庫等i/o請求相比根本不是乙個數量級的,因此效能瓶頸其實並不在語言本身。

但是在**量和可讀性上,python卻節約了程式設計師大量的編碼和維護時間。而在當今這種人比機器貴的年代,這就意味著巨大的價值。

關於Socket的一些概念

對於 tcp ip 我們還知道 tcp和 udp,前者可以保證資料的正確和可靠性,後者則允許資料丟失。最後,我們還知道,在建立連線前,必須知道對方的 ip位址和埠號。除此,普通的程式設計師就不會知道太多了,很多時候這些知識已經夠用了。最多,寫服務程式的時候,會使用多執行緒來處理併發訪問。我們還知道如...

關於A 演算法的一些研究

公式 f g h 代價計算,尋路代價最小的就是我們要找的 g 表示從起點 a 移動到網格上指定方格的移動耗費 可沿斜方向移動 h 表示從指定的方格移動到終點 b 的預計耗費 h 有很多計算方法,這裡我們設定只可以上下左右移動 下面拿個例子說明一下 原文參考 假如從a尋路到b 尋路步驟 1.從起點a開...

關於mirai的一些研究

關於mirai的一些研究 配置好對應的go開發環境,即可進行編譯,生成了主要的檔案 badbot為殭屍節點的可執行檔案,cnc為主控端的可執行檔案,其它一些為輔助工具。如下圖所示編譯主控端源 go語句編寫 生成可執行檔案cnc,執行cnc,在本地開啟了23和101的埠監聽 主控端的 主要由go語言編...