معرفی Server-Sent Events (SSE)

رویدادهای ارسال شده توسط سرور یا SSE، یک فناوری فشار سرور است که به کلاینت‌ها امکان می‌دهد به صورت خودکار بروزرسانی‌ها را از سمت سرور از طریق یک اتصال HTTP دریافت کنند. این مشخص می‌کند که چگونه سرور پس از برقراری ارتباط اولیه با کلاینت، انتقال داده را راه‌اندازی می‌کند. این به طور معمول برای ارسال به‌روزرسانی‌های پیام یا جریان‌های پیوسته اطلاعات به مرورگرهای کلاینت استفاده می‌شود، با هدف افزایش جریان‌های محلی مرورگرها از طریق رابط برنامه‌نویسی جاوااسکریپت به نام EventSource. کلاینت‌ها می‌توانند از این API برای درخواست یک URL خاص برای دریافت جریان‌های رویداد استفاده کنند. به‌عنوان بخشی از HTML5، API EventSource توسط WHATWG استاندارد‌سازی شده‌است. نوع رسانه برای SSE، text/event-stream است.

توجه: بزرگترین تفاوت بین SSE و Websocket این است که SSE یک فشار سرور به کلاینت یک‌طرفه است، درحالیکه Websocket یک فشار دوطرفه است. گاهی وقت‌ها، زمانی که نیازهای تجاری به این تعقیب نمی‌بخشند، فشار پیام یک‌طرفه SSE کافی است. این مورد مشابه فناوری‌های استفاده شده در گفتگوی هوش مصنوعی ChatGPT است.

مثال SSE فیبر

package main

import (
	"bufio"
	"fmt"
	"log"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/valyala/fasthttp"
)

// شبیه‌سازی کلاینت HTML، اینجا یک مثال از دریافت پیام‌های فشار داده شده از سمت پشتیبانی در JS است. در سناریوهای تجاری واقعی، کلاینت مرورگر این را پیاده‌سازی می‌کند.
var index = []بایت(`<!DOCTYPE html>
<html>
<body>

<h1>SSE Messages</h1>
<div id="result"></div>

<script>
if(typeof(EventSource) !== "undefined") {
  var source = new EventSource("http://127.0.0.1:3000/sse");
  source.onmessage = function(event) {
    document.getElementById("result").innerHTML += event.data + "<br>";
  };
} else {
  document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}
</script>

</body>
</html>
`)

func main() {
	// نمونه Fiber
	app := fiber.New()

	// محدودیت‌های CORS، اجازه دسترسی از هر دامنه‌ای را می‌دهد
	app.Use(cors.New(cors.Config{
		AllowOrigins:     "*",
		AllowHeaders:     "Cache-Control",
		AllowCredentials: true,
	}))

	// دسترسی به مسیر /، ابتدا صفحه جلویی را برمی‌گرداند تا به فرمانی که فرمان مربوط به شروع فشار داده شده SSE از سمت پشتیبانی را بگیرد
	app.Get("/", func(c *fiber.Ctx) error {
		c.Response().Header.SetContentType(fiber.MIMETextHTMLCharsetUTF8)

		return c.Status(fiber.StatusOK).Send(index)
	})

	// آدرس فشار داده شده SSE
	app.Get("/sse", func(c *fiber.Ctx) error {
		// تعیین هدرهای http sse، توجه به Content-Type
		c.Set("Content-Type", "text/event-stream")
		c.Set("Cache-Control", "no-cache")
		c.Set("Connection", "keep-alive")
		c.Set("Transfer-Encoding", "chunked")

		// شروع فشار داده شده به پیام‌ها
		c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
			fmt.Println("WRITER")
			var i int
			// شبیه‌سازی فشار داده شده پیوسته به کلاینت
			for {
				i++
				msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
				// استفاده از نویسنده و تابع Fprintf برای فشار داده شده پیام به کلاینت، در سناریوهای تجاری واقعی، متن JSON برای پردازش آسان سمت جلویی ارسال می‌شود
				fmt.Fprintf(w, "data: Message: %s\n\n", msg)
				fmt.Println(msg)

				// تخلیه دیتای خروجی به کلاینت
				err := w.Flush()
				if err != nil {
					// تازه‌سازی صفحه در مرورگر و یک اتصال SSE جدید برقرار می‌کند، اما فقط آخرین پیوسته اصلی است، پس اتصالات مرده باید در اینجا بسته شوند.
					fmt.Printf("Error while flushing: %v. Closing http connection.\n", err)
					break
				}
				time.Sleep(2 * time.Second)
			}
		}))

		return nil
	})

	// شروع سرور
	log.Fatal(app.Listen(":3000"))
}