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 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 เป็นส่วนสำคัญของกรอบงานเว็บ Python เกือบทุกแบบ

ในบทความนี้ เรายังไม่ได้เข้าใกล้วิธีที่ WSGI จัดการกับการอัปโหลดไฟล์ เนื่องจากสิ่งนี้ถือได้ว่าเป็นฟีเจอร์ที่ "ล้ำหน้ากว่า" ซึ่งไม่เหมาะกับบทความแนะนำ หากคุณต้องการทราบข้อมูลเพิ่มเติม โปรดดูส่วน PEP-3333 ที่กล่าวถึงการจัดการไฟล์

ฉันหวังว่าบทความนี้จะมีประโยชน์ในการช่วยสร้างความเข้าใจที่ดีขึ้นว่า Python พูดคุยกับเว็บเซิร์ฟเวอร์อย่างไร และช่วยให้นักพัฒนาสามารถใช้อินเทอร์เฟซนี้ในรูปแบบที่น่าสนใจและสร้างสรรค์

รับทราบ

ฉันขอขอบคุณบรรณาธิการ Nick McCrea ที่ช่วยฉันในบทความนี้ เนื่องจากงานของเขา ข้อความต้นฉบับจึงชัดเจนขึ้นมาก และข้อผิดพลาดหลายประการไม่ได้แก้ไข