5 طرق لجعل خادم بيثون على التوت بي الجزء 2

مرحبا يا هبر.

اليوم سوف نستمر في دراسة إمكانات شبكة Raspberry Pi ، أو بالأحرى تنفيذها في بيثون. في الجزء الأول ، درسنا الوظائف الأساسية لأبسط خادم ويب يعمل على Raspberry Pi. سنذهب الآن إلى أبعد من ذلك ، وسنفكر في عدة طرق لجعل خادمنا تفاعليًا.



تم تصميم المقال للمبتدئين.

قبل أن تبدأ بضع ملاحظات.

بادئ ذي بدء ، أنا شخصياً شككت فيما إذا كان الأمر يستحق القيام بتتمة وتوقعت تدفقًا أكبر للنقد ودرجات تقييم منخفضة ، ولكن كما أظهر الاستطلاع في الجزء الأول ، وجد 85٪ من القراء أن المعلومات المقدمة هناك مفيدة. أنا أفهم أن بعض المقالات المؤيدة للدمى مزعجة ، لكنها بدأت جميعها مرة واحدة ، لذلك عليك الانتظار.

ثانياً ، سأكتب عن البرمجة ، وليس عن الإدارة. لذلك لن يتم النظر هنا في مشكلات تكوين Raspbian ، configs ، VPN ، الأمان وأشياء أخرى. على الرغم من أن هذا مهم أيضًا ، إلا أنه لا يمكن احتضان الضخم. سيكون فقط حول بايثون ، وكيفية صنع خادم عليه.

يمكن لأولئك غير المهتمين النقر فوق الزر "السابق" في المستعرض الآن وعدم إضاعة وقتهم الثمين ؛)

وسوف نبدأ.

اسمحوا لي أن أذكرك بأننا انتهينا في الجزء السابق من إطلاق خادم ويب بسيط على Raspberry Pi يعرض صفحة ثابتة:

صورة

الآن سنذهب إلى أبعد من ذلك ونجعل خادمنا تفاعليًا ، أضف تحكم LED إلى صفحة الويب. بالطبع ، بدلاً من LED ، يمكن أن يكون هناك أي جهاز آخر يمكن التحكم فيه بواسطة GPIO ، لكن مع LED ، يكون من الأسهل إجراء تجربة.

تدريب


لن أصف كيفية توصيل LED بـ Raspberry Pi ، حيث يمكن لأي شخص العثور عليه في Google خلال 5 دقائق. سنكتب العديد من الوظائف لاستخدام GPIO في وقت واحد ، ثم نضيفها إلى خادمنا.

try: import RPi.GPIO as GPIO except ModuleNotFoundError: pass led_pin = 21 def raspberrypi_init(): try: GPIO.setmode(GPIO.BCM) GPIO.setup(led_pin, GPIO.OUT) except: pass def rasperrypi_pinout(pin: int, value: bool): print("LED ON" if value else "LED OFF") try: GPIO.output(pin, value) except: pass def rasperrypi_cleanup(): try: GPIO.cleanup() except: pass 

كما ترون ، كل وظيفة مكالمة GPIO هي "ملفوفة" في كتلة محاولة التجريب. لماذا يتم ذلك؟ يتيح لك ذلك تصحيح الخادم على أي جهاز كمبيوتر ، بما في ذلك Windows ، وهو مناسب تمامًا. الآن يمكننا إدراج هذه الوظائف في رمز خادم الويب.

مهمتنا هي إضافة أزرار إلى صفحة الويب التي تسمح لنا بالتحكم في مؤشر LED من المستعرض. سيتم النظر في 3 طرق للتنفيذ.

الطريقة 1: خطأ


لا يمكن أن تسمى هذه الطريقة جميلة ، لكنها قصيرة وأسهل الفهم.

