Skip to main content
البروتوكول الأصلي هو بروتوكول ثنائي موجَّه بالاتصال تتخاطب به عملاء وخوادم ClickHouse عبر TCP. وهو ينقل استعلامات SQL، وبيانات النتائج، وحمولات INSERT، وبيانات القياس عن بُعد الخاصة بالتنفيذ، وإشارات الأخطاء. وهو البروتوكول الذي يستند إليه عميل سطر الأوامر وC++ ومعظم برامج التشغيل الأصلية من جهات خارجية. تغطي هذه الصفحة البروتوكول نفسه: تأطير الحزم، وآلة حالات الاتصال، والتفاوض على الإصدار، وجسم كل رسالة لا تنتمي إلى Block. أما البايتات داخل حزم عائلة Data (أي Block، وأعمدته، وترميزات كل نوع) فهي موضوع منفصل، موثَّق في مواصفة Native Format.
مواصفة مرافقةتمثل هذه الصفحة أحد جزأَي هذه المجموعة، وتُنشر مع المواصفة المرافقة Native Format. وتقسم المواصفتان العمل بوضوح: تختص هذه الصفحة بطبقة الحزم والنقل، بينما تختص مواصفة Native Format بالبايتات داخل حزم عائلة Data.
هناك بعض الخصائص التي تنطبق على امتداد البروتوكول. فالبروتوكول ثنائي وموضعي: لا توجد وسوم للحقول إلا داخل BlockInfo، لذا فإن أي بايت يوضع في غير موضعه يفقد المزامنة مع كل ما يليه. وهو بروتوكول ذو حالة، ويعالج كل اتصال TCP استعلامًا واحدًا في كل مرة — ولا توجد فيه آلية تعدد الإرسال. أما الأعداد الصحيحة ثابتة العرض فتُخزَّن بترتيب little-endian.

نظرة عامة

تبدأ كل رسالة برمز نوع حزمة VarUInt، يتبعه جسم يعتمد شكله على ذلك الرمز وعلى إصدار البروتوكول المتفاوض عليه. يمر الاتصال عبر ثلاث مراحل — مصافحة لمرة واحدة، ثم أي عدد من عمليات تبادل Ping أو Query، ثم الإغلاق: ينقل بروتوكول TCP الأصلي دائمًا البيانات الجدولية بتنسيق Native، بغضّ النظر عن أي عبارة FORMAT في SQL. أمّا إعادة تنسيقها إلى RowBinary وCSV وJSON وما إلى ذلك، فهي من مهام العميل، وتتم بعد فك ترميز كتل Native. (أمّا واجهة HTTP فلها مسار برمجي مختلف يلتزم فعلًا بعبارة FORMAT؛ لكن HTTP خارج نطاق هذا المحتوى هنا.)

الأمان

أمان النقل (TLS)

يقع TLS في طبقة النقل، أسفل البروتوكول. وعند تفعيله، يُشفَّر تيار TCP بالكامل، وتظل رسائل البروتوكول متطابقة تمامًا، بايتًا ببايت، سواء استُخدم TLS أم لا.

المصادقة

تحدث المصادقة أثناء المصافحة، في رسالة ClientHello. يُنقل الحقلان user وpassword كسلاسل نصية غير مشفّرة، لذا فإن التشفير على مستوى النقل (TLS) هو ما يحمي بيانات الاعتماد أثناء انتقالها. تتوفر مصادقة التحدي والاستجابة عبر SSH بدءًا من إصدار البروتوكول 54466 — راجع مصادقة التحدي والاستجابة عبر SSH.

السر بين الخوادم

لتنفيذ الاستعلامات الموزعة، تتحقق الخوادم من هوية بعضها بعضًا عبر إثبات معرفة سر مشترك — من دون إرسال السر نفسه عبر الشبكة. تتضمن كل Query قيمة auth_hash بطول 32 بايت من نوع SHA-256 في الحقل 4 من Query، وتُحتسب استنادًا إلى salt وnonce والسر المُعدّ والاستعلام، ثم يعيد الخادم المستقبِل احتسابها ويقارنها. ولا يُفعَّل ذلك إلا مع الميزة INTERSERVER_SECRET ‏(v54441). وترسل البرامج العميلة الخارجية دائمًا سلسلة فارغة هنا. راجع المصادقة بين الخوادم.

الإصدارات وبوابات الميزات

التفاوض على الإصدار

يُعلن كلٌّ من العميل والخادم عن الحد الأقصى لإصدار البروتوكول الذي يدعمه أثناء المصافحة. ويكون الإصدار المتفَق عليه هو الأصغر بين الاثنين:
تستخدم كل رسالة بعد ذلك الإصدار المتفق عليه لتحديد الحقول الموجودة في البيانات المنقولة.

بوابات الميزات

تُحدَّد كل ميزة بإصدار البروتوكول الذي أدخلها، وتكون مفعّلة عندما يكون الإصدار المتفاوض عليه أكبر من هذا الرقم أو مساويًا له.
عندما تكون الميزة مفعّلة، يجب أن تكون حقولها موجودة في التمثيل الثنائي المنقول. يعتمد البروتوكول ترتيبًا موضعيًا صارمًا، لذا فإن حذف حقل خاضع لبوابات الميزات يفسد تدفق البايتات لكل حقل يليه.

جدول الميزات

غلاف الحزمة

