استخدام Spring Boot لتنفيذ WebSocket مع STOMP
نشرت: 2022-03-11يعد بروتوكول WebSocket أحد الطرق لجعل تطبيقك يتعامل مع الرسائل في الوقت الفعلي. البدائل الأكثر شيوعًا هي الاقتراع الطويل والأحداث المرسلة من الخادم. كل من هذه الحلول مزاياه وعيوبه. في هذه المقالة ، سأوضح لك كيفية تنفيذ WebSockets مع Spring Boot Framework. سأغطي كلا من جانب الخادم والإعداد من جانب العميل ، وسوف نستخدم STOMP عبر بروتوكول WebSocket للتواصل مع بعضنا البعض.
سيتم ترميز جانب الخادم تمامًا بلغة Java. ولكن ، في حالة العميل ، سأعرض مقتطفات مكتوبة بلغة جافا وجافا سكريبت (SockJS) ، حيث يتم عادةً تضمين عملاء WebSockets في تطبيقات الواجهة الأمامية. ستوضح أمثلة الكود كيفية بث الرسائل إلى عدة مستخدمين باستخدام نموذج pub-sub وكذلك كيفية إرسال الرسائل إلى مستخدم واحد فقط. في جزء آخر من المقالة ، سأناقش بإيجاز تأمين WebSocket وكيف يمكننا ضمان بقاء حلنا المستند إلى WebSocket قيد التشغيل حتى عندما لا تدعم البيئة بروتوكول WebSocket.
يرجى ملاحظة أن موضوع تأمين WebSockets سيتم التطرق إليه هنا لفترة وجيزة فقط لأنه موضوع معقد بدرجة كافية لمقال منفصل. نتيجة لهذا ، والعديد من العوامل الأخرى التي أتطرق إليها في WebSocket في الإنتاج؟ في النهاية ، أوصي بإجراء تعديلات قبل استخدام هذا الإعداد في الإنتاج ، واقرأ حتى النهاية للحصول على إعداد جاهز للإنتاج مع وجود إجراءات أمنية مطبقة.
WebSocket وبروتوكولات STOMP
يسمح لك بروتوكول WebSocket بتنفيذ اتصال ثنائي الاتجاه بين التطبيقات. من المهم معرفة أن HTTP يستخدم فقط لتوصيل الاتصال الأولي. بعد حدوث ذلك ، تتم ترقية اتصال HTTP إلى اتصال TCP / IP مفتوح حديثًا يستخدمه WebSocket.
بروتوكول WebSocket هو بروتوكول منخفض المستوى نوعًا ما. وهي تحدد كيفية تحويل تدفق البايت إلى إطارات. قد يحتوي الإطار على نص أو رسالة ثنائية. نظرًا لأن الرسالة نفسها لا توفر أي معلومات إضافية حول كيفية توجيهها أو معالجتها ، فمن الصعب تنفيذ تطبيقات أكثر تعقيدًا دون كتابة تعليمات برمجية إضافية. لحسن الحظ ، تسمح مواصفات WebSocket باستخدام البروتوكولات الفرعية التي تعمل على مستوى تطبيق أعلى. واحد منهم ، بدعم من إطار الربيع ، هو STOMP.
STOMP هو بروتوكول مراسلة نصي بسيط تم إنشاؤه في البداية للغات البرمجة النصية مثل Ruby و Python و Perl للاتصال بوسطاء رسائل المؤسسة. بفضل STOMP ، يمكن للعملاء والوسطاء الذين تم تطويرهم بلغات مختلفة إرسال واستقبال الرسائل من وإلى بعضهم البعض. يسمى بروتوكول WebSocket أحيانًا TCP للويب. بشكل مماثل ، يُطلق على STOMP اسم HTTP للويب. وهي تحدد مجموعة من أنواع الإطارات التي تم تعيينها على إطارات WebSockets ، على سبيل المثال ، CONNECT
أو SUBSCRIBE
أو UNSUBSCRIBE
أو ACK
أو SEND
. من ناحية ، هذه الأوامر مفيدة جدًا لإدارة الاتصال ، بينما تسمح لنا من ناحية أخرى بتنفيذ حلول بميزات أكثر تعقيدًا مثل التعرف على الرسائل.
جانب الخادم: Spring Boot و WebSockets
لبناء جانب خادم WebSocket ، سنستخدم إطار عمل Spring Boot الذي يسرع بشكل كبير من تطوير التطبيقات المستقلة وتطبيقات الويب في Java. يتضمن Spring Boot وحدة spring-WebSocket
، المتوافقة مع معيار Java WebSocket API (JSR-356).
لا يعد تنفيذ WebSocket من جانب الخادم باستخدام Spring Boot مهمة معقدة للغاية ولا تتضمن سوى خطوتين ، سنستعرضها واحدة تلو الأخرى.
الخطوة 1. أولاً ، نحتاج إلى إضافة تبعية مكتبة WebSocket.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
إذا كنت تخطط لاستخدام تنسيق JSON للرسائل المرسلة ، فقد ترغب في تضمين تبعية GSON أو Jackson أيضًا. على الأرجح ، قد تحتاج أيضًا إلى إطار عمل أمني ، على سبيل المثال ، Spring Security.
الخطوة 2. بعد ذلك ، يمكننا تكوين Spring لتمكين مراسلة WebSocket و STOMP.
Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }
تقوم طريقة configureMessageBroker
بأمرين:
- ينشئ وسيط الرسائل في الذاكرة بواجهة أو أكثر لإرسال الرسائل واستلامها. في المثال أعلاه ، تم تحديد بادئتين للوجهة:
topic
وقائمةqueue
. إنهم يتبعون العرف الذي ينص على أن وجهات الرسائل التي سيتم نقلها إلى جميع العملاء المشتركين عبر نموذج pub-sub يجب أن تكون مسبوقةtopic
. من ناحية أخرى ، عادة ما تكون وجهات الرسائل الخاصة مسبوقة بقائمةqueue
. - يحدد
app
البادئة الذي يتم استخدامه لتصفية الوجهات التي يتم التعامل معها من خلال الطرق الموضحة مع@MessageMapping
والتي ستقوم بتنفيذها في وحدة تحكم. وحدة التحكم ، بعد معالجة الرسالة ، سترسلها إلى الوسيط.
بالعودة إلى المقتطف أعلاه - ربما لاحظت استدعاءًا للطريقة withSockJS()
- فهي تتيح خيارات SockJS الاحتياطية. لإبقاء الأمور قصيرة ، سيسمح لمقابس الويب الخاصة بنا بالعمل حتى إذا كان بروتوكول WebSocket غير مدعوم من مستعرض الإنترنت. سأناقش هذا الموضوع بمزيد من التفصيل قليلاً.
هناك شيء آخر يحتاج إلى توضيح - لماذا نسمي طريقة setAllowedOrigins()
على نقطة النهاية. غالبًا ما يكون مطلوبًا لأن السلوك الافتراضي لـ WebSocket و SockJS هو قبول الطلبات من نفس الأصل فقط. لذلك ، إذا كان العميل وجانب الخادم يستخدمان مجالات مختلفة ، فيجب استدعاء هذه الطريقة للسماح بالاتصال بينهما.
الخطوة 3 . قم بتطبيق وحدة تحكم تتعامل مع طلبات المستخدم. سيتم بث الرسالة المستلمة لجميع المستخدمين المشتركين في موضوع معين.
فيما يلي نموذج للطريقة التي ترسل رسائل إلى الوجهة /topic/news
.
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }
بدلاً من التعليق التوضيحي @SendTo
، يمكنك أيضًا استخدام SimpMessagingTemplate
الذي يمكنك توصيله تلقائيًا داخل وحدة التحكم الخاصة بك.
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }
في الخطوات اللاحقة ، قد ترغب في إضافة بعض الفئات الإضافية لتأمين نقاط النهاية الخاصة بك ، مثل ResourceServerConfigurerAdapter
أو WebSecurityConfigurerAdapter
من إطار عمل Spring Security. أيضًا ، غالبًا ما يكون من المفيد تنفيذ نموذج الرسالة بحيث يمكن تعيين JSON المرسل للكائنات.