إنشاء خط مع صفحة HTML.

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <body> <h2>Hello from the Raspberry Pi!</h2> <p><a href="/led/on"><button class="button button_led">Led ON</button></a></p> <p><a href="/led/off"><button class="button button_led">Led OFF</button></a></p> </body> </html>''' 

هناك 3 نقاط يجب ملاحظتها هنا:

  • نحن نستخدم CSS لتحديد نمط الأزرار. لا يمكنك القيام بذلك والتوافق مع 4 أسطر فقط من تعليمات HTML البرمجية ، ولكن بعد ذلك ستبدو صفحتنا "تحيات من التسعينيات":

  • لكل زر ، نقوم بإنشاء رابط محلي مثل / led / on و / led / off
  • يعد خلط الموارد والرمز أسلوبًا سيئًا للبرمجة ، ومن الأفضل أن يتم فصل HTML بشكل منفصل عن شفرة Python. لكن هدفي هو عرض رمز عمل بسيط حيث يوجد حد أدنى لا لزوم له ، لذلك يتم حذف بعض الأشياء للبساطة. بالإضافة إلى ذلك ، يكون الأمر مناسبًا عندما يمكن ببساطة نسخ الشفرة من مقال ، دون أي ضجة مع ملفات إضافية.

لقد قمنا بالفعل بفحص الخادم في الجزء السابق ، يبقى لإضافة معالجة الخطوط "/ led / on" و "/ led / off" إليه. تم تحديث الكود بالكامل:

 from http.server import BaseHTTPRequestHandler, HTTPServer class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, Path:", self.path) if self.path == "/" or self.path.endswith("/led/on") or self.path.endswith("/led/off"): if self.path.endswith("/led/on"): rasperrypi_pinout(led_pin, True) if self.path.endswith("/led/off"): rasperrypi_pinout(led_pin, False) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def server_thread(port): server_address = ('', port) httpd = HTTPServer(server_address, ServerHandler) try: httpd.serve_forever() except KeyboardInterrupt: pass httpd.server_close() if __name__ == '__main__': port = 8000 print("Starting server at port %d" % port) raspberrypi_init() server_thread(port) rasperrypi_cleanup() 

نبدأ تشغيله ، وإذا تم كل شيء بشكل صحيح ، فيمكننا إذن التحكم في LED من خلال خادم الويب الخاص بنا:



يمكنك اختبار الخادم ليس فقط على Raspberry Pi ، ولكن أيضًا على Windows أو OSX ، في وحدة التحكم ستكون هناك رسائل LED ON و LED OFF عند النقر فوق الزر المقابل:



الآن نكتشف لماذا هذه الطريقة سيئة ، ولماذا هي "خاطئة". هذا المثال يعمل بشكل جيد ، وغالبًا ما يتم نسخه في برامج تعليمية مختلفة. ولكن هناك مشكلتان - أولاً ، من الخطأ إعادة تحميل الصفحة بأكملها عندما نريد فقط إضاءة المصباح. ولكن هذا لا يزال نصف المشكلة. المشكلة الثانية والأكثر خطورة هي أنه عندما نضغط على الزر لتشغيل المصباح ، يصبح عنوان الصفحة http://192.168.1.106:8000/led/on . تتذكر المتصفحات عادةً آخر صفحة تم فتحها ، وفي المرة التالية التي تفتح فيها المتصفح ، سيعمل الأمر الخاص بتشغيل LED مرة أخرى ، حتى لو لم نرغب في ذلك. لذلك ، سوف ننتقل إلى الطريقة الصحيحة التالية.

الطريقة 2: الحق


للقيام بكل ما هو صواب ، وضعنا وظائف تشغيل وإيقاف تشغيل مؤشر LED في طلبات منفصلة ، وسنسميها بشكل غير متزامن باستخدام Javascript. سيبدو رمز HTML للصفحة الآن كما يلي:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpGetAsync(method, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); } xmlHttp.open("GET", window.location.href + method, true); xmlHttp.send(null); } function ledOn() { console.log("Led ON..."); httpGetAsync("led/on", function(){ console.log("Done"); }); } function ledOff() { console.log("Led OFF..."); httpGetAsync("led/off", function(){ console.log("Done"); }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> </body> </html>''' 

كما ترون ، لقد تخلينا عن href ، وندعو وظائف ledOn و ledOff ، والتي بدورها تستدعي طرق الخادم المناظرة بشكل غير متزامن (هناك حاجة إلى طرق غير متزامنة حتى لا يتم حظر الصفحة حتى وصول استجابة الخادم).

الآن يبقى لإضافة معالجة الحصول على طلبات إلى الخادم:

  def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) elif self.path == "/led/on": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, True) self.wfile.write(b"OK") elif self.path == "/led/off": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, False) self.wfile.write(b"OK") else: self.send_error(404, "Page Not Found {}".format(self.path)) 

الآن ، كما ترون ، لم تعد الصفحة تعيد تحميلها عندما تحاول أن تضيء المصباح ، كل طريقة تفعل فقط ما يجب أن تفعله.

الطريقة 3: أكثر الصحيح


يبدو أن كل شيء يعمل بالفعل. لكن بالطبع ، يمكن تحسين الكود أعلاه. الحقيقة هي أننا نستخدم طلبات GET للتحكم في LED. هذا يوفر لنا مساحة في الكود ، لكن من الناحية المنهجية هذا غير صحيح تمامًا - طلبات GET مصممة لقراءة البيانات من الخادم ، ويمكن تخزينها مؤقتًا بواسطة المستعرض ، ويجب ألا تستخدم عمومًا لتعديل البيانات. الطريقة الصحيحة هي استخدام POST (للراغبين في التفاصيل ، مزيد من التفاصيل هنا ).