تشترك كل رسالة متبادلة عبر الشبكة في البنية الخارجية نفسها، في كلا الاتجاهين:
توجد جداول أنواع الحزم الكاملة في مرجع نوع الحزمة. نوع الحزمة هو VarUInt، وليس بايتًا ثابت العرض. بالنسبة إلى القيم الأقل من 128، ينتج VarUInt البايت المفرد نفسه، لكن يجب على التطبيقات استخدام ترميز VarUInt حتى تظل متوافقة إذا وصلت أنواع الحزم المستقبلية إلى 128 أو أكثر. يوثّق مرجع الرسائل جسم كل حزمة فقط — أي البايتات التي تأتي بعد رمز نوع الحزمة. يبدأ ترقيم الحقول من 1 مع أول حقل في الجسم.

التأطير المُجزّأ (v54470+)

عند التفاوض على الميزة CHUNKED_PROTOCOL (راجع المصافحة)، تُغلَّف كل حزمة على مستوى النقل بتأطير مُجزّأ. ويكون هذا التغليف منفصلًا لكل اتجاه: إذ يجري التفاوض على client→server و server→client كلٌّ على حدة، وقد ينتهي كل منهما إلى وضع مختلف (مُجزّأ أو غير مؤطَّر). البنية على مستوى النقل لكل حزمة:
تنسيق wire لكل chunk:
نوع الحزمة VarUInt يكون داخل التدفق المُجزّأ: فهو البايت الأول من حمولة الحزمة (أي البايت الأول من أول جزء)، وليس بايتًا منفصلًا يُرسَل مسبقًا قبل التأطير. حمولة الجزء لكل حزمة هي [VarUInt packet_type_code][message body] كاملةً كما ترد في غلاف الحزمة. وأي عميل يترك نوع الحزمة خارج التدفق المُجزّأ يجعل الطرف الآخر يقرأ بايت النوع هذا على أنه البايت الأول من حجم الجزء u32، ما يؤدي إلى فقدان تزامن الاتصال. قد تُقسَّم الحزمة الواحدة على عدة أجزاء إذا امتلأ المخزن المؤقت الخاص بالكاتب أثناء كتابة الحزمة؛ ويمكن أن يقع هذا الانقسام في أي موضع، بما في ذلك داخل VarUInt الخاص بنوع الحزمة. يقوم القارئ بضم حمولات الأجزاء، ويتعامل مع الصفر اللاحق المكوَّن من 4 بايتات على أنه حد شفاف للحزمة — فيستهلكه، لكنه لا يمرّره إلى أي مكوّن يقرأ أجسام الحزم. الحزم التي لا تحتوي على جسم تُغلَّف أيضًا: فالحزمة أحادية البايت مثل Ping أو Pong تصبح [u32 size = 1][0x04][u32 0] بمجرد الاتفاق على التجزئة. وأي وصف من نوع “بايت واحد على مستوى النقل” في موضع آخر من هذه الصفحة يشير إلى الصيغة السابقة للتجزئة. التفاوض. يحمل كلٌّ من ServerHello وAddendum حقلي String، واحدًا لكل اتجاه، بقيم مأخوذة من {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
  • chunked / notchunked وضعان صارمان: ذلك الجانب يشترط هذا الوضع تحديدًا.
  • صيغ _optional مرنة: فهي تقبل أيًّا من الوضعين الذي يختاره الطرف الآخر.
تُحتسب القيمة المتفق عليها لكل اتجاه على أساس ثنائي: على جانب العميل، يُفاوَض تفضيل SEND الخاص بالعميل مع تفضيل RECV الخاص بالخادم، والعكس صحيح. التوقيت. تنتقل سلاسل التفاوض عبر قناة غير مؤطَّرة: ClientHello → ServerHello (تفضيلات الخادم) → Addendum (القيم التي تفاوض عليها العميل). ويُطبَّق التحول إلى التأطير على كل بايت يُرسَل بعد تفريغ Addendum. أما Addendum نفسه وClientHello وServerHello فتبقى دائمًا غير مؤطَّرة.

دورة حياة الاتصال

في أي وقت، يكون الاتصال في واحدة فقط من أربع حالات: HANDSHAKE أو READY أو READING_RESPONSE أو يكون قد انتهى. وبما أن البروتوكول لا يدعم تعدد الإرسال، فإن العميل الذي يرسل طلبًا جديدًا قبل قراءة الاستجابة السابقة بالكامل سيتسبب في تداخل البايتات أثناء النقل وإفساد التدفق.

الحالات

يسير المسار المثالي مباشرةً إلى الأسفل — HANDSHAKE → READY → READING_RESPONSE → READY — مع الحلقة الذاتية لـ Ping/Pong، فيما تصبّ جميع حواف الفشل في المصب الوحيد Terminated.

مرحلة المصافحة

تتم المصادقة والتفاوض على إصدار البروتوكول. يحدث ذلك مرة واحدة فقط لكل اتصال، قبل أي شيء آخر. فُتح اتصال TCP للتو، ولم تُتبادل أي رسائل بعد. ويكون التسلسل كالتالي:
  1. يرسل العميل ClientHello مع أعلى إصدار بروتوكول يدعمه.
  2. يقرأ العميل الاستجابة ويعالجها وفقًا لنوع الحزمة:
  3. إذا كانت قيمة negotiated_version ≥ 54458 (ميزة ADDENDUM)، يرسل العميل Addendum. يستند هذا القرار إلى الإصدار المتفق عليه، لا إلى الإصدار الذي أعلنه العميل.
عند النجاح، ينتقل الاتصال إلى READY؛ وعند حدوث أي خطأ، ينتهي.

مرحلة Ping

فحص حيوية على مستوى التطبيق، مستقل عن TCP keepalive. تؤكد دورة Ping/Pong الناجحة ذهابًا وإيابًا أن اتصال TCP حيّ في كلا الاتجاهين وأن الخادم يستجيب. تكون Ping عديمة الحالة وغير مرتبطة بأي استعلام، لذا فإن عمليات Ping المتتالية مستقلة بعضها عن بعض. بدءًا من جاهز، يكون التدفق كما يلي:
  1. يرسل العميل Ping.
  2. يقرأ العميل الاستجابة:

مرحلة الاستعلام

يرسل العميل عبارة SQL، ثم يعيد الخادم بث كتل النتائج وبيانات القياس عن بُعد الخاصة بالتنفيذ. وتكون الاستجابة تسلسلاً من الحزم ينتهي بحزمة واحدة فقط، إما EndOfStream أو Exception. بدءًا من جاهز، يكون التدفق كما يلي: عند حدوث خطأ في أي مرحلة، يرسل الخادم Exception بدلًا من EndOfStream، مما يُنهي الاستعلام.
  1. يرسل العميل Query مع query_id فريد (عادةً ما يكون UUID).
  2. يرسل العميل أي جداول خارجية، ثم وسم Data فارغًا. تحتوي حزمة Data الفارغة على table_name = "" و num_columns = 0 و num_rows = 0. ولا يبدأ الخادم تنفيذ الاستعلام حتى يتلقى هذا الوسم.
  3. ينتقل العميل إلى READING_RESPONSE ويُفرغ مخزن الكتابة المؤقت لديه.
  4. يقرأ العميل حزم الاستجابة في حلقة، ويوجّه المعالجة حسب النوع:
عند EndOfStream أو Exception تمت معالجته، تعود connection إلى جاهز. أما مخالفة protocol أو خطأ I/O فيُنهيانها.
غالبًا ما تُربك حالة num_rows == 0 عمليات التنفيذ الجديدة. فالـ block ذو الصفوف الصفرية هو وسم فاصل أو ترويسة schema، وليس إشارة إلى نهاية التدفق. ولا يُنهي الاستجابة إلا EndOfStream أو Exception.

مرحلة INSERT

مرحلة INSERT هي مرحلة الاستعلام مع تبادلين إضافيين. يرسل العميل عبارة INSERT؛ ويرد الخادم بـ كتلة مخطط تصف الجدول الهدف؛ ثم يرسل العميل حزم Data التي تحتوي على الصفوف، ثم وسم Data الفارغ؛ ويُنهي الخادم العملية بـ EndOfStream أو Exception. بدءًا من الحالة جاهز، تكون SQL عبارة INSERT بالصيغة INSERT INTO <table> [(<cols>)] VALUES — من دون قيمة حرفية مضمنة من الشكل VALUES (...)، لأن بيانات الصفوف تتدفق عبر حزم Data. التدفق:
  1. يرسل العميل Query مع ضبط body على عبارة INSERT في SQL.
  2. يرسل العميل أي external tables إن وُجدت (وهذا نادر مع INSERT). وعلى خلاف مرحلة الاستعلام، فهو لا يرسل هنا وسم Data فارغًا. تُرسَل حزمة INSERT Query مع وجود بيانات قيد الانتظار، لذلك يُؤجَّل block الفارغ الذي يدل على نهاية البيانات إلى الخطوة 5؛ لأن إرساله قبل schema block سيجعل الخادم يقرأه على أنه نهاية تدفق الصفوف، فيُنهي INSERT بلا أي rows، ثم يُحلّل أول حزمة صفوف فعلية على أنها حزمة شاردة على المستوى الأعلى.
  3. يستنزف العميل حزم metadata ‏(TableColumns وProgress وProfileInfo وLog وProfileEvents) إلى أن يقرأ حزمة schema Data — وهي Block تضم 0 rows ولكن ببنية columns كاملة (الأسماء والأنواع). ويُعد schema block بمثابة العقد: يجب أن تطابق rows التي يرسلها العميل بعد ذلك بنية هذه columns.
  4. يرسل العميل data block واحدة أو أكثر. ولكل block يكتب VarUInt(ClientPacket::Data = 2)، ثم String("") لاسم external-table الفارغ، ثم الـ Block. يجب أن تتوافق column types مع columns الخاصة بـ schema block بحسب الموضع.
  5. يرسل العميل محدِّد نهاية الإدخال: حزمة Data تحتوي على Block فارغ (0 columns, 0 rows).
  6. يستنزف العميل تدفق الاستجابة إلى أن يصل إلى EndOfStream (نجاح) أو Exception (فشل).
INSERT غير المتزامن (v54484+). عندما يحمل الاستعلام async_insert = 1، يضع الخادم rows في queue ثم ينفّذ flush لها كجزء من batch. وعند الإصدار المتفاوض عليه ≥ 54484 (PROGRESS_IN_ASYNC_INSERT)، ما إن يكتمل flush حتى يُصدر الخادم حزمة Progress إضافية، تتبعها مباشرة ProfileEvents الخاصة بعملية insert، ثم EndOfStream. أما في الإصدارات الأقدم من 54484، فيتجاوز الخادم حزمة Progress اللاحقة هذه. وهذه الحزمة هي Progress عادية؛ ولأن الخادم يعيد ضبط query pipeline قبل احتساب counts الخاصة بالكتابة، فإن الزيادة لا تتضمن عمليًا سوى الزمن المنقضي، بينما تصل إلى العميل إحصاءات rows والبايتات المكتوبة عبر ProfileEvents المصاحبة. وأي عميل يستنزف بالفعل حزم Progress المتداخلة في الخطوة 6 لا يحتاج إلا إلى قبول حزمة إضافية واحدة. يعود connection إلى جاهز عند EndOfStream أو عند Exception تمت معالجته. أما مخالفات protocol وأخطاء I/O فتُنهيه.

مرجع الرسائل

تُدرج الحقول وفق ترتيبها على wire. ويستخدم العمود Type ما يلي:
  • VarUInt — عدد صحيح غير موقّع بطول متغيّر (انظر VarUInt).
  • String — بايتات مسبوقة بـ VarUInt (انظر String).
  • UInt8 وInt32 وما إلى ذلك — أعداد صحيحة ثابتة العرض بترتيب little-endian.
  • Bool — بايت واحد، 0x00 أو 0x01.
يوضح العمود Role الجهة التي تستخدم كل حقل:
  • client — يضبطه العملاء الخارجيون.
  • inter-server — يكون ذا معنى فقط في الاتصال بين الخوادم؛ ويكتب العملاء الخارجيون قيمة افتراضية.
  • universal — يستخدمه الطرفان.
توثّق هذه الجداول جزء body من كل حزمة فقط، بعد رمز نوع الحزمة.

ClientHello (نوع الحزمة 0)

العميل → الخادم. أول رسالة بعد فتح اتصال TCP.

ServerHello (نوع الحزمة 0)

Server → Client. الرد على ClientHello عند نجاح المصادقة. Rule — عنصر من password_complexity_rules: تعكس القائمة configuration الخاصة بمشغّل الخادم لسياسة password، وهي استرشادية بحتة — إذ لا يفرض الخادم هذه القواعد أثناء handshake. يمكن لـ client الذي يوفّر وظيفة تغيير/تعيين password استخدام هذه القواعد للإشارة إلى الأخطاء قبل إرسال password غير متوافقة إلى الخادم.
للحد من استخدام الموارد في مواجهة خادم عدائي أو سيئ الإعداد، اجعل الحد الأقصى للقيمة المفكوك ترميزها count هو 256 إدخالًا، ولكل من pattern وmessage من نوع String هو 4096 بايت. وتُعد قيمة count البالغة 0 (من دون أزواج تالية) الحالة الشائعة للخوادم التي لم تُضبط لها سياسة password.

ملحق (من دون نوع حزمة)

العميل → الخادم، ويكون مفعّلًا بواسطة ADDENDUM ‏(v54458). يُرسل مباشرةً بعد اكتمال تبادل المصافحة. وليس نوع حزمة مستقلًا — إذ تُرسل الحقول على الـ wire كما هي، من دون بايت بادئة لنوع الحزمة. يُطبَّق التحول إلى التأطير المقطّع بعد flush هذا الملحق — أما الملحق نفسه فغير مؤطَّر.

Ping (نوع الحزمة 4)

العميل → الخادم. بلا متن — تتكوّن الحزمة من بايت واحد 0x04 قبل التأطير بالمقاطع؛ وعند التفاوض على استخدام التقسيم إلى مقاطع، يصبح هذا البايت حمولةً من بايت واحد لمقطع واحد (انظر التأطير المُجزّأ).

Pong (نوع الحزمة 4)

الخادم → العميل. من دون محتوى — تتكوّن الحزمة من بايت واحد 0x04 قبل التأطير المُجزّأ؛ وعند الاتفاق على استخدام التجزئة، يصبح هذا البايت حمولةً من بايت واحد لجزء واحد (انظر التأطير المُجزّأ).

Exception (نوع الحزمة 2)

الخادم → العميل. تُرسل عندما يصادف الخادم خطأً أثناء أي مرحلة.

Query (نوع الحزمة 1)

العميل → الخادم.

ClientInfo (مضمّن في Query)

Client → Server، وهو مضمّن في جسم Query (الحقل 2). ومقيّد بـ CLIENT_INFO ‏(v54032). (بعض الحقول داخل ClientInfo مقيّدة بإصدارات أحدث، كما هو مذكور لكل حقل أدناه.)
التخطيط المعتمد على الواجهة (الحقول 7–12)الحقول 7–12 أعلاه تمثّل فرع TCP. عندما لا تكون قيمة query_interface (الحقل 6) هي TCP، تُستبدل هذه الحقول بتخطيط wire مختلف — وليست مجرد حقول اختيارية محذوفة، لذا يجب على وحدة فك الترميز أن تتفرع بناءً على الحقل 6.
  • query_interface = 2 (HTTP): تُكتب بدلًا من ذلك معلومات طلب HTTP المُمرَّر من الخادم — http_method (UInt8)، وhttp_user_agent (String)، ثم forwarded_for (String، ويخضع لـ X_FORWARDED_FOR_IN_CLIENT_INFO v54443) وhttp_referer (String، ويخضع لـ REFERER_IN_CLIENT_INFO v54447). ولا تظهر حقول os_user/client_hostname/client_name/version_*/protocol_version.
  • أي واجهة أخرى: لا تُكتب أي من حقول TCP (7–12)، ولا أي من حقول HTTP؛ ويستمر التدفّق مباشرةً مع quota_key.
بعد هذا التفرع، يعود التخطيط ليلتقي مجددًا: يأتي quota_key (الحقل 13) وdistributed_depth (الحقل 14) في جميع الواجهات، ثم يُكتب version_patch (الحقل 15) فقط في حالة TCP.تبرز أهمية هذا التفرع أساسًا في حركة المرور بين الخوادم، حيث يمرّر الخادم المُبادِر استعلامًا وصل أصلًا عبر HTTP. وأي وحدة فك ترميز تقرأ دائمًا حقول TCP ستُسيء تفسير مثل هذه الحزم — فتتعامل مع http_method أو http_user_agent على أنه quota_key.
ترميز OpenTelemetry (الحقل 16):

المصادقة بين الخوادم

الحقل 4 من الاستعلام (auth_hash) ليس السر المشترك للعنقود على مستوى النقل. إذ إن إرسال السر بصيغته الخام سيؤدي إلى فشل المصادقة وكشفه في الوقت نفسه. وبدلًا من ذلك، يثبت الخادم الذي يعمل كعميل بين الخوادم أنه يعرف السر باستخدام تجزئة SHA-256 مملّحة:
  1. الدخول إلى وضع inter-server. يشير الخادم المتصل إلى ذلك داخل ClientHello: تكون قيمة الحقل user هي وسم inter-server ويكون password فارغًا. ثم يُلحق سلسلتين إضافيتين — اسم العنقود وsalt مُولَّد حديثًا بطول 32 بايت (encodeSHA256 لقيمة عشوائية) — مباشرة بعد حقلي user/password، ضمن حزمة ClientHello نفسها. يقرأ الخادم هاتين السلسلتين قبل أن يرسل ServerHello، لذلك يجب على العميل كتابتهما مسبقًا؛ إذ إن انتظار ServerHello أولًا يسبب حالة deadlock، لأن الخادم يكون متوقفًا أثناء قراءتهما.
  2. الحصول على nonce. يحمل ServerHello قيمة nonce من النوع UInt64 بطول 8 بايت عند التفاوض على INTERSERVER_SECRET_V2 (v54462).
  3. حساب قيمة التجزئة. لكل حزمة Query غير InitialQuery، يكتب العميل encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles) في الحقل 4 — أي ناتج digest بطول 32 بايت. (تكون nonce بصيغة سلسلة عشرية، ولا تكون موجودة إلا عند التفاوض على إصدار ≥ v54462؛ ولا يُلحَق external_roles إلا عند التفاوض على INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472).) أما في حالة InitialQuery، أو عند عدم تهيئة أي سر للعنقود، فيكتب العميل سلسلة فارغة بدلًا من ذلك.
  4. التحقق. يقرأ الخادم الحقل 4 بحد أقصى 32 بايت ويعيد حساب عملية الربط نفسها باستخدام نسخته الخاصة من السر المشترك للعنقود؛ ويُرفض الاتصال إذا اختلفت قيمتا digest.
