การใช้ Spring Boot สำหรับการติดตั้ง WebSocket กับ STOMP

เผยแพร่แล้ว: 2022-03-11

โปรโตคอล WebSocket เป็นหนึ่งในวิธีที่จะทำให้แอปพลิเคชันของคุณจัดการกับข้อความแบบเรียลไทม์ ทางเลือกที่พบบ่อยที่สุดคือการทำโพลแบบยาวและเหตุการณ์ที่เซิร์ฟเวอร์ส่ง แต่ละโซลูชันมีข้อดีและข้อเสีย ในบทความนี้ ผมจะแสดงให้คุณเห็นถึงวิธีการใช้ WebSockets กับ Spring Boot Framework ฉันจะครอบคลุมทั้งการตั้งค่าฝั่งเซิร์ฟเวอร์และฝั่งไคลเอ็นต์ และเราจะใช้ STOMP ผ่านโปรโตคอล WebSocket เพื่อสื่อสารระหว่างกัน

ฝั่งเซิร์ฟเวอร์จะถูกเข้ารหัสใน Java ล้วนๆ แต่ในกรณีของไคลเอนต์ ฉันจะแสดงตัวอย่างที่เขียนทั้งใน Java และ JavaScript (SockJS) เนื่องจากโดยทั่วไปแล้ว ไคลเอนต์ WebSockets จะถูกฝังในแอปพลิเคชันส่วนหน้า ตัวอย่างโค้ดจะสาธิตวิธีการเผยแพร่ข้อความไปยังผู้ใช้หลายรายโดยใช้โมเดล pub-sub ตลอดจนวิธีการส่งข้อความไปยังผู้ใช้เพียงคนเดียว ในส่วนเพิ่มเติมของบทความ ฉันจะพูดคุยสั้น ๆ เกี่ยวกับการรักษาความปลอดภัย WebSockets และวิธีที่เราสามารถมั่นใจได้ว่าโซลูชันที่ใช้ WebSocket ของเราจะยังคงทำงานแม้ว่าสภาพแวดล้อมจะไม่สนับสนุนโปรโตคอล WebSocket

โปรดทราบว่าหัวข้อของการรักษาความปลอดภัย WebSockets จะกล่าวถึงที่นี่เพียงช่วงสั้นๆ เนื่องจากเป็นหัวข้อที่ซับซ้อนเพียงพอสำหรับบทความแยกต่างหาก ด้วยเหตุนี้ และปัจจัยอื่นๆ อีกหลายอย่างที่ฉันสัมผัสใน WebSocket ในการผลิต? ในตอนท้าย ฉันแนะนำให้ทำการแก้ไขก่อนที่จะใช้การตั้งค่านี้ใน production อ่านจนจบสำหรับการตั้งค่าที่พร้อมสำหรับการผลิตโดยใช้มาตรการความปลอดภัย

โปรโตคอล WebSocket และ STOMP

โปรโตคอล WebSocket อนุญาตให้คุณใช้การสื่อสารแบบสองทิศทางระหว่างแอปพลิเคชัน สิ่งสำคัญคือต้องรู้ว่า HTTP ใช้สำหรับจับมือครั้งแรกเท่านั้น หลังจากที่เกิดขึ้น การเชื่อมต่อ HTTP จะถูกอัพเกรดเป็นการเชื่อมต่อ TCP/IP ที่เพิ่งเปิดใหม่ซึ่งใช้โดย WebSocket

