LINUXTALKS.CO

Управляемые буферы динамического размера в go. Как правильно реализовать?

 ,

L


0

1

Пишу я свой WEB-фреймворк на go и сейчас добавляю поддержку webSocket.

У каналов в GO есть недостаток. При переполнении канала горутина «роутера» остановится ожидая пока горутина «сессии» заберёт сообщение из него. Конечно этого можно избежать если по уму писать горутины сессий. Но это всё таки фреймворк. И уж лучше пусть он в журналах ругается на расход памяти, чем зависает по причине того что JS скрипт со страницы отправил слишком много сообщений.

И я подумал, использовать карты с ключём типа int64, добавляя в них по порядку пронумерованные сообщение и удаляя прочитанные. Но мне кажется это совсем не правильное использование карт. Есть ли какие то получше механизмы реализации безразмерных буферов? Так что бы можно было знать сколько элементов накопилось и аварийно сбросить если горутина не отвечает.

★★★★★★
Ответ на: комментарий от anonymous

замонолитить свой бекенд

Я пришёл к выводу что монолит в сочетании с CDN и географическим зеркалированием является оптимальным в ближайшие 10 лет. Микросервисы же создают задержки и ошибки на ровном месте.

rezedent12    
★★★★★★
Windows / Firefox (RU)
Ответ на: комментарий от rezedent12

Каких игр? В которых клиенты легко флудят твои рутины?

А чем ты собрался синхронизировать сессии игроков, бродящих по одной игровой карте, но географически распиханых на разные экземпляры бекендов?

anonymous    

Android / Firefox (RU)
Ответ на: комментарий от anonymous

Каких игр?

Пока не буду описывать.

В которых клиенты легко флудят твои рутины?

Ну так то каждое соединение обрабатывается в своей горутине.

А чем ты собрался синхронизировать сессии игроков, бродящих по одной игровой карте, но географически распиханых на разные экземпляры бекендов?

Коли упрощённо, то в каждую горутину ввода-вывода будет передаваться указатель на структуру с данными сессии (конкретной карты). В которой помимо прочих данных, будет указатель на канал основной горутины игровой сессии.

rezedent12    
★★★★★★
Windows / Firefox (RU)
Ответ на: комментарий от rezedent12

каждое соединение обрабатывается в своей горутине

Которая является, насколько помню, «легкой» версией процесса/нити.

С таким подходом в разработке можно забыть про возможность иметь запас быстро отмолотить входящие одним инстансом.

Кроме того, ты закладываешься на географию, что уже подразумевает несколько инстансов.

Ты или крестик сними или штаны надень.

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

И где тут общий стейт по всем игрокам на конкретной карте, при условии, что их клиенты могут висеть на разных, физически не связанных инстансах?

anonymous    

Android / Firefox (RU)
Ответ на: комментарий от anonymous

Кроме того, ты закладываешься на географию, что уже подразумевает несколько инстансов.

Я подразумеваю такую возможность. Главным образом в плане использования зеркал со статичным контентом.

Которая является, насколько помню, «легкой» версией процесса/нити.

Кооперативная многозадачность без переключения контекста.

С таким подходом в разработке можно забыть про возможность иметь запас быстро отмолотить входящие одним инстансом.

Это по производительности почти эквивалентно.

И где тут общий стейт по всем игрокам на конкретной карте, при условии, что их клиенты могут висеть на разных, физически не связанных инстансах?

Каждая сессия в пределах одного сервера. Максимум играющих клиентов для одной сессии вряд ли будет больше 16. В плане производительности упор будет не в число подключенных игроков к одной сессии, а в число активных «пешек» которыми они управляют. Предвижу проблемы если пешек будет более тысячи. Но это уже не сетевая проблема, а движка игры.

rezedent12    
★★★★★★
Windows / Firefox (RU)
Ответ на: комментарий от rezedent12

Вебсокет подразумевает асинхронный дуплекс.

В твоей гошечке можно повесить нормальный ивент на чтение/запись сокета?

Если да, то одна рутина на одного клиента и она легко перелопатит всех пешек.

Если нет, то выкинь каку и возьми сишку с готовой либой вебсокетов.

anonymous    

Android / Firefox (RU)
Ответ на: комментарий от anonymous

В твоей гошечке можно повесить нормальный ивент на чтение/запись сокета?

В общем да

