كيف يبدو أرشيف الرمز البريدي وما الذي يمكننا فعله به. الجزء 3 - التطبيق العملي

استمرار المقال كيف يبدو أرشيف الرمز البريدي وماذا يمكننا أن نفعل به. الجزء 2 - واصف البيانات وضغطها .

أعزائي القراء ، أرحب بكم مرة أخرى في نقل البرمجة غير التقليدية في PHP. لفهم ما يحدث ، أوصيك بقراءة المقالات السابقة حول أرشيفات zip: كيف يبدو أرشيف zip وماذا يمكننا أن نفعل به ؟ كيف يبدو أرشيف zip وماذا يمكننا أن نفعل به. الجزء 2 - واصف البيانات وضغطها

في وقت سابق ، تحدثت عن كيفية إنشاء أرشيفات باستخدام كود PHP فقط وعدم استخدام أي مكتبات وإضافات (بما في ذلك الرمز البريدي القياسي) ، كما ذكرت بعض سيناريوهات الاستخدام. سأحاول اليوم إعطاء مثال على أحد هذه السيناريوهات.

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

الطبخ


يحتوي أحد مشروعاتي على الحيوانات الأليفة على برنامج نصي بسيط يمكنني استخدامه لتنزيل مجموعة من الصور وحفظها في الأرشيف. يقبل البرنامج النصي قائمة الروابط في json لإدخال STDIN ، ويعطي الأرشيف نفسه إلى STDOUT ، و json بهيكل مصفوفة في STDERR (بالطبع ، هذا أمر مبالغة قليلاً وليس من الضروري إنشاء أرشيف باستخدام PHP أصلي ، يمكنك استخدام أدوات أكثر ملائمة لهذا ، ومن ثم اقرأ الهيكل - سأحاول تسليط الضوء على هذه النقطة في المستقبل ، لكن في هذه المرحلة ، يبدو لي أنها ستكون أكثر وضوحًا).

هذا البرنامج النصي ، مع بعض التعديلات ، أقتبس أدناه:

zip.php
<?php $buffer = ''; while (!feof(STDIN)) { $buffer .= fread(STDIN, 4096); } $photos = json_decode($buffer, true, 512, JSON_THROW_ON_ERROR); $skeleton = ['files' => []]; $written = 0; [$written, $skeleton] = writeZip($written, $skeleton, $photos); $CDFH_Offset = $written; foreach ($skeleton['files'] as $index => $info) { $written += fwrite(STDOUT, $info['CDFH']); $skeleton['files'][$index]['CDFH'] = base64_encode($info['CDFH']); } $skeleton['EOCD'] = pack('LSSSSLLS', 0x06054b50, 0, 0, $records = count($skeleton['files']), $records, $written - $CDFH_Offset, $CDFH_Offset, 0); $written += fwrite(STDOUT, $skeleton['EOCD']); $skeleton['EOCD'] = base64_encode($skeleton['EOCD']); fwrite(STDERR, json_encode($skeleton)); function writeZip($written, $skeleton, $files) { $c = curl_init(); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 50, CURLOPT_FOLLOWLOCATION => true, ]); foreach ($files as $index => $url) { $fileName = $index . '.jpg'; for ($i = 0; $i < 1; $i++ ) { try { curl_setopt($c, CURLOPT_URL, $url); [$content, $code, $contentLength] = [ curl_exec($c), (int) curl_getinfo($c, CURLINFO_HTTP_CODE), (int) curl_getinfo($c, CURLINFO_CONTENT_LENGTH_DOWNLOAD) ]; if ($code !== 200) { throw new \Exception("[{$index}] " . 'Photo download error (' . $code . '): ' . curl_error($c)); } if (strlen($content) !== $contentLength) { var_dump(strlen($content), $contentLength); throw new \Exception("[{$index}] " . 'Different content-length'); } if ((false === $imageSize = @getimagesizefromstring($content)) || $imageSize[0] < 1 || $imageSize[1] < 1) { throw new \Exception("[{$index}] " . 'Broken image'); } [$width, $height] = $imageSize; $t = null; break; } catch (\Throwable $t) {} } if ($t !== null) { throw new \Exception('Error: ' . $index . ' > ' . $url, 0, $t); } $fileInfo = [ 'versionToExtract' => 10, 'generalPurposeBitFlag' => 0, 'compressionMethod' => 0, 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => hexdec(hash('crc32b', $content)), 'compressedSize' => $size = strlen($content), 'uncompressedSize' => $size, 'filenameLength' => strlen($fileName), 'extraFieldLength' => 0, ]; $LFH_Offset = $written; $skeleton['files'][$index] = [ 'LFH' => pack('LSSSSSLLLSSa*', 0x04034b50, ...array_values($fileInfo + ['filename' => $fileName])), 'CDFH' => pack('LSSSSSSLLLSSSSSLLa*', 0x02014b50, 798, ...array_values($fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $LFH_Offset, 'filename' => $fileName, ])), 'width' => $width, 'height' => $height, ]; $written += fwrite(STDOUT, $skeleton['files'][$index]['LFH']); $written += fwrite(STDOUT, $content); $skeleton['files'][$index]['LFH'] = base64_encode($skeleton['files'][$index]['LFH']); } curl_close($c); return [$written, $skeleton]; } 

