Напишите простой push-сервис WebSocket на Go

задняя часть Go браузер WebSocket

Напишите простой push-сервис WebSocket на Go

Эта статья может бытьGitHub.com/Alfred — О, ты…Получать.

задний план

Недавно я получил требование отобразить информацию о тревоге на веб-странице. Если раньше информация о тревоге передавалась пользователям через SMS, WeChat и приложение, то теперь пользователи, вошедшие в систему, также могут получать уведомления о тревоге в режиме реального времени на веб-странице.

Я смутно припоминаю, что сталкивался с подобными потребностями, когда работал в прошлом. Поскольку предыдущие стандарты браузеров были относительно старыми и в то время больше использовалась Java, для решения этой проблемы в то время использовался Comet4J. Конкретный принцип - длинный опрос, длинная ссылка. Но теперь, когда html5 стал популярным, IE был заменен на Edge, и использование предыдущей технологии устарело.

Название WebSocket я слышал давно, но поскольку браузеры многих пользователей в то время его не поддерживали, я просто попробовал эту технологию и не слишком глубоко ее изучал. Теперь воспользуйтесь потребностями проекта, давайте немного углубимся в понимание.

Введение в веб-сокеты

Раньше браузеры получали данные на стороне сервера, отправляя HTTP-запросы и затем ожидая ответов на стороне сервера. То есть сторона браузера всегда была инициатором всего запроса, и только когда она активна, она может получить данные. Чтобы сторона браузера могла получать данные с сервера в режиме реального времени, она должна постоянно инициировать запросы к серверу. Хотя фактические данные в большинстве случаев не получаются, это сильно увеличивает нагрузку на сеть, а также резко возрастает нагрузка на сервер.

Позже мы узнали способ использовать длительное соединение + длинный опрос. Другими словами, чтобы продлить срок службы HTTP-запроса, сохранить HTTP-соединение. Хотя это снижает большое давление в определенную степень, но все равно надо постоянно опросить, вы не можете сделать настоящий в режиме реального времени. (Заимствовать карту)

long-polling

С появлением HTML5 WebSocket стал стандартом в 2011 году (см.RFC 6455).

Заимствуя слова «Go Web Programming». WebSocket использует некоторые специальные заголовки, так что браузеру и серверу нужно только выполнить рукопожатие, чтобы установить канал соединения между браузером и сервером. И соединение останется активным, и вы сможете использовать JavaScript для записи и получения данных из соединения, как если бы вы использовали обычный сокет TCP. Это решает проблему Интернета в реальном времени.

Поскольку WebSocket представляет собой полнодуплексную связь, поэтому при установлении соединения WebSocket следующая связь аналогична традиционной связи TCP. Клиент и сервер могут отправлять данные друг другу, проблем в реальном времени нет.

Выбор пакета разработки

В официальном Go SDK нет поддержки WebSocket, поэтому необходимо использовать стороннюю библиотеку.

Для разработки WebSocket с Golang выбор в основномx/net/websocketиgorilla/websocketмежду. Примеры в книге "Go Web Programming" используютx/net/websocketВ качестве комплекта для разработки, и кажется, что он также более официальный и формальный. На самом деле, судя по отзывам, которые я получил от онлайн-запросов, похоже, что это не так.x/net/websocketПохоже, что багов много, и они относительно нестабильны, а решение проблемы несвоевременно. В отличие,gorilla/websocketболее отлично.

Там естьGorilla web toolkitВклад организации должен быть оценен. 🙏. Существуют не только реализации WebSocket, но и некоторые другие инструменты. Каждый может использовать и оставлять отзывы или дополнения.

Реализация push-сервиса

Фундаментальный

Предварительный дизайн проекта выглядит следующим образом:

После запуска сервера будут зарегистрированы два обработчика.

  • websocketHandler используется для предоставления стороне браузера отправки запроса на обновление и обновления до соединения WebSocket.
  • PushHandler используется для предоставления запросов для отправки push-данных с внешних кнопочных клемм.