العملاء الخارجيون (غير العاملين بين الخوادم) لا يدخلون هذا الوضع مطلقًا، ويرسلون دائمًا auth_hash فارغًا.

الإعداد

يُرمَّز هذا مضمّنًا داخل قائمة الإعدادات في جسم Query (حزمة Query، الحقل 3). تكون القائمة موجودة دائمًا، بغضّ النظر عن الإصدار المتفاوض عليه، وتنتهي بإعداد ذي مفتاح فارغ — أي VarUInt 0 واحد، من دون أي flags أو قيمة بعده. ويعتمد ترميز كل إعداد فقط على الإصدار المتفاوض عليه، وتتحكم فيه SETTINGS_SERIALIZED_AS_STRINGS ‏(v54429). v54429+ (STRINGS_WITH_FLAGS) — يكون كل إعداد هو الثلاثي الموضّح هنا: يغيب الحقلان 2 و3 عندما يكون key فارغًا. قبل 54429 (BINARY) — يكون كل إعداد بالشكل [String key][type-specific binary value]: لا يُكتب الحقل flags إطلاقًا، وتُرمَّز القيمة بصيغتها الثنائية الأصلية الخاصة بالإعداد (على سبيل المثال، عدد صحيح ثابت العرض أو سلسلة مسبوقة بالطول) بدلًا من سلسلة عشرية/نصية. وتظل القائمة منتهيةً بـ key فارغ. يجب على العميل الذي يستهدف إصدارًا متفاوضًا عليه أقل من 54429 أن يقرأ هذه الصيغة الثنائية ويكتبها، لا الثلاثي أعلاه. (الإعدادات المخصّصة المعرّفة من قبل المستخدم هي الاستثناء: فهي تتضمن دائمًا flags وقيمة نصية، في كلا الترميزين.) يحتوي الحقل flags على:
  • 0x01مهم: يؤثر الإعداد في نتائج الاستعلام، ويجب ألا تتجاهله النظراء الأقدم بصمت.
  • 0x02مخصّص: إعداد مخصّص معرّف من قبل المستخدم.
  • 0x0c — حقل tier من 2 بت، وليس علامة مستقلة: 0x00 = Production، 0x04 = Obsolete، 0x08 = Experimental، 0x0c = Beta. اقرأ البتين معًا (flags & 0x0c) — لأن اختبارًا ساذجًا مثل flags & 0x04 سيُصنّف Beta (0x0c) خطأً على أنها Obsolete.
  • 0x80HotReload (إعادة تحميل config من دون إعادة تشغيل؛ معرّف في تعداد العلامات، ويظهر أساسًا في إعدادات coordination).