func main() {
	fmt.Println("Сервер запускается...")
	errLC := ReLoadConfigServer(`путь/test-config.txt`)
	if errLC == nil {
		fmt.Println("Конфигурация загружена.")
	} else {
		fmt.Println("Ошибка загрузки конфигурации...")
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/", processRequestHTTP)
	mux.HandleFunc(WebSocketDir, websocketHandler) // Для WebSocket
	server := &http.Server{
		Addr:    fmt.Sprintf(":%v", httpPort),
		Handler: mux,
	}
	go processStoper(server)
	fmt.Println("Сервер запущен на порту ", fmt.Sprintf("%v", httpPort))
	err := server.ListenAndServe()
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("Сервер остановлен")
}

При каждом новом соединении запускается новый экземпляр указанной горутины.

Если да, то одна рутина на одного клиента и она легко перелопатит всех пешек.

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

Я решил для упрощения логики обработки сложных внутренних состояний пешек, «мышление» каждой пешки обрабатывать отдельной горутиной. На входе в горутину управляющую пешкой подаётся тик-структура (множитель ускорения времени, обычно 1), а на выходе она выдаёт что делать пешке в конкретном месте и в конкретном времени. Таким образом можно будет реализовать достаточно произвольную логику без усложнения основного кода игры. Минус этого подхода в том, что save/load будет как бы сбивать фокус или внимание пешки. Но я думаю надо игровой процесс сделать таким, что бы поощрять отказ от save/load в случае неудач.

Причём здесь save/load и сетевая браузерная игра? В этом смысле игра будет вроде factorio. То есть поддерживать кооперативность. Выбор WEB в качестве интерфейса - это способ порешать проблемы сетевой синхронизации, читов и кросплатформенности. Основной фичей игры, будет моделирование душевных состояний и эволюции убеждений не напрямую контролируемых персонажей. Что то вроде rimworld с идеологией, но качественно глубже.

rezedent12    
★★★★★★
Последнее исправление: rezedent12 (всего исправлений: 1)

Windows / Firefox (RU)
Ответ на: комментарий от rezedent12

mux.HandleFunc(WebSocketDir, websocketHandler)

Это какое-то фуфло.

На базовом уровне тебе нужны два хендлера.

Первый срабатывает, когда в сокет пришли данные от клиента. В этом хендлере ты можешь вычитать из буфера сокета в свой буфер. Как только прочтешь все что доступно, то получишь EAGAIN. Если сообщение получено не полностью, то выходишь из хендлера. Если полностью, то смотришь, что просил клиент и роутишь в нужную функцию.

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

Протокол вебсокетов (полу-)бинарный. Может библиотека есть готовая?

anonymous    

Android / Firefox (RU)
Ответ на: комментарий от anonymous

Если сообщение получено не полностью, то выходишь из хендлера. Если полностью, то смотришь, что просил клиент и роутишь в нужную функцию.

Этот уровень чуть ниже того на котором я пишу.

Протокол вебсокетов (полу-)бинарный. Может библиотека есть готовая?

Уже свою почти написал:

func processWebSocket(w http.ResponseWriter, r *http.Request) { // потоки WebSocket
	var DataR CollectRequestDataHTTP
	var err error
	var WSM *WebSocketMessage

	MuCountR.Lock()
	CountR++ // счётчик горутин
	MuCountR.Unlock()

	err = DataR.ParseRequestHTTP(r) // Извлечение данных из запроса
	if err == nil {
		conn, err := upgrader.Upgrade(w, r, nil)
		if err != nil { // Если не получилось установить соединение.
			http.Error(w, "Failed to upgrade to websocket", http.StatusInternalServerError)
		} else { // Получилось установить WebSocket соединение
			defer conn.Close() // Потом закрыть
			for {
				errE := conn.SetReadDeadline(time.Now().Add(ShutdownServerTicker)) // Проверка наличия данных чтения с тайм-аутом
				if ShutdownServer {
					fmt.Println("Отладка. Остановка работы горутины принимающей WS из за остановки сервера")
					break // Выход из цикла
				}
				if errE == nil { // Есть сообщение
					typeM, message, err := conn.ReadMessage()
					if err != nil {
						fmt.Println("Error reading message:", err)
						break
					}
					WSM = newWebSocketMessage()
					WSM.LoadMessageWS(&DataR, typeM, &message)
					fmt.Printf("Отладка. Received message: %s\n", message)

				}
			}
		}
	} else {
		http.Error(w, "Failed to read to websocket", http.StatusInternalServerError)
	}

	MuCountR.Lock()
	CountR-- // счётчик горутин
	MuCountR.Unlock()
}

type WebSocketMessage struct {
	TextData   string                  // Строка текста
	BinaryData *[]byte                 // Двоичные данные
	IsText     bool                    // Если строка текста
	DataR      *CollectRequestDataHTTP // Данные исходного запроса
}

func (dataWSM *WebSocketMessage) LoadMessageWS(DataRequestWEB *CollectRequestDataHTTP, messageType int, messageBytes *[]byte) {
	dataWSM.DataR = DataRequestWEB
	dataWSM.BinaryData = messageBytes
	dataWSM.DataR = DataRequestWEB
	if messageType == websocket.TextMessage { // Текстовое сообщение
		dataWSM.IsText = true
		dataWSM.TextData = string(*messageBytes)
	}
}
func newWebSocketMessage() *WebSocketMessage { // Создание пустого экземпляра WebSocketMessage
	return &WebSocketMessage{
		TextData:   "",
		BinaryData: nil,
		IsText:     false,
		DataR:      nil,
	}
}

Сейчас думаю над архитектурой обёртки, которая позволит самой программе (игре) просто определять кому она отправляет данные, а router будут сам определять нужные сессии.

rezedent12    
★★★★★★
Windows / Firefox (RU)
Ответ на: комментарий от rezedent12

Потому что не хочу разбираться в существующих

Как ты собрался проектировать веб-фреймворк, если ты не разобрался ни в одном существующем? С чего ты взял что тебе это вообще нужно, может существующих вполне достаточно для твоих задач?

sorrow    
★★★★★★★★★★★★
Linux / Firefox (NL)
Ответ на: комментарий от sorrow

Как ты собрался проектировать веб-фреймворк, если ты не разобрался ни в одном существующем?

Ну можно сказать что go сам является фрейворком для проектирования таких фреймворков.

С чего ты взял что тебе это вообще нужно, может существующих вполне достаточно для твоих задач?

Ну я не знал что используемая мною библиотека gorilla уже считается фреймворком. Используя её пишу фреймворк более высокого уровня.

rezedent12    
★★★★★★
Windows / Firefox (RU)
Ответ на: комментарий от anonymous

Я так и не понял, где у тебя обработка ситуации, когда сообщение клиента пришло в сокет не полностью?

Гопатыч сказал что gorilla передаёт лишь завершённые сообщения.

rezedent12    
★★★★★★
Windows / Firefox (RU)

На данный момент реализация такая:
https://text-host.ru/realizatsiya-buferov-s-myagkim-limitom-na-go-gnu-gpl-v3

Копия:

type BufferWebSocketMessage struct {
	Messages    map[uint64]*WebSocketMessage // Очередь сообщений
	MuMessages  sync.Mutex                   // Мьютекс для очереди сообщений
	CountWrite  uint64                       // Индекс последнего записанного сообщения
	CountRead   uint64                       // Индекс последнего прочитанного сообщения
	LimitBuffer uint64                       // Мягкий лимит сообщений
}

func (b *BufferWebSocketMessage) Put(msg *WebSocketMessage) bool { // добавляет новое сообщение в буфер. Возвращает логическую еденицу если лимит превышен.
	var exceeded bool
	b.MuMessages.Lock()
	defer b.MuMessages.Unlock()
	t := b.CountWrite + 1    // счётчик
	if t == math.MaxUint64 { // Если очередь достигла 18446744073709551615 сообщений
		t = 0
	}
	if b.Messages == nil {
		b.Messages = make(map[uint64]*WebSocketMessage)
	}
	b.CountWrite = t
	b.Messages[b.CountWrite] = msg
	if b.LimitBuffer > 0 { // Лимит задан
		if b.CountWrite > b.CountRead {
			c := b.CountWrite - b.CountRead
			if c > b.LimitBuffer {
				exceeded = true
			}
		}
	}
	return exceeded
}
func (b *BufferWebSocketMessage) Get() (*WebSocketMessage, bool) {
	var msg *WebSocketMessage
	var ok bool
	b.MuMessages.Lock()
	defer b.MuMessages.Unlock()

	if b.CountRead == b.CountWrite {
		msg = nil
		ok = false // Нет новых сообщений
	} else {
		t := b.CountRead + 1
		if t == math.MaxUint64 { // Если счётчик достиг максимального значения
			t = 0
		}
		b.CountRead = t
		msg, ok = b.Messages[b.CountRead]
		if ok {
			delete(b.Messages, b.CountRead)
		}
	}
	return msg, ok
}
func (b *BufferWebSocketMessage) Clear() {
	b.MuMessages.Lock()
	defer b.MuMessages.Unlock()
	b.Messages = make(map[uint64]*WebSocketMessage)
	b.CountRead = 0
	b.CountWrite = 0
}

rezedent12    
★★★★★★
Последнее исправление: rezedent12 (всего исправлений: 1)

Windows / Firefox (RU)