دعنا نحفظه في مكان ما ، اتصل بـ zip.php ونحاول تشغيله.

لدي بالفعل قائمة من الروابط الجاهزة ، أوصي باستخدامها ، نظرًا لوجود صور تقارب 18 ميجابايت تقريبًا ، ونحن بحاجة إلى ألا يتجاوز حجم الأرشيف الإجمالي 20 ميغابايت (بعد قليل سأخبرك بذلك) - https://gist.githubusercontent.com /userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json

سوف نعمل مثل هذا:

 $ curl -s \ https://gist.githubusercontent.com/userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json \ | php zip.php \ 2> structure.json \ 1> photos.zip 

عندما ينتهي البرنامج النصي من عمله ، في الخرج يجب أن نحصل على ملفين - photos.zip ، أوصي بالتحقق من الأمر:

 $ unzip -tq photos.zip 

و structure.json ، حيث نقوم بتخزين json مع base64 لكل شيء موجود في أرشيفنا ، باستثناء البيانات نفسها - جميع هياكل LFH و CDFH ، وكذلك EOCD. لا أرى أي تطبيق عملي لـ EOCD بشكل منفصل عن الأرشيف حتى الآن ، ولكن يتم تحديد موضع إزاحة LFH بالنسبة لبداية الملف وطول البيانات في CDFH. لذلك ، ومع معرفة طول LFH ، يمكننا بالتأكيد معرفة من الموضع الذي ستبدأ فيه البيانات وأين ستنتهي.

الآن نحن بحاجة إلى تحميل ملفنا إلى بعض الخادم البعيد.

على سبيل المثال ، سوف أستخدم بوت التلغرام - هذا هو الأسهل ، وهذا هو السبب في أنه من المهم للغاية أن ننسجم مع حد 20 ميجابايت.

قم بتسجيل الروبوت باستخدامBotFather ، إذا لم يكن لديك حساب بالفعل ، اكتب شيئًا مرحبًا به في الروبوت الخاص بك ، وابحث عن رسالتك في https://api.telegram.org/bot{{{KEN►►/getUpdates ، حيث نعزل خاصية message.from .id = هذا هو معرف روبوت الدردشة.

املأ أرشيفنا معك ، تم الحصول عليه في الخطوة السابقة:

 $ curl -F document=@"photos.zip" "https://api.telegram.org/bot{{TOKEN}}/sendDocument?chat_id={{CHAT ID}}" > stored.json 

الآن لدينا بالفعل ملفين json - structure.json و storage.json .

وإذا سارت الأمور على ما يرام ، فسيحتوي ملف store.json على json بحقول موافق تساوي true ، وكذلك result.document.file_id ، وهو ما نحتاج إليه.

النموذج


الآن بعد أن أصبح كل شيء جاهزًا أخيرًا ، دعنا نبدأ العمل:

 <?php define('TOKEN', ''); //    $structure = json_decode(file_get_contents('structure.json'), true, 512, JSON_THROW_ON_ERROR); $stored = json_decode(file_get_contents('stored.json'), true, 512, JSON_THROW_ON_ERROR); $selectedFile = $structure['files'][array_rand($structure['files'])]; $LFH = base64_decode($selectedFile['LFH']); $CDFH = base64_decode($selectedFile['CDFH']); $fileLength = unpack('Llen', substr($CDFH, 24, 4))['len']; $fileStart = unpack('Lpos', substr($CDFH, 42, 4))['pos'] + strlen($LFH); $fileEnd = $fileStart + $fileLength; $response = json_decode(file_get_contents('https://api.telegram.org/bot' . TOKEN . '/getFile?' . http_build_query([ 'file_id' => $stored['result']['document']['file_id'] ])), true, 512, JSON_THROW_ON_ERROR); header('Content-Type: image/jpeg'); readfile('https://api.telegram.org/file/bot' . TOKEN . '/' . $response['result']['file_path'], false, stream_context_create([ 'http' => ['header' => "Range: bytes={$fileStart}-{$fileEnd}"] ])); 

في هذا البرنامج النصي ، نختار ملفًا عشوائيًا من القائمة التي لدينا في الأرشيف ، ونجد موضع بداية LFH ، ونضيف طول LFH إليه ، وبالتالي نحصل على موضع بداية بيانات ملف معين في الأرشيف.

بإضافة طول الملف إلى هذا ، نحصل على موضع نهاية البيانات.

يبقى الحصول على عنوان الملف نفسه (هذا في حالة معينة مع برقية) ومن ثم يكفي تقديم طلب باستخدام عنوان Range: bytes=<->-<->

(على افتراض أن الخادم يدعم طلبات النطاق)

نتيجة لذلك ، عند الوصول إلى البرنامج النصي المحدد ، سنرى محتويات صورة عشوائية من أرشيفنا.

لا ، حسنًا ، ليست خطيرة ...


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