المعلَمة

معلمات الاستعلام، للاستعلامات ذات المعلَمات مثل SELECT {x:UInt64}. تُرمَّز بالطريقة نفسها تمامًا مثل إعداد مع ضبط العلامة Custom (0x02)، وتُنهى بمفتاح فارغ بالطريقة نفسها.
قيمة المعلَمة هي تمثيل SQL للقيمة، وليست قيمة حرفية خام. يجب تمرير المعلَمات من النوع String بعد إحاطتها مسبقًا بعلامات اقتباس مفردة (على سبيل المثال، القيمة لـ {name:String} هي 'Alice' وليست Alice)؛ وإلا فسيرفض محلل القيم في الخادم تحليلها.

Data (نوع الحزمة 1 server→client، ونوع الحزمة 2 client→server)

في كلا الاتجاهين. تحمل كتل النتائج، وبيانات INSERT، والجداول الخارجية، وعلامات نهاية البيانات. تنسيق النقل متماثلة — إذ يتضمن كلا الاتجاهين بادئة table_name قبل Block. والاختلاف الوحيد هو بايت نوع الحزمة.
وسم نهاية البيانات هو حزمة يكون فيها Block فارغًا — 0 أعمدة و0 صفوف — بغض النظر عن table_name. ويتعامل الخادم مع حزمة Data من العميل على أنها المُنهِي فقط عندما تكون كتلة البيانات المفككة فارغة (block.empty())؛ أما الحزمة التي فيها table_name = "" وBlock غير فارغ، فهي حزمة صفوف عادية وليست مُنهِيًا. لذا فإن تدفق صفوف INSERT هو تسلسل من كتل Data غير الفارغة، تتبعها كتلة Data فارغة واحدة تُنهيه. تُوثَّق متغيرات Block وما تعنيه ضمن متغيرات Block.

