النشر الموزّع
خدمة مستند واحد من عدة عقد
عندما تقوم عدة عقد بخدمة نفس المستندات، فإن العقدة التي تقوم بتصيير المستند ليست بالضرورة هي العقدة التي ترد على طلبات صفحات المتصفح. هذا الإصدار يقسم تلك المهمة إلى نصفين واضحين — جانب الكتابة الذي ينشر مخرجات المستند إلى التخزين المشترك، وجانب القراءة الذي يخدمها من أي عقدة.
إذا كان تطبيقك يستدعي حالياً UseDoconutWebFarm(...)، فإن هذه الصفحة هي بديلك. لا يوجد UseDoconutWebFarm ولا WebFarmOptions في هذا الإصدار.
بنيتك لا تتغير. ضع الجذر المشترك على نفس مسار UNC الذي تستخدمه اليوم وستستمر كل عقدة في العمل كما هي الآن. ما يتغير هو أن النشر أصبح عملية تستدعيها، بدلاً من أن يكون تأثيراً جانبيًا للتصيير.
ما استبدل وسيط الويب-فارم
سابقًا كان وسيط واحد يقوم بكل شيء: كان يخدم الصور المُصَغَّرة مسبقًا من مجلد مشترك ويستطلع بينما كانت عقدة أخرى لا تزال تكتبها. كان الانتظار ميزانية عمياء — "حجب حتى ثلاث دقائق على أمل ظهور الصفحة".
الآن نفس المهمة هي نداءان يمكنك رؤيتهما وفهمهما:
node A OpenDocument request
-> PublishAsync(...) writes pages, document info,
search index and metadata
into RootPath/{token}/
-> returns the token
node B ?token=...&page=7
-> UseDoconutCloud<FileShareCloudHandler>
-> reads RootPath/{token}/ and serves the pageالتكوين المشترك الذي يحتاجه النصفان
الناشر يكتب باستخدام FileShareConfig ومعالج القراءة يخدم باستخدام واحد. إذا كان RootPath أو UsePerTokenSubfolders مختلفًا بينهما، فإن العقدة الأخرى تُعيد 404. أنشئ التكوين في مكان واحد فقط واستدعِه من كلا الجانبين — يحتفظ التطبيق المرجعي بمساعد SharedStore.Config(...) واحد لهذا السبب:
internal static class SharedStore
{
public static FileShareConfig Config(string rootPath, bool asyncPublish) => new()
{
Location = CloudLocation.FileShare,
RootPath = rootPath, // e.g. \\fileserver\doconut
UsePerTokenSubfolders = true,
ReadReadyPollMs = asyncPublish ? 250 : 0,
};
}FileShareConfig موجودة في Doconut.Clouds، إلى جانب AzureConfig وAmazonConfig وGoogleCloudConfig وDropBoxConfig وRedisConfig وCDNConfig وFTPConfig. في المكتبة السابقة كانت هذه في Doconut.Configs.Cloud؛ لم تتغير أسماء الفئات.
| الخاصية | الغرض |
|---|---|
Location | CloudLocation.FileShare لمشاركة UNC/محلية |
RootPath | المجلد المشترك الذي يمكن لكل عقدة الوصول إليه |
UsePerTokenSubfolders | احتفظ بمخرجات كل مستند في مجلد {token} خاص به |
ReadReadyPollMs | مدة انتظار جانب القراءة لصفحة لا تزال تُكتب؛ 0 يعطل الاستطلاع |
جانب الكتابة — نشر المستند
قم بحقن DistributedDocumentPublisher وانشر بدلاً من فتح جلسة محلية. نداء واحد يصيغ ويكتب مجموعة المخرجات الكاملة — الصفحات، معلومات المستند، فهرس البحث، والبيانات الوصفية:
var shareConfig = SharedStore.Config(rootPath, asyncPublish: false);
var documentOptions = new DocOptions { TimeOut = 60 };
var config = new PdfConfig { AllowSearch = true };
var publishOptions = new DistributedDocumentPublishOptions
{
CleanUpStagingDirectory = true,
FirstPagePriority = true,
};
DistributedDocumentPublishResult result = await _publisher.PublishAsync(
pathToFile, shareConfig, config, documentOptions, publishOptions, ct);
return Content(result.Token);DistributedDocumentPublishResult يحمل Token وArtifactPath وTotalPages وFormat. يتم إرسال الرمز إلى المتصفح تمامًا كما يحدث مع رمز عارض عادي.
هذا النداء الواحد يستبدل الزوج السابق — فتح المستند في جلسة محلية ثم تفريغ الصور إلى المشاركة باستخدام ExportToPng(sharedPath). لا يوجد ما يعادل ExportToPng وExportToCloud هنا.
الجانب القارئ — الخدمة من المشاركة
قم بتركيب معالج FileShare على نفس الفرع الذي يستدعيه العميل بالفعل:
app.UseDoconutResources();
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconutCloud<FileShareCloudHandler>(options =>
{
options.Location = CloudLocation.FileShare;
options.CloudUploadConfig = SharedStore.Config(rootPath, asyncPublish: false);
}, "/"));UseDoconutCloud هو عام في هذا الإصدار: الموفر هو معامل النوع بدلاً من قيمة Location التي يتم التبديل بناءً عليها داخل الوسيط. تُسمى المعالجات <Provider>CloudHandler — FileShareCloudHandler، AzureCloudHandler، AmazonS3CloudHandler، GoogleCloudHandler، DropBoxCloudHandler، RedisCloudHandler، FTPCloudHandler، وCDNCloudHandler.
معالج CDN هو للقراءة فقط — يقوم بخدمة ما هو موجود بالفعل على CDN؛ لا يقوم بالرفع.
أولوية الصفحة الأولى و HTTP 202
FirstPagePriority = true يجعل الصفحة 1 قابلة للخدمة فورًا. الصفحات اللاحقة تُجيب بـ HTTP 202 Accepted حتى يتم كتابتها، بدلاً من حظر الطلب.
هذا ينقل الانتظار إلى العميل، حيث يكون محدودًا ومرئيًا. فعّل إعادة المحاولة على الواجهة:
const objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/',
ResPath: 'doconut-res/images',
retryOn409: true // retries 202 (and legacy 409) readiness responses
});عميل لا يقوم بإعادة المحاولة يعرض مربعات مكسورة للصفحات التي لا تزال تُعرض. ميزانية إعادة المحاولة وإعداداتها الافتراضية (retryInitialDelayMs، retryBackoffFactor، retryMaxDelayMs، retryMaxAttempts، retryMaxTotalMs) موثقة في تكوين العارض.
هذا الزوج — FirstPagePriority بالإضافة إلى الاستطلاع من الجانب القارئ — هو ما يحل محل إعدادات PageWaitTimeSeconds و StartWaitFromPage السابقة. لا وجود لأي منهما الآن.
اختياري: النشر في الخلفية
بشكل افتراضي، PublishAsync يُنشئ قبل أن يُعيد النتيجة. للعودة بمجرد إنشاء الرمز المميز والسماح لعامل مستضاف بالإنشاء في نفس المجلد، سجِّل طابور الاشتراك الاختياري:
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();بعد ذلك ضع العناصر في الطابور بدلاً من النشر داخل السطر، واضبط Async = true في خيارات النشر. الطابور محدود: عندما يُشبَع، يرمي EnqueueAsync استثناء DistributedPublishQueueFullException. استجب بذلك برمز 503 ورأس Retry-After حتى يتراجع العملاء بدلاً من التوقف:
try
{
var token = await _publishQueue.EnqueueAsync(
pathToFile, shareConfig, config, documentOptions, publishOptions, ct);
return Content(token);
}
catch (DistributedPublishQueueFullException)
{
Response.Headers.RetryAfter = "5";
Response.StatusCode = 503;
return Content("The publish queue is full. Please retry.");
}بدون هذا التسجيل لا يُسجَّل شيء ويظل النشر متزامنًا — المسار الافتراضي هو مطابقة تامة للبايت.
عند تشغيل النشر في الخلفية، أعطِ الجانب القارئ قيمة غير صفرية لـ ReadReadyPollMs حتى ينتظر لفترة قصيرة صفحة لم يكتبها العامل بعد.
الأمان عبر العقد
الرموز المرتبطة بالجلسة تفترض أن العقدة الخادمة رأيت الطلب الافتتاحي. في مجموعة من العقد لم يحدث ذلك، لذا فإن الفحص لكل جلسة سيُرفض الطلبات المشروعة. لهذا السبب يضبط التطبيق المرجعي الموزع UnsafeMode = true.
UnsafeMode = true ليس نموذج الأمان — إنه إلغاء نموذج لم يعد صالحًا. استبدله بتذاكر وصول موقعة، يمكن لكل عقدة التحقق منها بشكل مستقل:
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });لا تُعيد أبداً UnsafeMode = true إلى نشر عقدة واحدة، ولا تتعامل معه كاختصار لفحص جلسة فاشل على عقدة واحدة — الحل الصحيح هناك هو AddSession() / UseSession(). راجع الجلسات والأمان.
إغلاق مستند منشور
CloseDocument(token) على الـ Viewer المُحقن يُغلق جلسة محلية. لإزالة القطع المنشورة للمستند من المشاركة، يُقدِّم الناشر:
bool CloseDistributedDocument(FileShareConfig fileShareConfig, string token)مرّر نفس FileShareConfig الذي نُشر به المستند.
تخطيط الإعدادات خطوة بخطوة
| السابق | الآن |
|---|---|
UseDoconutWebFarm(new WebFarmOptions { … }) | PublishAsync(...) على جانب الكتابة بالإضافة إلى UseDoconutCloud<FileShareCloudHandler>(...) على جانب القراءة |
WebFarmOptions.Path | FileShareConfig.RootPath |
PageWaitTimeSeconds / StartWaitFromPage | DistributedDocumentPublishOptions.FirstPagePriority بالإضافة إلى FileShareConfig.ReadReadyPollMs |
viewer.OpenDocument(...) then viewer.ExportToPng(share) | استدعاء واحد لـ PublishAsync(...) |
viewer.ExportToCloud(cloudConfig) | PublishAsync(...) مع إعدادات الموفر |
UseDoconutCloud(new CloudOptions { Location = … }) | UseDoconutCloud<THandler>(options => …, pathPrefix) |
DocOptions.IsWebfarm / WebfarmPath | ما زال موجودًا، مكتوبًا IsWebFarm / WebFarmPath — غير مطلوب في مسار النشر |
Doconut.Configs.Cloud | Doconut.Clouds |
قائمة التحقق
- أنشئ
FileShareConfigفي مساعد واحد واستدعِه من كلا الجزأين. - وجّه
RootPathإلى المشاركة التي تستخدمها بالفعل. - انشر باستخدام
PublishAsync(...)؛ احذف أي استدعاءاتExportToPng/ExportToCloud. - قدِّم باستخدام
UseDoconutCloud<FileShareCloudHandler>(...)على الفرع الذي يستدعيه العميل. - فعّل
FirstPagePriorityوretryOn409معًا، أو اتركهما معطَّلين. - استبدل فحص الجلسة الفردية بتذاكر وصول موقعة — لا تُرسل
UnsafeMode = trueبمفرده. - تحقق بفتح مستند على عقدة واحدة وطلب صفحة متأخرة من أخرى.
هل كانت هذه الصفحة مفيدة؟