بناء عميل WebSocket
يعد تنفيذ العميل مهمة أبسط.
الخطوة 1. عميل Autowire Spring STOMP.
@Autowired private WebSocketStompClient stompClient;
الخطوة 2. افتح اتصالاً.
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();
بمجرد القيام بذلك ، يمكن إرسال رسالة إلى وجهة. سيتم إرسال الرسالة إلى جميع المستخدمين المشتركين في الموضوع.
stompSession.send("topic/greetings", "Hello new user");
من الممكن أيضًا الاشتراك في الرسائل.
session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }
في بعض الأحيان تكون هناك حاجة لإرسال رسالة إلى مستخدم مخصص فقط (على سبيل المثال عند تنفيذ محادثة). بعد ذلك ، يجب على العميل والخادم استخدام وجهة منفصلة مخصصة لهذه المحادثة الخاصة. يمكن إنشاء اسم الوجهة من خلال إلحاق معرف فريد باسم الوجهة العام ، على سبيل المثال ، /queue/chat-user123
. يمكن استخدام معرفات جلسة HTTP أو جلسة STOMP لهذا الغرض.
الربيع يجعل إرسال الرسائل الخاصة أسهل كثيرًا. نحتاج فقط إلى إضافة تعليق توضيحي على طريقة وحدة التحكم باستخدام @SendToUser
. بعد ذلك ، سيتم التعامل مع هذه الوجهة بواسطة UserDestinationMessageHandler
، والتي تعتمد على معرف الجلسة. من جانب العميل ، عندما يشترك العميل في وجهة مسبوقة بـ /user
، يتم تحويل هذه الوجهة إلى وجهة فريدة لهذا المستخدم. على جانب الخادم ، يتم حل وجهة المستخدم بناءً على Principal
المستخدم.
نموذج التعليمات البرمجية من جانب الخادم مع التعليق التوضيحي @SendToUser
:
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }
أو يمكنك استخدام SimpMessagingTemplate
:
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);
دعنا الآن نلقي نظرة على كيفية تنفيذ عميل JavaScript (SockJS) قادر على تلقي الرسائل الخاصة التي يمكن إرسالها عن طريق كود Java في المثال أعلاه. تجدر الإشارة إلى أن WebSockets جزء من مواصفات HTML5 وتدعمها معظم المتصفحات الحديثة (يدعمها Internet Explorer منذ الإصدار 10).
function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }
كما لاحظت على الأرجح ، لتلقي رسائل خاصة ، يحتاج العميل إلى الاشتراك في وجهة عامة /queue/greetings
مسبوقة بـ /user
. ليس من الضروري أن تهتم بأي معرفات فريدة. ومع ذلك ، يحتاج العميل إلى تسجيل الدخول إلى التطبيق من قبل ، لذلك تتم تهيئة الكائن Principal
على جانب الخادم.
تأمين مآخذ الويب
تستخدم العديد من تطبيقات الويب المصادقة القائمة على ملفات تعريف الارتباط. على سبيل المثال ، يمكننا استخدام Spring Security لتقييد الوصول إلى صفحات أو وحدات تحكم معينة للمستخدمين المسجلين فقط. ثم يتم الحفاظ على سياق أمان المستخدم من خلال جلسة HTTP المستندة إلى ملف تعريف الارتباط والتي ترتبط لاحقًا بجلسات WebSocket أو SockJS التي تم إنشاؤها لهذا المستخدم. يمكن تأمين نقاط نهاية WebSockets مثل أي طلبات أخرى ، على سبيل المثال ، في WebSecurityConfigurerAdapter
Spring.
في الوقت الحاضر ، غالبًا ما تستخدم تطبيقات الويب واجهات برمجة تطبيقات REST باعتبارها النهاية الخلفية ورموز OAuth / JWT لمصادقة المستخدم والترخيص. لا يصف بروتوكول WebSocket كيفية قيام الخوادم بمصادقة العملاء أثناء تبادل رسائل HTTP. في الممارسة العملية ، يتم استخدام رؤوس HTTP القياسية (مثل التخويل) لهذا الغرض. لسوء الحظ ، لا يتم دعمه من قبل جميع عملاء STOMP. يسمح عميل STOMP من Spring Java بتعيين رؤوس للمصافحة:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
لكن عميل جافا سكريبت SockJS لا يدعم إرسال رأس التفويض مع طلب SockJS. ومع ذلك ، فإنه يسمح بإرسال معلمات الاستعلام التي يمكن استخدامها لتمرير رمز مميز. يتطلب هذا الأسلوب كتابة رمز مخصص في جانب الخادم والذي سيقرأ الرمز المميز من معلمات الاستعلام والتحقق من صحته. من المهم أيضًا التأكد من عدم تسجيل الرموز المميزة مع الطلبات (أو أن السجلات محمية جيدًا) لأن هذا قد يؤدي إلى انتهاك أمني خطير.
خيارات SockJS الاحتياطية
قد لا يتم دائمًا التكامل مع WebSocket بسلاسة. بعض المتصفحات (على سبيل المثال ، IE 9) لا تدعم WebSockets. علاوة على ذلك ، قد تجعل البروكسيات المقيدة من المستحيل إجراء ترقية HTTP أو تقطع الاتصالات المفتوحة لفترة طويلة جدًا. في مثل هذه الحالات ، يأتي SockJS للإنقاذ.
تنقسم عمليات نقل SockJS إلى ثلاث فئات عامة: WebSockets و HTTP Streaming و HTTP Long Polling. يبدأ الاتصال بإرسال SockJS GET /info
للحصول على المعلومات الأساسية من الخادم. بناءً على الاستجابة ، يقرر SockJS استخدام وسيلة النقل. الخيار الأول هو WebSockets. إذا لم تكن مدعومة ، فسيتم استخدام البث إن أمكن. إذا لم يكن هذا الخيار ممكنًا أيضًا ، فسيتم اختيار الاستقصاء كطريقة نقل.
WebSocket في الإنتاج؟
بينما يعمل هذا الإعداد ، فإنه ليس "الأفضل". يسمح لك Spring Boot باستخدام أي نظام مراسلة كامل مع بروتوكول STOMP (على سبيل المثال ، ActiveMQ ، RabbitMQ) ، وقد يدعم الوسيط الخارجي المزيد من عمليات STOMP (على سبيل المثال ، الإقرارات ، الإيصالات) أكثر من الوسيط البسيط الذي استخدمناه. يوفر STOMP Over WebSocket معلومات مثيرة للاهتمام حول WebSockets وبروتوكول STOMP. يسرد أنظمة المراسلة التي تتعامل مع بروتوكول STOMP ويمكن أن تكون حلاً أفضل لاستخدامه في الإنتاج. خاصة إذا كان وسيط الرسائل يحتاج إلى مجموعة ، نظرًا للعدد الكبير من الطلبات. (وسيط الرسائل البسيط في Spring غير مناسب للتجميع.) بعد ذلك ، بدلاً من تمكين الوسيط البسيط في WebSocketConfig
، يلزم تمكين وسيط Stomp الذي يقوم بإعادة توجيه الرسائل من وإلى وسيط الرسائل الخارجي. باختصار ، قد يساعدك وسيط الرسائل الخارجي في بناء حل أكثر قوة وقابلية للتوسع.
إذا كنت مستعدًا لمواصلة رحلة Java Developer الخاصة بك لاستكشاف Spring Boot ، فحاول قراءة استخدام Spring Boot لـ OAuth2 و JWT REST Protection بعد ذلك.