Progress (نوع الحزمة 3)

الخادم → العميل. تُرسَل دوريًا أثناء تنفيذ الاستعلام. جميع الحقول من نوع VarUInt، وتحمل كل حزمة الزيادات منذ حزمة Progress السابقة، لا الإجماليات التراكمية. قبل الإرسال، يقرأ الخادم عدّاداته ويُعيد ضبطها ذريًا إلى الصفر، ويحسب elapsed_ns على أنه فرق الزمن منذ آخر إرسال. لذلك يجب على العميل تجميع الحزم المتعاقبة محليًا للحصول على الإجماليات الجارية — فالتعامل مع الحزمة على أنها قيمة مطلقة يجعل عرض التقدّم يرتد إلى الخلف أو يقلّل العدد عند وصول أكثر من حزمة واحدة.

ProfileInfo (نوع الحزمة 6)

الخادم → العميل. يُرسل مرة واحدة لكل استعلام، قرب نهاية التنفيذ.

الإجماليات (نوع الحزمة 7)

الخادوم → العميل. يُرسَل للاستعلامات التي تتضمن WITH TOTALS. تنسيق النقل على مستوى النقل مطابق تمامًا لـ Data: سلسلة table_name (وتكون فارغة دائمًا) تليها كتلة. والاختلاف الوحيد هو بايت نوع الحزمة.