โปรโตคอล WebSocket เป็นโปรโตคอลที่ค่อนข้างต่ำ กำหนดวิธีการแปลงสตรีมของไบต์เป็นเฟรม เฟรมอาจมีข้อความหรือข้อความไบนารี เนื่องจากตัวข้อความเองไม่ได้ให้ข้อมูลเพิ่มเติมเกี่ยวกับวิธีการกำหนดเส้นทางหรือประมวลผล จึงเป็นการยากที่จะปรับใช้แอปพลิเคชันที่ซับซ้อนมากขึ้นโดยไม่ต้องเขียนโค้ดเพิ่มเติม โชคดีที่ข้อกำหนด WebSocket อนุญาตให้ใช้โปรโตคอลย่อยที่ทำงานในระดับแอปพลิเคชันที่สูงขึ้น หนึ่งในนั้นได้รับการสนับสนุนโดย Spring Framework คือ 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 ทำสองสิ่ง:

  1. สร้างตัวรับ ส่งข้อความ ในหน่วยความจำที่มีปลายทางสำหรับการส่งและรับข้อความตั้งแต่หนึ่งรายการขึ้นไป ในตัวอย่างข้างต้น มีการกำหนดคำนำหน้าปลายทางสองรายการ: topic และ queue พวกเขาปฏิบัติตามข้อตกลงที่ว่าปลายทางสำหรับข้อความที่จะส่งต่อไปยังไคลเอนต์ที่สมัครรับข้อมูลทั้งหมดผ่านโมเดล pub-sub ควรนำหน้าด้วย topic ในทางกลับกัน ปลายทางสำหรับข้อความส่วนตัวมักจะนำหน้าด้วย queue
  2. กำหนด app คำนำหน้าที่ใช้ในการกรองปลายทางที่จัดการโดยวิธีการที่มีคำอธิบายประกอบด้วย @MessageMapping ซึ่งคุณจะนำไปใช้ในตัวควบคุม หลังจากประมวลผลข้อความแล้ว ผู้ควบคุมจะส่งไปยังนายหน้า

Spring Boot WebSocket: วิธีจัดการกับข้อความบนฝั่งเซิร์ฟเวอร์

วิธีจัดการกับข้อความบนฝั่งเซิร์ฟเวอร์ (ที่มา: เอกสารประกอบ Spring)


ย้อนกลับไปที่ข้อมูลโค้ดด้านบน—บางทีคุณอาจสังเกตเห็นการเรียกใช้เมธอด withSockJS() — มันเปิดใช้งานตัวเลือกทางเลือก SockJS สำรอง เพื่อให้สั้นที่สุด มันจะช่วยให้ WebSockets ของเราทำงานได้แม้ว่าอินเทอร์เน็ตเบราว์เซอร์ไม่รองรับโปรโตคอล 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 เพื่อจุดประสงค์นี้ได้

Spring ทำให้การส่งข้อความส่วนตัวง่ายขึ้นมาก เราต้องใส่คำอธิบายประกอบวิธีการของ Controller ด้วย @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 บนฝั่งเซิร์ฟเวอร์จึงถูกเตรียมใช้งาน

การรักษาความปลอดภัย WebSockets

เว็บแอปพลิเคชันจำนวนมากใช้การพิสูจน์ตัวตนแบบใช้คุกกี้ ตัวอย่างเช่น เราสามารถใช้ Spring Security เพื่อจำกัดการเข้าถึงบางหน้าหรือตัวควบคุมเฉพาะผู้ใช้ที่บันทึกไว้เท่านั้น บริบทความปลอดภัยของผู้ใช้จะได้รับการดูแลผ่านเซสชัน HTTP ที่ใช้คุกกี้ซึ่งเชื่อมโยงกับเซสชัน WebSocket หรือ SockJS ที่สร้างขึ้นสำหรับผู้ใช้นั้นในภายหลัง ปลายทาง WebSockets สามารถรักษาความปลอดภัยได้เช่นเดียวกับคำขออื่น ๆ เช่นใน WebSecurityConfigurerAdapter ของ Spring

ปัจจุบัน เว็บแอปพลิเคชันมักใช้ REST API เป็นแบ็คเอนด์และโทเค็น OAuth/JWT สำหรับการตรวจสอบสิทธิ์และการอนุญาตของผู้ใช้ โปรโตคอล WebSocket ไม่ได้อธิบายว่าเซิร์ฟเวอร์ควรตรวจสอบสิทธิ์ไคลเอ็นต์ระหว่าง HTTP handshake อย่างไร ในทางปฏิบัติ ส่วนหัว HTTP มาตรฐาน (เช่น การอนุญาต) ถูกใช้เพื่อจุดประสงค์นี้ ขออภัย ลูกค้า STOMP ทั้งหมดไม่รองรับ ไคลเอนต์ STOMP ของ Spring Java อนุญาตให้ตั้งค่าส่วนหัวสำหรับการจับมือ:

 WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);

แต่ไคลเอ็นต์ SockJS JavaScript ไม่รองรับการส่งส่วนหัวการอนุญาตด้วยคำขอ 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 ต่อไป