日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

[Python]再学 socket 之非阻塞 Server

發布時間:2025/4/16 python 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [Python]再学 socket 之非阻塞 Server 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

再學 socket 之非阻塞 Server

本文是基于 python2.7 實現,運行于 Mac 系統下

本篇文章是上一篇初探 socket 的續集,
上一篇文章介紹了:如何建立起一個基本的 socket 連接、TCP 和 UDP 的概念、socket 常用參數和方法

Socket 是用來通信、傳輸數據的對象,上一篇已經研究了如果進行基本的通行和傳輸數據。因為,在這個互
聯網爆發的時代,做為 Server 的 socket 要同時接收很多的請求。

通過閱讀:地址,強烈推薦閱讀原文。

整理了下面的文字,如何:創建一個 非阻塞的 server。

一、阻塞 Server

  • 阻塞 Server 示例
  • 為什么會出現阻塞

1.1 阻塞 Server 示例

下面就通過C/S模型,展示阻塞狀態:

  • 接收其它 socket 請求的 socket 叫做:Server(S)
  • 請求 Server 的 socket 叫做:Client(C)

該代碼片段分別是:阻塞的 Server 和測試用的 Client:

#!/usr/bin/env python # -*- coding:utf-8 -*- # # Author : XueWeiHan # Date : 17/2/25 上午10:39 # Desc : 阻塞 server import socket import timeSERVER_ADDRESS = (HOST, PORT) = '', 50007 REQUEST_QUEUE_SIZE = 5def handle_request(client_connection):"""處理請求"""request = client_connection.recv(1024)print('Server recv: {request_data}'.format(request_data=request.decode()))time.sleep(10) # 模擬阻塞事件http_response = "Hello, I'm server"client_connection.sendall(http_response)def server():listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)listen_socket.bind(SERVER_ADDRESS)listen_socket.listen(REQUEST_QUEUE_SIZE)print('Server on port {port} ...'.format(port=PORT))while 1:client_connection, client_address = listen_socket.accept()handle_request(client_connection)client_connection.close()if __name__ == '__main__':server()
  • REQUEST_QUEUE_SIZE:在 sever 阻塞的時,允許掛起幾個連接。便于可以處理時直接從該隊列中取得連接,減少建立連接的時間
  • time.sleep:用于模擬阻塞

測試用的 Client

#!/usr/bin/env python # -*- coding:utf-8 -*- # # Author : XueWeiHan # Date : 17/2/25 上午11:13 # Desc : 測試 clientimport socketSERVER_ADDRESS = (HOST, PORT) = '', 50007def send_message(s, message):"""發送請求"""s.sendall(message)def client():message = "Hello, I'm client"s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect(SERVER_ADDRESS)send_message(s, message)print 'Client is Waiting response...'data = s.recv(1024)s.close()print 'Client recv:', repr(data) # 打印從服務器接收回來的數據if __name__ == '__main__':client()

打開三個終端,先運行 Server,在另外兩個終端運行 Client(分別起名為client1、client2),會發現
服務器先接收 client1 的數據,然后返回響應。再此之前 client2 一直處于等待的狀態。只有等 Server
處理完 client1 的請求后,才會接收 client2 的數據。

這樣一個個地接收請求、處理請求的 Server 就叫做 阻塞 Server。

1.2 為什么會出現阻塞?

因為服務器處理請求是需要消耗時間的,正如我上面的阻塞 Server 代碼中的time.sleep(10),用于模擬
服務器處理請求消耗的時間。

在處理完上一個請求(返回給 Client 數據)的這段時間中,服務器無法處理其它的請求,只能讓其它的 Client 等待。這樣的效率是
極其低下的,所以下面就介紹如何創建一個非阻塞的 Server

二、非阻塞 Server

  • 需要知道的一些基本概念
  • 非阻塞 Server 示例(多進程)

后面會用多進程實現 非阻塞socket,在此之前需要了解一些基本知識和概念,便于理解后面的代碼。

2.1 需要知道的一些基本概念

  • Socket 處理請求的過程
  • 進程
  • 文件描述符
  • 如何查看進程和用戶資源

2.1.1 Socket 處理請求的過程

參照上面寫的阻塞 Server 的代碼,可以看出:服務器端的socket對象,listen_socket 從不和客戶端交換數據。它只會通過accept方法接受連接。然后,創建一個新的socket對象,client_connection用于和客戶端通信。

所以,服務器端的socket 分為:接受請求的socket(listen_socket)與客戶端傳輸數據的socket(client_connection)

正如上面說到的,真正阻塞地方是:與客戶端傳輸數據的socket(client_connection) 需要等待處理請求的結果,然后返還給客戶端,結束這次通信,才能處理后面的請求。

2.1.2 進程

存在硬盤中的叫做‘程序’(*.py),當程序運行加載到內存中的時候叫做‘進程’。系統會分配給每個進程一個唯一 ID,
這個 ID 叫做:PID ,進程還分為父進程和子進程,父進程(PPID)創建子進程(PID)。關系如下圖:

可以通過ps命令來查看進程的信息:每天一個linux命令(41):ps命令

需要注意:

  • 子進程一定要關閉
  • 子進程關閉一定要通知父進程,否則會出現‘僵尸進程’
  • 一定要先結束父進程,再結束子進程,否則會出現‘孤兒進程’

僵尸進程:一個進程使用fork創建子進程,如果子進程退出,而父進程并沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。這種進程稱之為僵尸進程。(系統所能使用的進程號是有限制的,如果大量的產生僵死進程,將因為沒有可用的進程號而導致系統不能產生新的進程。則會拋出OSError: [Errno 35] Resource temporarily unavailable異常)

孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,并由init進程對它們完成狀態收集工作。(沒有危害)

2.1.3 文件描述符

在UNIX中一切都是一個文件,當操作系統打開一存在的個文件的時候,便會返回一個‘文件描述符’,進程通過
操作該文件操作符,從而實現對文件的讀寫。Socket 是一個操作文件描述符的進程,Python 的 socket
模塊提供了這些操作系統底層的實現。我們只需要調用socket對象的方式就可以了。

需要注意

  • 文件描述符的回收機制是采用引用計數方式
  • 每次操作完文件描述符需要調用close()方法,關閉文件描述符。道理和進程一樣,操作系統都會最多可創建的文本描述符的限制,如果一直不關閉文本描述符的話,導致數量太多無法創建新的,就會拋出OSError: [Errno 24] Too many open file異常。

2.1.4 如何查看進程和用戶資源極限

計算機的計算和存儲能力都是有限的,統稱為計算機資源。

上面說了進程和文件描述符號都是有個最大數量(極限),下面就是用于查看和修改用戶資源限制的命令——ulimit。

-a 列出所有當前資源極限。 -c 以 512 字節塊為單位,指定核心轉儲的大小。 -d 以 K 字節為單位指定數據區域的大小。 -f 使用 Limit 參數時設定文件大小極限(以塊為單位),或者在未指定參數時報告文件大小極限。缺省值為 -f 標志。 -H 指定設置某個給定資源的硬極限。如果用戶擁有 root 用戶權限,可以增大硬極限。任何用戶均可減少硬極限。 -m 以 K 字節為單位指定物理內存的大小(駐留集合大小)。系統未強制實施此限制。 -n 指定一個進程可以擁有的文件描述符數的極限。 -r 指定對進程擁有線程數的限制。 -s 以 K 字節為單位指定堆棧的大小。 -S 指定為給定的資源設置軟極限。軟極限可增大到硬極限的值。如果 -H 和 -S 標志均未指定,極限適用于以上二者。 -t 指定每個進程所使用的秒數。 -u 指定對用戶可以創建的進程數的限制。

常用命令如下:

  • ulimit -a:查看
  • ulimit -n:設置一個進程可擁有文件描述符數量
  • ulimit -u:最多可以創建多少個進程

2.2 Fork 方式的非阻塞 Server

采用 fork 的方式實現非阻塞 Server,主要原理就是當 socket 接受到(accept)一個請求,就 fork 出一個子進程
去處理這個請求。然后父進程繼續接受請求。從而實現并發的處理請求,不需要處理上一個請求才能接受、處理下一個請求。

import errno import os import signal import socketSERVER_ADDRESS = (HOST, PORT) = '', 8888 REQUEST_QUEUE_SIZE = 1024def grim_reaper(signum, frame):while True:try:pid, status = os.waitpid(-1, # Wait for any child processos.WNOHANG # Do not block and return EWOULDBLOCK error)except OSError:returnif pid == 0: # no more zombiesreturndef handle_request(client_connection):request = client_connection.recv(1024)print(request.decode())http_response = b"""\ HTTP/1.1 200 OKHello, World! """client_connection.sendall(http_response)def serve_forever():listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)listen_socket.bind(SERVER_ADDRESS)listen_socket.listen(REQUEST_QUEUE_SIZE)print('Serving HTTP on port {port} ...'.format(port=PORT))signal.signal(signal.SIGCHLD, grim_reaper)while True:try:client_connection, client_address = listen_socket.accept()except IOError as e:code, msg = e.args# restart 'accept' if it was interruptedif code == errno.EINTR:continueelse:raisepid = os.fork()if pid == 0: # childlisten_socket.close() # close child copyhandle_request(client_connection)client_connection.close()os._exit(0)else: # parentclient_connection.close() # close parent copy and loop overif __name__ == '__main__':serve_forever()

如閱讀代碼時出現的問題,可以參考下面的關鍵字:

  • Python os.fork,文件句柄(引用計數)、子進程(pid==0)
  • linux ulimt命令
  • 僵尸進程,如何避免僵尸進程,采用os.wait。
  • python signal模塊
  • error.EINTR(慢系統調用:可能永遠阻塞的系統調用,例如:socket)
  • 因為過多的子進程并發開始,同時結束,會并發的發出結束的信號,父進程的 signal 一瞬間接收過多的信號,導致了有的信號丟失,這種情況還是會遺留一些僵尸進程。這個時候就需要寫一個handle信號的方法。采用waitpid的os.WHOHANG選項,進行死循環。以確保獲取到所有 signal
  • OSError 因為waitpid的os.WNOHANG選項,不會阻塞,但是如果沒有子進程退出,會拋出OSError,需要 catch 到這個異常,保證父進程接收到了每個子進程的結束信息,從而保證沒有僵尸進程。
  • waitpid()函數的options選項:os.WNOHANG - 如果沒有子進程退出,則不阻塞waitpid()調用os.WCONTINUED - 如果子進程從stop狀態變為繼續執行,則返回進程自前一次報告以來的信息。os.WUNTRACED - 如果子進程被停止過而且其狀態信息還沒有報告過,則報告子進程的信息。

    最后

    該非阻塞 Server 是通過操作系統級別的 fork 實現的,用到了多進程和信號機制。

    因為多進程解決非阻塞的問題,很好理解,但是十分消耗計算機資源的,后面會介紹更加輕量級的——利用事件循環實現非阻塞 Server。

    挖個坑~

    參考

    • 什么是孤兒進程、僵尸進程
    • linux 如何清理僵尸進程
    • 什么是pid、ppid
    • ulimit 命令

    總結

    以上是生活随笔為你收集整理的[Python]再学 socket 之非阻塞 Server的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。