Напишите простой push-сервис WebSocket на Go
Эта статья может бытьGitHub.com/Alfred — О, ты…Получать.
задний план
Недавно я получил требование отобразить информацию о тревоге на веб-странице. Если раньше информация о тревоге передавалась пользователям через SMS, WeChat и приложение, то теперь пользователи, вошедшие в систему, также могут получать уведомления о тревоге в режиме реального времени на веб-странице.
Я смутно припоминаю, что сталкивался с подобными потребностями, когда работал в прошлом. Поскольку предыдущие стандарты браузеров были относительно старыми и в то время больше использовалась Java, для решения этой проблемы в то время использовался Comet4J. Конкретный принцип - длинный опрос, длинная ссылка. Но теперь, когда html5 стал популярным, IE был заменен на Edge, и использование предыдущей технологии устарело.
Название WebSocket я слышал давно, но поскольку браузеры многих пользователей в то время его не поддерживали, я просто попробовал эту технологию и не слишком глубоко ее изучал. Теперь воспользуйтесь потребностями проекта, давайте немного углубимся в понимание.
Введение в веб-сокеты
Раньше браузеры получали данные на стороне сервера, отправляя HTTP-запросы и затем ожидая ответов на стороне сервера. То есть сторона браузера всегда была инициатором всего запроса, и только когда она активна, она может получить данные. Чтобы сторона браузера могла получать данные с сервера в режиме реального времени, она должна постоянно инициировать запросы к серверу. Хотя фактические данные в большинстве случаев не получаются, это сильно увеличивает нагрузку на сеть, а также резко возрастает нагрузка на сервер.
Позже мы узнали способ использовать длительное соединение + длинный опрос. Другими словами, чтобы продлить срок службы HTTP-запроса, сохранить HTTP-соединение. Хотя это снижает большое давление в определенную степень, но все равно надо постоянно опросить, вы не можете сделать настоящий в режиме реального времени. (Заимствовать карту)
С появлением 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 в любое время.