القيم القصوى (نوع الحزمة 8)

الخادم → العميل. تُرسَل عندما يكون الإعداد extremes مفعّلًا. تنسيق النقل مطابق تمامًا لـ Data. تحتوي الكتلة على صفّين بالضبط: يحتوي الصف 0 على الحد الأدنى لكل عمود، ويحتوي الصف 1 على الحد الأقصى.

Log (نوع الحزمة 10)

الخادم → العميل. تُرسَل هذه الحزمة عندما تكون للاستعلام قائمة انتظار سجلات نشطة (إعداد send_logs_level؛ راجع بث السجلات). تنسيق الغلاف والمحتوى مطابق لتنسيق Data. تحتوي الكتلة على قيمة ثابتة num_columns = 8 ومخطط مُعرّف مسبقًا. ويمثّل كل سطر سجل صفًا واحدًا عبر الأعمدة الثمانية جميعها، وقد تتضمن حزمة Log واحدة صفوفًا عديدة.
الأعمدة الثمانية، بهذا الترتيب تمامًا:

ProfileEvents (نوع الحزمة 14)

الخادم → العميل. يحمل عدّادات أداء لكل استعلام. له نفس تنسيق الغلاف والمحتوى كما في Data. تحتوي الكتلة على قيمة ثابتة لـ num_columns = 6 ومخطط محدد مسبقًا. ويمثل كل حدث صفًا واحدًا.
الأعمدة الستة:
نوع العنصر في العمود value ليس ثابتًا عبر الحزم — إذ ترسل الخوادم الأقدم UInt64، بينما ترسل الأحدث Int64. اقرأ سلسلة نوع العمود من ترويسة block بدلًا من افتراض عرض واحد.

TableColumns (نوع الحزمة 11)

الخادم → العميل، ويكون إرسالها مشروطًا بـ COLUMN_DEFAULTS_METADATA ‏(v54410). يرسلها الخادم قبل كتلة المخطط الخاصة بـ INSERT لنقل البيانات الوصفية للقيم الافتراضية للأعمدة، ولكن فقط عندما يكون الإصدار المتفاوض عليه ≥ 54410 و يكون الإعداد input_format_defaults_for_omitted_fields مفعّلًا. في الإصدارات الأقدم من 54410، لا تُرسل هذه الحزمة مطلقًا، لذلك يجب على العميل الأقدم ألا ينتظرها — إذ تأتي كتلة المخطط Data مباشرةً. ينبغي أن يكون عميل v54410+ مستعدًا لأيٍّ من الترتيبين: TableColumns اختيارية، ثم كتلة المخطط.
جسم مضغوط عند v54481+عند الإصدار المتفاوض عليه ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS)، يكتب الخادم كلا الحقلين عبر مسار الإخراج نفسه القابل للضغط اختياريًا، لذا عندما يكون compression = true في query، يكون جسم TableColumns بالكامل (external_table + columns_description) داخل إطار الضغط؛ ويقرأه العميل عبر stream فك الضغط المطابقة. وعندما لا تكون هناك عملية ضغط في query، يكون الجسم على wire غير مضغوط تمامًا كما يوضحه الجدول أعلاه. وهذا مهم لاستجابات كتلة المخطط الخاصة بـ INSERT: فالعميل الذي يبدّل معالجة الضغط لكل من Log وProfileEvents دون TableColumns سيُسيء قراءة الاستجابة عند تفعيل ضغط query.

TimezoneUpdate (نوع الحزمة 17)