Браузер сначала подключается к websocketHandler (адрес по умолчаниюws://ip:port/ws) Запрос на обновление представляет собой соединение WebSocket.После установления соединения необходимо отправить регистрационную информацию для регистрации. Эта регистрационная информация содержит информацию о маркере. Сервер проверит предоставленный токен и получит соответствующий идентификатор пользователя (обычно идентификатор пользователя может быть связан со многими токенами одновременно), а также сохранит и поддержит взаимосвязь между токеном, идентификатором пользователя и соединением.

Сторона push отправляет запрос на push-данные в pushHandler (адрес по умолчанию:ws://ip:port/push), запрос содержит поле userId и поле сообщения. Сервер получит все подключения, подключенные к серверу в это время в соответствии с userId, а затем отправит сообщения одно за другим.

Из-за того, что служба push-уведомлений работает в режиме реального времени, отправляемые данные не кэшируются и не нуждаются в них.

Подробный код

Я кратко опишу базовую структуру кода здесь, и, кстати, расскажу о некоторых распространенных методах написания и паттернах на языке Go (я также перешел на язык Go с других языков, ведь язык Go — это довольно молод. Так что, если у вас есть какие-либо предложения, пожалуйста, не стесняйтесь предлагать.) Поскольку большинство изобретателей и некоторые основные разработчики языка Go пришли из языка C/C++, код языка Go также более склонен к системе C/C++.

первый взгляд наServerСтруктура:

// Server defines parameters for running websocket server.
type Server struct {
	// Address for server to listen on
	Addr string

	// Path for websocket request, default "/ws".
	WSPath string

	// Path for push message, default "/push".
	PushPath string

	// Upgrader is for upgrade connection to websocket connection using
	// "github.com/gorilla/websocket".
	//
	// If Upgrader is nil, default upgrader will be used. Default upgrader is
	// set ReadBufferSize and WriteBufferSize to 1024, and CheckOrigin always
	// returns true.
	Upgrader *websocket.Upgrader

	// Check token if it's valid and return userID. If token is valid, userID
	// must be returned and ok should be true. Otherwise ok should be false.
	AuthToken func(token string) (userID string, ok bool)

	// Authorize push request. Message will be sent if it returns true,
	// otherwise the request will be discarded. Default nil and push request
	// will always be accepted.
	PushAuth func(r *http.Request) bool

	wh *websocketHandler
	ph *pushHandler
}

PS: Так как комментарии всего моего проекта написаны на английском, прошу прощения, надеюсь не помешает читать.

ЗдесьUpgrader *websocket.Upgrader,Этоgorilla/websocketОбъект пакета, который используется для обновления HTTP-запросов.

Если структура имеет слишком много параметров, обычно не рекомендуется инициализировать ее напрямую, а использовать предоставленныеNewметод. вот:

// NewServer creates a new Server.
func NewServer(addr string) *Server {
	return &Server{
		Addr:     addr,
		WSPath:   serverDefaultWSPath,
		PushPath: serverDefaultPushPath,
	}
}

Это также распространенное использование языка Go для предоставления методов инициализации внешнему миру.

потомServerиспользоватьListenAndServeМетоды и начните порту прослушивания, а такжеhttpИспользование пакета аналогично:

// ListenAndServe listens on the TCP network address and handle websocket
// request.
func (s *Server) ListenAndServe() error {
	b := &binder{
		userID2EventConnMap: make(map[string]*[]eventConn),
		connID2UserIDMap:    make(map[string]string),
	}

	// websocket request handler
	wh := websocketHandler{
		upgrader: defaultUpgrader,
		binder:   b,
	}
	if s.Upgrader != nil {
		wh.upgrader = s.Upgrader
	}
	if s.AuthToken != nil {
		wh.calcUserIDFunc = s.AuthToken
	}
	s.wh = &wh
	http.Handle(s.WSPath, s.wh)

	// push request handler
	ph := pushHandler{
		binder: b,
	}
	if s.PushAuth != nil {
		ph.authFunc = s.PushAuth
	}
	s.ph = &ph
	http.Handle(s.PushPath, s.ph)

	return http.ListenAndServe(s.Addr, nil)
}

Здесь мы генерируем дваHandler, соответственноwebsocketHandlerиpushHandler.websocketHandlerОтвечает за установление связи с браузером и передача данных, в то время какpushHandlerЗатем обрабатывать запрос на толчок. Как вы можете видеть, есть дваHandlerупаковалbinderобъект. этоbinderдля поддержанияtoken <-> userID <-> ConnОтношение:

// binder is defined to store the relation of userID and eventConn
type binder struct {
	mu sync.RWMutex

	// map stores key: userID and value of related slice of eventConn
	userID2EventConnMap map[string]*[]eventConn

	// map stores key: connID and value: userID
	connID2UserIDMap map[string]string
}

websocketHandler

Посмотрите на это подробноwebsocketHandlerреализация.

// websocketHandler defines to handle websocket upgrade request.
type websocketHandler struct {
	// upgrader is used to upgrade request.
	upgrader *websocket.Upgrader

	// binder stores relations about websocket connection and userID.
	binder *binder

	// calcUserIDFunc defines to calculate userID by token. The userID will
	// be equal to token if this function is nil.
	calcUserIDFunc func(token string) (userID string, ok bool)
}

Очень простая структура.websocketHandlerДостигнутоhttp.Handlerинтерфейс:

// First try to upgrade connection to websocket. If success, connection will
// be kept until client send close message or server drop them.
func (wh *websocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	wsConn, err := wh.upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	defer wsConn.Close()

	// handle Websocket request
	conn := NewConn(wsConn)
	conn.AfterReadFunc = func(messageType int, r io.Reader) {
		var rm RegisterMessage
		decoder := json.NewDecoder(r)
		if err := decoder.Decode(&rm); err != nil {
			return
		}

		// calculate userID by token
		userID := rm.Token
		if wh.calcUserIDFunc != nil {
			uID, ok := wh.calcUserIDFunc(rm.Token)
			if !ok {
				return
			}
			userID = uID
		}

		// bind
		wh.binder.Bind(userID, rm.Event, conn)
	}
	conn.BeforeCloseFunc = func() {
		// unbind
		wh.binder.Unbind(conn)
	}

	conn.Listen()
}

Сначала поместите входящийhttp.Requestпреобразовать вwebsocket.Conn, а затем упакуйте его в один из наших пользовательскихwserver.Conn(Инкапсуляция или композиция типична для Go. Помните, что в Go нет наследования, только композиция). затем установитеConnизAfterReadFuncиBeforeCloseFuncметод, затем начнитеconn.Listen().AfterReadFuncозначает, когдаConnПрочитав данные, попробуйте проверить и согласноtokenрассчитатьuserID, конечноbindРегистрация привязки.BeforeCloseFuncБылConnОтвязать перед закрытием.

pushHandler

pushHandlerлегко понять. Он анализирует запрос и отправляет данные:

// Authorize if needed. Then decode the request and push message to each
// realted websocket connection.
func (s *pushHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}

	// authorize
	if s.authFunc != nil {
		if ok := s.authFunc(r); !ok {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
	}

	// read request
	var pm PushMessage
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&pm); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(ErrRequestIllegal.Error()))
		return
	}

	// validate the data
	if pm.UserID == "" || pm.Event == "" || pm.Message == "" {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(ErrRequestIllegal.Error()))
		return
	}

	cnt, err := s.push(pm.UserID, pm.Event, pm.Message)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(err.Error()))
		return
	}

	result := strings.NewReader(fmt.Sprintf("message sent to %d clients", cnt))
	io.Copy(w, result)
}