لنجرب أصعب بعض الشيء - قم بتشغيلها جميعًا على amphp. بعد كل شيء ، يقوم الناس بتوزيع الإحصائيات في الإنتاج على node.js ، لكن ما الذي يمنعنا (حسنًا ، بصرف النظر عن المنطق السليم ، بالطبع)؟ لدينا تزامن هنا أيضا ، كما تعلمون.

سنحتاج 3 حزم:

  • amphp / http-server - من الواضح ، الخادم الذي سنقوم بتوزيع الملفات عليه
  • amphp / artax - عميل http الذي سنقوم من خلاله بسحب البيانات ، وكذلك تحديث رابط مباشر للملف في ذاكرة التخزين المؤقت.
  • amphp / parallel هي مكتبة متعددة مؤشرات الترابط وكل ذلك. لكن لا تخف ، فنحن نحتاج فقط إلى SharedMemoryParcel منه ، والذي سيخدمنا كذاكرة تخزين مؤقتة.

نضع التبعيات اللازمة:

 $ composer require amphp/http-server amphp/artax amphp/parallel 

واكتب هذا

سيناريو
 <?php define('TOKEN', ''); //    use Amp\Artax\DefaultClient; use Amp\Artax\Request as ArtaxRequest; use Amp\Http\Server\RequestHandler\CallableRequestHandler; use Amp\Http\Server\Server as HttpServer; use Amp\Http\Server\Request; use Amp\Http\Server\Response; use Amp\Http\Status; use Amp\Parallel\Sync\SharedMemoryParcel; use Amp\Socket; use Psr\Log\NullLogger; require 'vendor/autoload.php'; Amp\Loop::run(function () { $structure = json_decode(file_get_contents('structure.json'), true, 512, JSON_THROW_ON_ERROR); $stored = json_decode(file_get_contents('stored.json'), true, 512, JSON_THROW_ON_ERROR); $parcel = SharedMemoryParcel::create($id = bin2hex(random_bytes(10)), []); $client = new DefaultClient(); $handler = function (Request $request) use ($parcel, $client, $structure, $stored) { $cached = yield $parcel->synchronized(function (array $value) use ($client, $stored) { if (!isset($value['file_path']) || $value['expires'] <= time()) { $response = yield $client->request('https://api.telegram.org/bot' . TOKEN . '/getFile?' . http_build_query([ 'file_id' => $stored['result']['document']['file_id'] ])); $json = json_decode(yield $response->getBody(), true, 512, JSON_THROW_ON_ERROR); $value = ['file_path' => $json['result']['file_path'], 'expires' => time() + 55 * 60]; } return $value; }); $selectedFile = $structure['files'][array_rand($structure['files'])]; $LFH = base64_decode($selectedFile['LFH']); $CDFH = base64_decode($selectedFile['CDFH']); $fileLength = unpack('Llen', substr($CDFH, 24, 4))['len']; $fileStart = unpack('Lpos', substr($CDFH, 42, 4))['pos'] + strlen($LFH); $fileEnd = $fileStart + $fileLength; $request = (new ArtaxRequest('https://api.telegram.org/file/bot' . TOKEN . '/' . $cached['file_path'])) ->withHeader('Range', "bytes={$fileStart}-{$fileEnd}"); $response = yield $client->request($request); if (!in_array($response->getStatus(), [200, 206])) { return new Response(Status::SERVICE_UNAVAILABLE, ['content-type' => 'text/plain'], "Service Unavailable."); } return new Response(Status::OK, ['content-type' => 'image/jpeg'], $response->getBody()); }; $server = new HttpServer([Socket\listen("0.0.0.0:10051")], new CallableRequestHandler($handler), new NullLogger); yield $server->start(); Amp\Loop::onSignal(SIGINT, function (string $watcherId) use ($server) { Amp\Loop::cancel($watcherId); yield $server->stop(); }); }); 

ما نحصل عليه نتيجة لذلك: لدينا الآن ذاكرة تخزين مؤقت حيث نقوم بحفظ رابط إلى مستند لمدة 55 دقيقة ، مما يضمن أيضًا أننا لن نطلب رابطًا عدة مرات على التوالي إذا تلقينا الكثير من الطلبات في وقت انتهاء صلاحية ذاكرة التخزين المؤقت. حسنًا ، كل هذا أسهل من قراءة ملف PHP-FPM (أو ، لا سمح الله ، PHP-CGI).

علاوة على ذلك ، يمكنك رفع تجمع مثيل amphp - SharedMemoryParcel ، باسمه ، يشير إلى أن ذاكرة التخزين المؤقت الخاصة بنا سوف تتعثر بين العمليات / مؤشرات الترابط. حسنًا ، أو إذا كنت لا تزال لديك مخاوف صحية بشأن موثوقية هذا التصميم ، فقم بالبحث عن proxy_pass و nginx .

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

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

أو إذا كنت تقوم بتخزين الملفات عن بعد. في برقية ، من 20 ميغابايت ، بالطبع ، لا يمكنك الالتفاف ، ولكن من يدري ، هناك خيارات أخرى.

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


All Articles