[Python]再学 socket 之非阻塞 Server
再學(xué) socket 之非阻塞 Server
本文是基于 python2.7 實(shí)現(xiàn),運(yùn)行于 Mac 系統(tǒng)下
本篇文章是上一篇初探 socket 的續(xù)集,
上一篇文章介紹了:如何建立起一個(gè)基本的 socket 連接、TCP 和 UDP 的概念、socket 常用參數(shù)和方法
Socket 是用來通信、傳輸數(shù)據(jù)的對象,上一篇已經(jīng)研究了如果進(jìn)行基本的通行和傳輸數(shù)據(jù)。因?yàn)?#xff0c;在這個(gè)互
聯(lián)網(wǎng)爆發(fā)的時(shí)代,做為 Server 的 socket 要同時(shí)接收很多的請求。
通過閱讀:地址,強(qiáng)烈推薦閱讀原文。
整理了下面的文字,如何:創(chuàng)建一個(gè) 非阻塞的 server。
一、阻塞 Server
- 阻塞 Server 示例
- 為什么會(huì)出現(xiàn)阻塞
1.1 阻塞 Server 示例
下面就通過C/S模型,展示阻塞狀態(tài):
- 接收其它 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 阻塞的時(shí),允許掛起幾個(gè)連接。便于可以處理時(shí)直接從該隊(duì)列中取得連接,減少建立連接的時(shí)間
- 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):"""發(fā)送請求"""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) # 打印從服務(wù)器接收回來的數(shù)據(jù)if __name__ == '__main__':client()打開三個(gè)終端,先運(yùn)行 Server,在另外兩個(gè)終端運(yùn)行 Client(分別起名為client1、client2),會(huì)發(fā)現(xiàn)
服務(wù)器先接收 client1 的數(shù)據(jù),然后返回響應(yīng)。再此之前 client2 一直處于等待的狀態(tài)。只有等 Server
處理完 client1 的請求后,才會(huì)接收 client2 的數(shù)據(jù)。
這樣一個(gè)個(gè)地接收請求、處理請求的 Server 就叫做 阻塞 Server。
1.2 為什么會(huì)出現(xiàn)阻塞?
因?yàn)榉?wù)器處理請求是需要消耗時(shí)間的,正如我上面的阻塞 Server 代碼中的time.sleep(10),用于模擬
服務(wù)器處理請求消耗的時(shí)間。
在處理完上一個(gè)請求(返回給 Client 數(shù)據(jù))的這段時(shí)間中,服務(wù)器無法處理其它的請求,只能讓其它的 Client 等待。這樣的效率是
極其低下的,所以下面就介紹如何創(chuàng)建一個(gè)非阻塞的 Server
二、非阻塞 Server
- 需要知道的一些基本概念
- 非阻塞 Server 示例(多進(jìn)程)
后面會(huì)用多進(jìn)程實(shí)現(xiàn) 非阻塞socket,在此之前需要了解一些基本知識和概念,便于理解后面的代碼。
2.1 需要知道的一些基本概念
- Socket 處理請求的過程
- 進(jìn)程
- 文件描述符
- 如何查看進(jìn)程和用戶資源
2.1.1 Socket 處理請求的過程
參照上面寫的阻塞 Server 的代碼,可以看出:服務(wù)器端的socket對象,listen_socket 從不和客戶端交換數(shù)據(jù)。它只會(huì)通過accept方法接受連接。然后,創(chuàng)建一個(gè)新的socket對象,client_connection用于和客戶端通信。
所以,服務(wù)器端的socket 分為:接受請求的socket(listen_socket) 和 與客戶端傳輸數(shù)據(jù)的socket(client_connection)。
正如上面說到的,真正阻塞地方是:與客戶端傳輸數(shù)據(jù)的socket(client_connection) 需要等待處理請求的結(jié)果,然后返還給客戶端,結(jié)束這次通信,才能處理后面的請求。
2.1.2 進(jìn)程
存在硬盤中的叫做‘程序’(*.py),當(dāng)程序運(yùn)行加載到內(nèi)存中的時(shí)候叫做‘進(jìn)程’。系統(tǒng)會(huì)分配給每個(gè)進(jìn)程一個(gè)唯一 ID,
這個(gè) ID 叫做:PID ,進(jìn)程還分為父進(jìn)程和子進(jìn)程,父進(jìn)程(PPID)創(chuàng)建子進(jìn)程(PID)。關(guān)系如下圖:
可以通過ps命令來查看進(jìn)程的信息:每天一個(gè)linux命令(41):ps命令
需要注意:
- 子進(jìn)程一定要關(guān)閉
- 子進(jìn)程關(guān)閉一定要通知父進(jìn)程,否則會(huì)出現(xiàn)‘僵尸進(jìn)程’
- 一定要先結(jié)束父進(jìn)程,再結(jié)束子進(jìn)程,否則會(huì)出現(xiàn)‘孤兒進(jìn)程’
僵尸進(jìn)程:一個(gè)進(jìn)程使用fork創(chuàng)建子進(jìn)程,如果子進(jìn)程退出,而父進(jìn)程并沒有調(diào)用wait或waitpid獲取子進(jìn)程的狀態(tài)信息,那么子進(jìn)程的進(jìn)程描述符仍然保存在系統(tǒng)中。這種進(jìn)程稱之為僵尸進(jìn)程。(系統(tǒng)所能使用的進(jìn)程號是有限制的,如果大量的產(chǎn)生僵死進(jìn)程,將因?yàn)闆]有可用的進(jìn)程號而導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程。則會(huì)拋出OSError: [Errno 35] Resource temporarily unavailable異常)
孤兒進(jìn)程:一個(gè)父進(jìn)程退出,而它的一個(gè)或多個(gè)子進(jìn)程還在運(yùn)行,那么那些子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號為1)所收養(yǎng),并由init進(jìn)程對它們完成狀態(tài)收集工作。(沒有危害)
2.1.3 文件描述符
在UNIX中一切都是一個(gè)文件,當(dāng)操作系統(tǒng)打開一存在的個(gè)文件的時(shí)候,便會(huì)返回一個(gè)‘文件描述符’,進(jìn)程通過
操作該文件操作符,從而實(shí)現(xiàn)對文件的讀寫。Socket 是一個(gè)操作文件描述符的進(jìn)程,Python 的 socket
模塊提供了這些操作系統(tǒng)底層的實(shí)現(xiàn)。我們只需要調(diào)用socket對象的方式就可以了。
需要注意:
- 文件描述符的回收機(jī)制是采用引用計(jì)數(shù)方式
- 每次操作完文件描述符需要調(diào)用close()方法,關(guān)閉文件描述符。道理和進(jìn)程一樣,操作系統(tǒng)都會(huì)最多可創(chuàng)建的文本描述符的限制,如果一直不關(guān)閉文本描述符的話,導(dǎo)致數(shù)量太多無法創(chuàng)建新的,就會(huì)拋出OSError: [Errno 24] Too many open file異常。
2.1.4 如何查看進(jìn)程和用戶資源極限
計(jì)算機(jī)的計(jì)算和存儲(chǔ)能力都是有限的,統(tǒng)稱為計(jì)算機(jī)資源。
上面說了進(jìn)程和文件描述符號都是有個(gè)最大數(shù)量(極限),下面就是用于查看和修改用戶資源限制的命令——ulimit。
-a 列出所有當(dāng)前資源極限。 -c 以 512 字節(jié)塊為單位,指定核心轉(zhuǎn)儲(chǔ)的大小。 -d 以 K 字節(jié)為單位指定數(shù)據(jù)區(qū)域的大小。 -f 使用 Limit 參數(shù)時(shí)設(shè)定文件大小極限(以塊為單位),或者在未指定參數(shù)時(shí)報(bào)告文件大小極限。缺省值為 -f 標(biāo)志。 -H 指定設(shè)置某個(gè)給定資源的硬極限。如果用戶擁有 root 用戶權(quán)限,可以增大硬極限。任何用戶均可減少硬極限。 -m 以 K 字節(jié)為單位指定物理內(nèi)存的大小(駐留集合大小)。系統(tǒng)未強(qiáng)制實(shí)施此限制。 -n 指定一個(gè)進(jìn)程可以擁有的文件描述符數(shù)的極限。 -r 指定對進(jìn)程擁有線程數(shù)的限制。 -s 以 K 字節(jié)為單位指定堆棧的大小。 -S 指定為給定的資源設(shè)置軟極限。軟極限可增大到硬極限的值。如果 -H 和 -S 標(biāo)志均未指定,極限適用于以上二者。 -t 指定每個(gè)進(jìn)程所使用的秒數(shù)。 -u 指定對用戶可以創(chuàng)建的進(jìn)程數(shù)的限制。常用命令如下:
- ulimit -a:查看
- ulimit -n:設(shè)置一個(gè)進(jìn)程可擁有文件描述符數(shù)量
- ulimit -u:最多可以創(chuàng)建多少個(gè)進(jìn)程
2.2 Fork 方式的非阻塞 Server
采用 fork 的方式實(shí)現(xiàn)非阻塞 Server,主要原理就是當(dāng) socket 接受到(accept)一個(gè)請求,就 fork 出一個(gè)子進(jìn)程
去處理這個(gè)請求。然后父進(jìn)程繼續(xù)接受請求。從而實(shí)現(xiàn)并發(fā)的處理請求,不需要處理上一個(gè)請求才能接受、處理下一個(gè)請求。
如閱讀代碼時(shí)出現(xiàn)的問題,可以參考下面的關(guān)鍵字:
最后
該非阻塞 Server 是通過操作系統(tǒng)級別的 fork 實(shí)現(xiàn)的,用到了多進(jìn)程和信號機(jī)制。
因?yàn)槎噙M(jìn)程解決非阻塞的問題,很好理解,但是十分消耗計(jì)算機(jī)資源的,后面會(huì)介紹更加輕量級的——利用事件循環(huán)實(shí)現(xiàn)非阻塞 Server。
挖個(gè)坑~
參考
- 什么是孤兒進(jìn)程、僵尸進(jìn)程
- linux 如何清理僵尸進(jìn)程
- 什么是pid、ppid
- ulimit 命令
總結(jié)
以上是生活随笔為你收集整理的[Python]再学 socket 之非阻塞 Server的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读《Oracle DBA工作笔记》知识点
- 下一篇: python 爬虫之爬取大街网(思路)