الخادم → العميل، ويخضع لـ TIMEZONE_UPDATES ‏(v54464). تُرسَل في موضع واحد فقط: مُهيِّئ دالة الجدول input (استعلام بالصيغة INSERT INTO <table> SELECT ... FROM input('<structure>')، حيث تتدفق الصفوف من العميل). مباشرةً بعد أن يرسل الخادم كتلة Data الخاصة بمخطط الإدخال (انظر مرحلة INSERT)، يُرسل TimezoneUpdate حاملةً قيمة session_timezone الحالية في سياق الاستعلام، لكي يفسِّر العميل الصفوف التي يوشك على إرسالها باستخدام المنطقة الزمنية نفسها. لا يرسل الخادم هذه الحزمة عند أي تغييرات لاحقة على SET session_timezone أثناء الاستعلام، ولا لإبلاغ العميل بكيفية تنسيق كتل النتائج اللاحقة. تصل الحزمة مرة واحدة، مباشرةً بعد كتلة مخطط الإدخال وقبل أن يبدأ العميل في إرسال كتل الصفوف. ويجب على decoder الذي يتجاهل TimezoneUpdate أن يستهلك أيضًا قيمة String اللاحقة للحفاظ على محاذاة wire.

مصادقة SSH بالتحدي والاستجابة (أنواع الحزم 11 و12 و18)

تخضع لـ SSH_AUTHENTICATION ‏(v54466)، ولا تُفعَّل إلا عند الاختيار الصريح. يدخل الاتصال في مسار SSH عندما يرسل ClientHello القيمة user = " SSH KEY AUTHENTICATION " + <real_user> (مع المسافات البادئة واللاحقة) وpassword = "". يقرأ الخادم البادئة، ويزيلها لاستعادة اسم المستخدم الحقيقي، ثم ينتقل إلى نمط التحدي والاستجابة. يعمل هذا المسار بدلًا من password authentication، ويحدث تبادل التحدي والاستجابة قبل ServerHello — إذ يؤجل الخادم رد Hello الخاص به حتى تنجح المصادقة:
  1. يرسل العميل ClientHello مع بادئة وسم SSH وكلمة مرور فارغة.
  2. يرسل العميل SSHChallengeRequest (الحزمة 11). لم يرسل الخادم ServerHello بعد — إذ يعالج المصادقة أولًا ويتوقف هنا منتظرًا هذه الحزمة.
  3. يرد الخادم بـ SSHChallenge متضمنةً بايتات عشوائية (الحزمة 18).
  4. ينشئ العميل السلسلة المطلوب توقيعها ويوقّع هذه السلسلة، لا challenge الخام، ثم يرسل SSHChallengeResponse (الحزمة 12) مع التوقيع. الرسالة الموقَّعة هي ربط بايت-ببايت، من دون فواصل، لأربعة أجزاء بهذا الترتيب الدقيق:
  5. يتحقق الخادم من التوقيع باستخدام public key المسجل للمستخدم، مع إعادة تكوين السلسلة نفسها decimal(protocol_version) + default_database + user + challenge. عند النجاح، يرسل ServerHello — وهو الرد نفسه كما في مسار كلمة المرور — وتستمر المصافحة بشكل طبيعي (Addendum، إلخ)؛ وعند الفشل، يعيد Exception وينهي الاتصال. سيفشل العميل الذي يوقّع بايتات التحدي الخام فقط في المصادقة.
هذا عكس ما يحدث في مصافحة كلمة المرور، إذ يأتي ServerHello مباشرةً بعد ClientHello. أما في مصادقة SSH، فيُحجب ServerHello إلى ما بعد التحقق من التوقيع، لذا يتداخل تبادل التحدي والاستجابة الخاص بـ SSH ضمن المصافحة قبل ظهور أي ServerHello.
العملاء الخارجيون الذين لا يستخدمون مصادقة SSH لا يرون الحزم 11 أو 12 أو 18 مطلقًا — إذ لا تظهر على wire إلا إذا اختار المستخدم ذلك صراحةً عبر بادئة اسم المستخدم.

مرجع أنواع الحزم

العميل → الخادم

الخادم → العميل

التهيئة

يغطي هذا القسم الإعدادات القابلة للضبط التي تُحدِّد سلوك اتصالات البروتوكول الأصلي: تعكس القيم الافتراضية أدناه إصدارًا حديثًا من الخادم؛ وقد تختلف بين الإصدارات وعمليات النشر.

إعدادات طبقة النقل

خيارات المقبس

المهلات الزمنية

تتداخل المهلات الزمنية على النحو التالي:
تعمل آلية keepalive في نظام التشغيل أولًا، وقد تكتشف الأطراف النظيرة المتوقفة بصمت على مستوى النواة. وتُعد مهلة الاستقبال في التطبيق خط الدفاع التالي. أما مهلة الخمول فهي الملاذ الأخير للتخلص من الاتصالات غير المستخدمة منذ وقت طويل.

حدود الاتصالات

يمكن للاتصال الذي يرسل استعلامات بانتظام أن يبقى مفتوحًا إلى أجل غير مسمى. ولا تُغلق بعد ساعة إلا الاتصالات الخاملة، ولا يوجد حد أقصى افتراضي لمدة الاتصال.

إعدادات طبقة التطبيق

تُنقل هذه الإعدادات مع كل استعلام ضمن قائمة إعدادات حزمة Query. وهي تغيّر ما يرسله الخادم على الـwire، أو كيفية تأطيره.

الضغط

تؤدي العلامة compression في حزمة Query (الحقل 6) إلى تفعيل الضغط أو تعطيله؛ وتحدد هذه الإعدادات ترميز الضغط المستخدم عند تفعيله.

بث السجلات

يؤدي ضبط send_logs_level على أي قيمة غير "none" إلى جعل الخادم يرسل حزم Log أثناء تنفيذ الاستعلام.

الإبلاغ عن التقدّم

