WSGI: อินเทอร์เฟซเซิร์ฟเวอร์แอปพลิเคชันสำหรับ Python
เผยแพร่แล้ว: 2022-03-11ในปี 1993 เว็บยังอยู่ในช่วงเริ่มต้น มีผู้ใช้ประมาณ 14 ล้านคนและเว็บไซต์ 100 แห่ง เพจเป็นแบบคงที่แต่มีความจำเป็นอยู่แล้วในการผลิตเนื้อหาแบบไดนามิก เช่น ข่าวสารและข้อมูลที่เป็นปัจจุบัน ในการตอบสนองต่อสิ่งนี้ Rob McCool และผู้มีส่วนร่วมอื่น ๆ ได้ใช้ Common Gateway Interface (CGI) ในเว็บเซิร์ฟเวอร์ HTTPd ของ National Center for Supercomputing Applications (NCSA) (บรรพบุรุษของ Apache) นี่เป็นเว็บเซิร์ฟเวอร์เครื่องแรกที่สามารถให้บริการเนื้อหาที่สร้างโดยแอปพลิเคชันแยกต่างหาก
ตั้งแต่นั้นมา จำนวนผู้ใช้บนอินเทอร์เน็ตก็เพิ่มขึ้นอย่างรวดเร็ว และเว็บไซต์แบบไดนามิกก็แพร่หลายไปทั่ว เมื่อเริ่มเรียนภาษาใหม่หรือแม้แต่เรียนรู้การเขียนโค้ดครั้งแรก นักพัฒนาก็ต้องการทราบวิธีเชื่อมโยงโค้ดของตนเข้ากับเว็บในไม่ช้า
Python บนเว็บและการเพิ่มขึ้นของ WSGI
นับตั้งแต่การสร้าง CGI มีการเปลี่ยนแปลงมากมาย แนวทาง CGI เริ่มทำไม่ได้ เนื่องจากจำเป็นต้องมีการสร้างกระบวนการใหม่ในแต่ละคำขอ ทำให้หน่วยความจำและ CPU สิ้นเปลือง แนวทางระดับต่ำอื่นๆ ปรากฏขึ้น เช่น FastCGI](http://www.fastcgi.com/) (1996) และ mod_python (2000) ซึ่งมีอินเทอร์เฟซที่แตกต่างกันระหว่างเฟรมเวิร์กเว็บ Python และเว็บเซิร์ฟเวอร์ เมื่อแนวทางต่างๆ เพิ่มขึ้น ทางเลือกของเฟรมเวิร์กของนักพัฒนาก็จบลงด้วยการจำกัดทางเลือกของเว็บเซิร์ฟเวอร์และในทางกลับกัน
เพื่อแก้ไขปัญหานี้ ในปี 2003 Phillip J. Eby ได้เสนอ PEP-0333 ซึ่งเป็น Python Web Server Gateway Interface (WSGI) แนวคิดคือการจัดเตรียมอินเทอร์เฟซสากลระดับสูงระหว่างแอปพลิเคชัน Python และเว็บเซิร์ฟเวอร์
ในปี 2546 PEP-3333 ได้อัปเดตอินเทอร์เฟซ WSGI เพื่อเพิ่มการรองรับ Python 3 ปัจจุบัน Python framework เกือบทั้งหมดใช้ WSGI เป็นสื่อกลางในการสื่อสารกับเว็บเซิร์ฟเวอร์ นี่คือวิธีที่ Django, Flask และเฟรมเวิร์กยอดนิยมอื่น ๆ ทำ
บทความนี้มีจุดมุ่งหมายเพื่อให้ผู้อ่านเข้าใจถึงวิธีการทำงานของ WSGI และอนุญาตให้ผู้อ่านสร้างแอปพลิเคชันหรือเซิร์ฟเวอร์ WSGI อย่างง่าย มันไม่ได้หมายความว่าจะละเอียดถี่ถ้วน และนักพัฒนาที่ตั้งใจจะใช้เซิร์ฟเวอร์หรือแอปพลิเคชันที่พร้อมสำหรับการผลิต ควรตรวจสอบข้อกำหนด WSGI อย่างละเอียดยิ่งขึ้น
อินเทอร์เฟซ Python WSGI
WSGI ระบุกฎง่ายๆ ที่เซิร์ฟเวอร์และแอปพลิเคชันต้องปฏิบัติตาม เริ่มต้นด้วยการทบทวนรูปแบบโดยรวมนี้
ส่วนต่อประสานแอปพลิเคชัน
ใน Python 3.5 อินเทอร์เฟซของแอปพลิเคชันจะเป็นดังนี้:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
ใน Python 2.7 อินเทอร์เฟซนี้จะไม่แตกต่างกันมากนัก การเปลี่ยนแปลงเพียงอย่างเดียวก็คือร่างกายจะแสดงด้วยวัตถุ str
แทนที่จะเป็นหนึ่ง bytes
แม้ว่าเราจะใช้ฟังก์ชันในกรณีนี้ แต่ callable อะไรก็ได้ กฎสำหรับวัตถุแอปพลิเคชันที่นี่คือ:
- ต้องเป็น callable ที่มีพารามิเตอร์
environ
และstart_response
- ต้องโทรกลับ
start_response
ก่อนส่งเนื้อความ - ต้องส่งคืน iterable พร้อมชิ้นส่วนของเนื้อหาเอกสาร
อีกตัวอย่างหนึ่งของวัตถุที่เป็นไปตามกฎเหล่านี้และจะทำให้เกิดผลเช่นเดียวกันคือ:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
อินเทอร์เฟซเซิร์ฟเวอร์
เซิร์ฟเวอร์ WSGI อาจเชื่อมต่อกับแอปพลิเคชันนี้ ::
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()
ดังที่คุณอาจสังเกตเห็นว่า start_response
callable ส่งคืนการ write
callable ซึ่งแอปพลิเคชันอาจใช้เพื่อส่งข้อมูลกลับไปยังไคลเอนต์ แต่ไม่ได้ใช้โดยตัวอย่างโค้ดแอปพลิเคชันของเรา อินเทอร์เฟซการ write
นี้เลิกใช้แล้ว และตอนนี้เราสามารถเพิกเฉยได้ จะมีการกล่าวถึงสั้น ๆ ในบทความต่อไป
ลักษณะพิเศษอีกอย่างของความรับผิดชอบของเซิร์ฟเวอร์คือการเรียกวิธีการ close
ทางเลือกในตัววนซ้ำการตอบสนอง หากมี ดังที่ได้กล่าวไว้ในบทความของ Graham Dumpleton ที่นี่ เป็นคุณลักษณะที่มักถูกมองข้ามของ WSGI การเรียกใช้เมธอดนี้ หากมี จะทำให้แอปพลิเคชันสามารถปล่อยทรัพยากรใดๆ ที่อาจยังคงมีอยู่
อาร์กิวเมนต์ environ
ของ Application Callable
พารามิเตอร์ environ
ควรเป็นวัตถุพจนานุกรม ใช้เพื่อส่งคำขอและข้อมูลเซิร์ฟเวอร์ไปยังแอปพลิเคชัน เช่นเดียวกับที่ CGI ทำ อันที่จริง ตัวแปรสภาพแวดล้อม CGI ทั้งหมดนั้นใช้ได้ใน WSGI และเซิร์ฟเวอร์ควรส่งผ่านทุกตัวแปรที่ใช้กับแอปพลิเคชัน
แม้ว่าจะมีคีย์เสริมมากมายที่ส่งผ่านได้ แต่ก็มีหลายคีย์ที่บังคับ ยกตัวอย่างคำขอ GET
ต่อไปนี้:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
คีย์เหล่านี้เป็นคีย์ที่เซิร์ฟเวอร์ ต้อง จัดเตรียม และค่าที่ต้องใช้:
สำคัญ | ค่า | ความคิดเห็น |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | ขึ้นอยู่กับการตั้งค่าเซิร์ฟเวอร์ |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | ขึ้นอยู่กับการตั้งค่าเซิร์ฟเวอร์ |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | ลูกค้าระบุส่วนหัว HTTP | |
wsgi.version | (1, 0) | tuple พร้อมเวอร์ชัน WSGI |
wsgi.url_scheme | "http" | |
wsgi.input | วัตถุคล้ายไฟล์ | |
wsgi.errors | วัตถุคล้ายไฟล์ | |
wsgi.multithread | False | เป็น True หากเซิร์ฟเวอร์เป็นแบบมัลติเธรด |
wsgi.multiprocess | False | True หากเซิร์ฟเวอร์เรียกใช้หลายกระบวนการ |
wsgi.run_once | False | True หากเซิร์ฟเวอร์คาดหวังให้สคริปต์นี้ทำงานเพียงครั้งเดียว (เช่น: ในสภาพแวดล้อม CGI) |
ข้อยกเว้นของกฎนี้คือถ้าหนึ่งในคีย์เหล่านี้ว่างเปล่า (เช่น CONTENT_TYPE
ในตารางด้านบน) ก็สามารถละเว้นจากพจนานุกรมและจะถือว่าสอดคล้องกับสตริงว่าง
wsgi.input
และ wsgi.errors
คีย์ environ
ส่วนใหญ่ตรงไปตรงมา แต่สองคีย์ควรได้รับการชี้แจงเพิ่มเติมเล็กน้อย: wsgi.input
ซึ่งต้องมีสตรีมที่มีเนื้อหาคำขอจากไคลเอ็นต์ และ wsgi.errors
ซึ่งแอปพลิเคชันรายงานข้อผิดพลาดที่พบ ข้อผิดพลาดที่ส่งจากแอปพลิเคชันไปยัง wsgi.errors
โดยทั่วไปจะถูกส่งไปยังบันทึกข้อผิดพลาดของเซิร์ฟเวอร์
คีย์ทั้งสองนี้ต้องมีวัตถุที่เหมือนไฟล์ นั่นคืออ็อบเจ็กต์ที่มีอินเทอร์เฟซสำหรับอ่านหรือเขียนเป็นสตรีม เช่นเดียวกับอ็อบเจ็กต์ที่เราได้รับเมื่อเราเปิดไฟล์หรือซ็อกเก็ตใน Python นี้อาจดูเหมือนยุ่งยากในตอนแรก แต่โชคดีที่ Python ให้เครื่องมือที่ดีในการจัดการกับสิ่งนี้
อันดับแรก เรากำลังพูดถึงสตรีมประเภทใด ตามคำจำกัดความของ WSGI wsgi.input
และ wsgi.errors
ต้องจัดการอ็อบเจ็กต์ bytes
ใน Python 3 และอ็อบเจ็กต์ str
ใน Python 2 ไม่ว่าในกรณีใด หากเราต้องการใช้บัฟเฟอร์ในหน่วยความจำเพื่อส่งผ่านหรือรับข้อมูลผ่าน WSGI อินเทอร์เฟซ เราสามารถใช้คลาส io.BytesIO

ตัวอย่างเช่น หากเรากำลังเขียนเซิร์ฟเวอร์ WSGI เราสามารถจัดเตรียมเนื้อหาคำขอให้กับแอปพลิเคชันดังนี้:
- สำหรับ Python 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- สำหรับ Python 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
ด้านแอปพลิเคชัน หากเราต้องการเปลี่ยนอินพุตสตรีมที่เราได้รับเป็นสตริง เราต้องการเขียนดังนี้:
- สำหรับ Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
- สำหรับ Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
ควรใช้สตรีม wsgi.errors
เพื่อรายงานข้อผิดพลาดของแอปพลิเคชันไปยังเซิร์ฟเวอร์ และบรรทัดควรลงท้ายด้วย \n
เว็บเซิร์ฟเวอร์ควรดูแลการแปลงบรรทัดสิ้นสุดที่แตกต่างกันตามระบบ
อาร์กิวเมนต์ start_response
ของ Application Callable
อาร์กิวเมนต์ start_response
ต้องเป็นอาร์กิวเมนต์ที่เรียกได้โดยมีอาร์กิวเมนต์ที่จำเป็นสองอาร์กิวเมนต์ คือ status
และ headers
และอาร์กิวเมนต์ทางเลือกหนึ่งอาร์กิวเมนต์ exc_info
แอปพลิเคชันจะต้องถูกเรียกก่อนที่ส่วนใดส่วนหนึ่งของร่างกายจะถูกส่งกลับไปยังเว็บเซิร์ฟเวอร์
ในตัวอย่างการใช้งานครั้งแรกที่ตอนต้นของบทความนี้ เราได้ส่งคืนเนื้อหาของการตอบสนองเป็นรายการ ดังนั้นเราจึงไม่สามารถควบคุมได้ว่าเมื่อใดที่รายการจะถูกทำซ้ำ ด้วยเหตุนี้ เราจึงต้องเรียก start_response
ก่อนส่งคืนรายการ
ในอันที่สอง เราได้เรียก start_response
ก่อนที่จะให้ส่วนแรก (และในกรณีนี้เท่านั้น) ของเนื้อหาการตอบสนอง ทั้งสองวิธีใช้ได้ภายในข้อกำหนด WSGI
จากฝั่งเซิร์ฟเวอร์ของเว็บ การเรียก start_response
ไม่ควรส่งส่วนหัวไปยังไคลเอ็นต์ แต่ให้หน่วงเวลาจนกว่าจะมีไบต์สตริงที่ไม่ว่างอย่างน้อยหนึ่งรายการในเนื้อหาการตอบกลับเพื่อส่งกลับไปยังไคลเอ็นต์ สถาปัตยกรรมนี้ช่วยให้สามารถรายงานข้อผิดพลาดได้อย่างถูกต้องจนถึงช่วงสุดท้ายที่เป็นไปได้ของการดำเนินการของแอปพลิเคชัน
status
อาร์กิวเมนต์ของ start_response
อาร์กิวเมนต์ status
ที่ส่งไปยังการเรียกกลับ start_response
ต้องเป็นสตริงที่ประกอบด้วยรหัสสถานะ HTTP และคำอธิบาย โดยคั่นด้วยช่องว่างเดียว ตัวอย่างที่ถูกต้องคือ: '200 OK'
หรือ '404 Not Found'
headers
อาร์กิวเมนต์ของ start_response
อาร์กิวเมนต์ headers
ที่ส่งไปยัง start_response
callback ต้องเป็น list
Python ของ tuple
s โดยที่ tuple แต่ละรายการประกอบด้วย (header_name, header_value)
ทั้งชื่อและค่าของส่วนหัวแต่ละอันต้องเป็นสตริง (โดยไม่คำนึงถึงเวอร์ชันของ Python) นี่เป็นตัวอย่างที่พบไม่บ่อยในประเภทที่มีความสำคัญ เนื่องจากจำเป็นอย่างยิ่งในข้อกำหนด WSGI
นี่คือตัวอย่างที่ถูกต้องของลักษณะของอาร์กิวเมนต์ header
:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
ส่วนหัว HTTP นั้นไม่คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ และหากเรากำลังเขียนเว็บเซิร์ฟเวอร์ที่สอดคล้องกับ WSGI นั่นเป็นสิ่งที่ควรทราบเมื่อตรวจสอบส่วนหัวเหล่านี้ นอกจากนี้ รายการส่วนหัวของแอปพลิเคชันไม่ควรจะละเอียดถี่ถ้วน เป็นความรับผิดชอบของเซิร์ฟเวอร์ที่จะต้องตรวจสอบให้แน่ใจว่ามีส่วนหัว HTTP ที่จำเป็นทั้งหมดก่อนที่จะส่งการตอบกลับไปยังไคลเอนต์ โดยกรอกส่วนหัวที่แอปพลิเคชันไม่ได้ให้มา
อาร์กิวเมนต์ exc_info
ของ start_response
การเรียกกลับ start_response
ควรสนับสนุนอาร์กิวเมนต์ที่สาม exc_info
ซึ่งใช้สำหรับการจัดการข้อผิดพลาด การใช้และการใช้งานอาร์กิวเมนต์นี้อย่างถูกต้องมีความสำคัญสูงสุดสำหรับเว็บเซิร์ฟเวอร์และแอปพลิเคชันที่ใช้งานจริง แต่อยู่นอกขอบเขตของบทความนี้
ข้อมูลเพิ่มเติมสามารถดูได้ในข้อกำหนด WSGI ที่นี่
start_response
Return Value – การ write
Callback
เพื่อจุดประสงค์ด้านความเข้ากันได้แบบย้อนหลัง เว็บเซิร์ฟเวอร์ที่ใช้ WSGI ควรส่งคืนการ write
ที่เรียกได้ การเรียกกลับนี้ควรอนุญาตให้แอปพลิเคชันเขียนข้อมูลการตอบสนองของร่างกายโดยตรงกลับไปยังไคลเอนต์ แทนที่จะส่งไปยังเซิร์ฟเวอร์ผ่านตัววนซ้ำ
แม้ว่าจะมีอยู่ แต่นี่เป็นอินเทอร์เฟซที่เลิกใช้แล้วและแอปพลิเคชันใหม่ ๆ ควรละเว้นจากการใช้งาน
การสร้างเนื้อหาการตอบสนอง
แอปพลิเคชันที่ใช้ WSGI ควรสร้างเนื้อหาการตอบสนองโดยการส่งคืนอ็อบเจ็กต์ที่ทำซ้ำได้ สำหรับแอปพลิเคชันส่วนใหญ่ เนื้อหาการตอบสนองไม่ใหญ่มากและพอดีกับหน่วยความจำของเซิร์ฟเวอร์ได้อย่างง่ายดาย ในกรณีนั้น วิธีที่มีประสิทธิภาพมากที่สุดในการส่งทั้งหมดคือทั้งหมดในครั้งเดียว โดยสามารถทำซ้ำได้เพียงองค์ประกอบเดียว ในกรณีพิเศษที่ไม่สามารถโหลดทั้งเนื้อความลงในหน่วยความจำได้ แอปพลิเคชันอาจส่งคืนส่วนต่อส่วนผ่านอินเทอร์เฟซที่ทำซ้ำได้นี้
มีความแตกต่างเพียงเล็กน้อยระหว่าง WSGI ของ Python 2 และ Python 3: ใน Python 3 เนื้อหาการตอบสนองจะแสดงด้วยอ็อบเจ็กต์ bytes
ใน Python 2 ชนิดที่ถูกต้องสำหรับสิ่งนี้คือ str
การแปลงสตริง UTF-8 เป็น bytes
หรือ str
เป็นเรื่องง่าย:
- หลาม 3.5:
body = 'unicode stuff'.encode('utf-8')
- หลาม 2.7:
body = u'unicode stuff'.encode('utf-8')
หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการจัดการ Unicode และ bytestring ของ Python 2 มีบทช่วยสอนที่ดีบน YouTube
เว็บเซิร์ฟเวอร์ที่ใช้ WSGI ควรสนับสนุนการเรียกกลับการ write
เพื่อความเข้ากันได้แบบย้อนหลัง ดังที่อธิบายไว้ข้างต้น
การทดสอบแอปพลิเคชันของคุณโดยไม่มีเว็บเซิร์ฟเวอร์
ด้วยความเข้าใจในอินเทอร์เฟซที่เรียบง่ายนี้ เราจึงสามารถสร้างสคริปต์เพื่อทดสอบแอปพลิเคชันของเราได้อย่างง่ายดายโดยไม่จำเป็นต้องเริ่มต้นเซิร์ฟเวอร์
ใช้สคริปต์ขนาดเล็กนี้ ตัวอย่างเช่น:
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}
ด้วยวิธีนี้ เราอาจเริ่มต้นข้อมูลการทดสอบและจำลองโมดูลในแอปของเรา และทำการเรียก GET
เพื่อทดสอบว่าตอบสนองตามนั้นหรือไม่ เราจะเห็นได้ว่าไม่ใช่เว็บเซิร์ฟเวอร์จริง แต่เชื่อมต่อกับแอปของเราในลักษณะที่เทียบเท่ากันโดยให้แอปพลิเคชันมีการเรียกกลับ start_response
และพจนานุกรมที่มีตัวแปรสภาพแวดล้อมของเรา ที่ส่วนท้ายของคำขอ จะใช้ตัววนซ้ำเนื้อหาการตอบสนองและส่งกลับสตริงที่มีเนื้อหาทั้งหมด คุณสามารถสร้างวิธีการที่คล้ายกัน (หรือวิธีทั่วไป) สำหรับคำขอ HTTP ประเภทต่างๆ
สรุป
ในบทความนี้ เรายังไม่ได้เข้าใกล้วิธีที่ WSGI จัดการกับการอัปโหลดไฟล์ เนื่องจากสิ่งนี้ถือได้ว่าเป็นฟีเจอร์ที่ "ล้ำหน้ากว่า" ซึ่งไม่เหมาะกับบทความแนะนำ หากคุณต้องการทราบข้อมูลเพิ่มเติม โปรดดูส่วน PEP-3333 ที่กล่าวถึงการจัดการไฟล์
ฉันหวังว่าบทความนี้จะมีประโยชน์ในการช่วยสร้างความเข้าใจที่ดีขึ้นว่า Python พูดคุยกับเว็บเซิร์ฟเวอร์อย่างไร และช่วยให้นักพัฒนาสามารถใช้อินเทอร์เฟซนี้ในรูปแบบที่น่าสนใจและสร้างสรรค์
รับทราบ
ฉันขอขอบคุณบรรณาธิการ Nick McCrea ที่ช่วยฉันในบทความนี้ เนื่องจากงานของเขา ข้อความต้นฉบับจึงชัดเจนขึ้นมาก และข้อผิดพลาดหลายประการไม่ได้แก้ไข