Conn

Conn(здесь имеется в видуwserver.Conn) заwebsocket.Connпакет из.

// Conn wraps websocket.Conn with Conn. It defines to listen and read
// data from Conn.
type Conn struct {
	Conn *websocket.Conn

	AfterReadFunc   func(messageType int, r io.Reader)
	BeforeCloseFunc func()

	once   sync.Once
	id     string
	stopCh chan struct{}
}

Самый важный методListen():

// Listen listens for receive data from websocket connection. It blocks
// until websocket connection is closed.
func (c *Conn) Listen() {
	c.Conn.SetCloseHandler(func(code int, text string) error {
		if c.BeforeCloseFunc != nil {
			c.BeforeCloseFunc()
		}

		if err := c.Close(); err != nil {
			log.Println(err)
		}

		message := websocket.FormatCloseMessage(code, "")
		c.Conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
		return nil
	})

	// Keeps reading from Conn util get error.
ReadLoop:
	for {
		select {
		case <-c.stopCh:
			break ReadLoop
		default:
			messageType, r, err := c.Conn.NextReader()
			if err != nil {
				// TODO: handle read error maybe
				break ReadLoop
			}

			if c.AfterReadFunc != nil {
				c.AfterReadFunc(messageType, r)
			}
		}
	}
}

В основном установите обработку, когда соединение с веб-сокетом закрыто, и продолжайте читать данные.

Трудно полностью описать процесс работы всего кода в тексте, например, чтобы прочитать код, перейдите наGitHub.com/Alfred — О, ты…Получать.

постскриптум

Код, который я протестировал, некоторое время работал в производственной среде. Но код все еще может быть недостаточно стабильным, поэтому проблемы во время использования — это нормально. Не стесняйтесь присылать мне вопросы или PR в любое время.

Ссылаться на