هذا حد أدنى مستهدف، وليس حدًا أقصى صارمًا: قد يرسل الخادوم حزم Progress بوتيرة أبطأ عندما لا يولّد الاستعلام عملاً بالسرعة الكافية.

غلاف النتيجة

INSERT غير المتزامن

التتبّع الموزّع

إعدادات خارج النطاق

يُظَنّ أحيانًا، خطأً، أن هذه الإعدادات تخص مستوى البروتوكول، لكنها تتحكم في تنفيذ SQL أو التخزين أو استخدام CPU، لا في السلوك على مستوى النقل عبر السلك. لذلك لا يحتاج تنفيذ البروتوكول إلى التعامل معها بشكل خاص.
  • max_threads — التوازي داخل تنفيذ الاستعلام.
  • max_memory_usage — الحد الأقصى للذاكرة لكل استعلام.
  • max_block_size, preferred_block_size_bytes — تحديد حجم الكتل داخليًا أثناء معالجة الاستعلام؛ أما كتل النقل عبر السلك فهي مستقلة عن هذه الإعدادات.
  • compile_expressions — الترجمة الفورية JIT؛ تخص CPU فقط.
  • async_insert_max_data_size — مخزن queue المؤقت على جهة الخادم.
  • جميع إعدادات input_format_* وoutput_format_* باستثناء مجموعة input_format_native_* / output_format_native_* — الإعدادات غير native تختار تنسيقات أخرى أو تضبطها (على سبيل المثال عبر HTTP) ولا تغيّر كتل Data في البروتوكول الأصلي.
تمثل إعدادات *_native_* الاستثناء: فهي تغيّر البايتات داخل كتل Data في native TCP، لذا يجب على أي تنفيذ للبروتوكول أن يراعيها. يبدّل output_format_native_encode_types_in_binary_format حقل type في العمود من سلسلة نصية إلى ترميز نوع ثنائي، ويُخرج output_format_native_write_json_as_string أعمدة JSON على هيئة String، ويختار output_format_native_use_flattened_dynamic_and_json_serialization تخطيط Dynamic/JSON من نوع FLATTENED. ولأن هذه الإعدادات تؤثر في جسم الكتلة لا في غلاف الحزمة، فهي محددة في مواصفة Native Format — راجع بنية الأعمدة على السلك والأنواع ذات الإصدارات.

المسرد

Cancel — حزمة يبدأها العميل (النوع 3) لإلغاء query قيد التنفيذ. لا تُعرض تفاصيلها في هذه الصفحة. وسم نهاية بيانات العميل — حزمة Data فارغة (0 columns, 0 rows) يرسلها العميل لإغلاق stream الإدخال. يختلف موضعها بحسب نوع query:
  • query عادي (SELECT, etc.): تُرسل بعد حزمة Query وأي حزم Data للجداول الخارجية للإشارة إلى “لا مزيد من البيانات الخارجية”. بعدها يبدأ server التنفيذ.
  • INSERT: لا يرسل العميل وسمًا قبل schema. يرسل server أولًا كتلة المخطط، ثم يبث العميل data blocks الخاصة بالصفوف، وبعد ذلك فقط يرسل حزمة Data الفارغة لإنهاء stream الصفوف. إن إرسال وسم فارغ قبل كتلة المخطط سيُفسَّر على أنه نهاية فورية للصفوف، ما يؤدي إلى فقدان البيانات.
Feature — تغيير في تنسيق النقل أُدخل في إصدار محدد من protocol. تكون الميزة نشطة عندما تكون protocol version المتفاوض عليها مساوية لإصدار الميزة أو أعلى منه. راجع الإصدار وبوابات الميزات. Inter-server — تسمية دور لحقل لا يكون ذا معنى إلا في distributed queries بين الخوادم. ويكتب العملاء الخارجيون default value (عادةً سلسلة فارغة أو 0 أو false). الإصدار المتفاوض عليهmin(client_version, server_version)، ويُحتسب أثناء المصافحة. ويحدد الميزات النشطة طوال lifetime الخاص بالاتصال. Packet — رسالة wire: رمز packet type من نوع VarUInt يتبعه body يعتمد تنسيقه على النوع. راجع غلاف الحزمة. رمز نوع الحزمة — قيمة VarUInt الأولى في الحزمة، وهي التي تحدد تنسيقها. القيم من 0 إلى 18 مخصَّصة حاليًا. راجع مرجع أنواع الحزم. تدفق الاستجابة — تسلسل الحزم التي يصدرها server أثناء query. وهو مفتوح الطول، وينتهي بـ EndOfStream واحد فقط (نجاح) أو Exception (فشل). راجع مرحلة query. كتلة المخطط — header block (أي Block يحتوي على columns لكن 0 rows) يرسله server أثناء مرحلة INSERT للإعلان عن أشكال columns المتوقعة قبل أن يرسل العميل البيانات. قائمة الإعدادات — تسلسل من tuples بالشكل (key, flags, value) داخل body الخاص بـ Query، وينتهي بمفتاح فارغ. يحمل configuration على مستوى التطبيق لكل query. راجع Setting. Stage — حقل VarUInt في حزمة Query (الحقل 5) يحدد إلى أي مدى ينفّذ server الـ query. يرسل العملاء الخارجيون عادةً 2 (Complete)، بينما تستخدم distributed queries وquery plans المُسلسلة القيم الأعلى. راجع الحقل 5 في Query للاطلاع على المجموعة الكاملة من قيم wire. Terminator — حزمة تُنهي stream. تنتهي استجابة Query عند EndOfStream (نجاح) أو Exception (فشل). وينتهي stream الإدخال الخاص بالعميل عند وسم Data الفارغ.
Last modified on June 25, 2026