سنقوم بتغيير مكالمات HTML من get to post ، ولكن في الوقت نفسه ، نظرًا لأن الكود الخاص بنا غير متزامن ، فسنعرض حالة انتظار استجابة الخادم وعرض نتائج العمل. لن يكون هذا ملحوظًا بالنسبة لشبكة محلية ، لكن بالنسبة إلى الاتصال البطيء ، فهو مناسب للغاية. لجعلها أكثر إثارة للاهتمام ، سوف نستخدم JSON لتمرير المعلمات.

الإصدار النهائي يشبه هذا:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpPostAsync(method, params, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); else callback(`Error ${xmlHttp.status}`) } xmlHttp.open("POST", window.location.href + method, true); xmlHttp.setRequestHeader("Content-Type", "application/json"); xmlHttp.send(params); } function ledOn() { document.getElementById("textstatus").textContent = "Making LED on..."; httpPostAsync("led", JSON.stringify({ "on": true }), function(resp) { document.getElementById("textstatus").textContent = `Led ON: ${resp}`; }); } function ledOff() { document.getElementById("textstatus").textContent = "Making LED off..."; httpPostAsync("led", JSON.stringify({ "on": false }), function(resp) { document.getElementById("textstatus").textContent = `Led OFF: ${resp}`; }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> <span id="textstatus">Status: Ready</span> </body> </html>''' 

إضافة دعم لطلبات GET و POST إلى الخادم:

 import json class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def do_POST(self): content_length = int(self.headers['Content-Length']) body = self.rfile.read(content_length) try: print("POST request, path:", self.path, "body:", body.decode('utf-8')) if self.path == "/led": data_dict = json.loads(body.decode('utf-8')) if 'on' in data_dict: rasperrypi_pinout(led_pin, data_dict['on']) self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"OK") else: self.send_response(400, 'Bad Request: Method does not exist') self.send_header('Content-Type', 'application/json') self.end_headers() except Exception as err: print("do_POST exception: %s" % str(err)) 

كما ترون ، نستخدم الآن وظيفة واحدة تقودها ، والتي يتم فيها تمرير المعلمة "on" باستخدام json ، التي تقبل True أو False (عندما يتم استدعاء HTML ، تُرسل سلسلة json من النموذج {"on": true} ، على التوالي). يجدر أيضًا الانتباه إلى تجربة المحاولة - حيث يمنع ذلك الخادم من السقوط ، على سبيل المثال ، إذا أرسل شخص ما سلسلة تحتوي على json غير صالح إلى الخادم.

إذا تم كل شيء بشكل صحيح ، فسنحصل على خادم يحتوي على تعليقات ، والذي يجب أن يبدو مثل هذا:



تتيح لك التعليقات ، في حالتنا ، الرسالة "OK" (موافق) ، رؤية تأكيد من الخادم بأنه تمت معالجة الرمز بالفعل.

هل يمكن تحسين هذا الخادم حتى الآن؟ يمكنك ، على سبيل المثال ، الاستعاضة عن استخدام وظيفة الطباعة باستخدام التسجيل ، وهذا صحيح ، ويسمح لك بعرض سجلات الخادم ليس فقط على الشاشة ، ولكن أيضًا إذا كنت تريد كتابتها في ملف بالتناوب التلقائي. أولئك الذين يرغبون في القيام بذلك من تلقاء أنفسهم.

استنتاج


إذا تم كل شيء بشكل صحيح ، فسنحصل على خادم صغير يسمح لك بالتحكم في مؤشر LED أو أي جهاز آخر من خلال متصفح من أي جهاز شبكة.

مهم: احتياطات السلامة

مرة أخرى ، لاحظت أنه لا توجد حماية أو مصادقة هنا ، لذا يجب ألا "تنشر" مثل هذه الصفحة على الإنترنت إذا كنت تخطط لإدارة بعض الحمل المسؤول أكثر أو أقل. على الرغم من أن حالات الهجمات على هذه الخوادم غير معروفة بالنسبة لي ، إلا أن الأمر لا يستحق إعطاء أي شخص يريد أن يكون قادرًا على فتح باب المرآب عن بعد أو تشغيل سخان الكيلوواط. إذا كنت تريد التحكم عن بُعد من خلال هذه الصفحة ، فإن الأمر يستحق إعداد VPN أو شيء مشابه.

في الختام ، أكرر أن المادة مصممة للمبتدئين ، وآمل أن يكون هذا مفيدًا إلى حد ما. من الواضح أن ليس كل شخص في خبر راضٍ عن توفر المقالات "للدمى" ، لذا ما إذا كان الجزء التالي سيعتمد على الدرجات النهائية أم لا. إذا كان هناك ، فسوف ينظر في أطر Flask و WSGI ، وكذلك أساليب المصادقة الأساسية.

جميع التجارب الناجحة.

Source: https://habr.com/ru/post/ar472464/


All Articles