/
ALMY
/
Chat_WebSocket
Обзор
Документация
Войти
/
ALMY
/
Chat_WebSocket
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
server.go
392 строки
15 KB
almy-win
iteration 5-optimization + fix bugs + add comments
01 июн 2026, 20:08
01 июн 2026, 20:08
2cfadb7
Код
Авторство
О чём код?
// Пакет main — точка входа в программу. package main // Импорт необходимых пакетов: import ( "bufio" // Для буферизированного чтения (например, из файла конфигурации) "encoding/json" // Для декодирования JSON-конфига "flag" // Для обработки аргументов командной строки (например, --config) "fmt" // Для форматированного вывода (fmt.Printf, fmt.Errorf) "log" // Для логирования (если включено в конфиге) "net/http" // Для HTTP-сервера и обработки запросов "os" // Для работы с файловой системой и переменными окружения "strings" // Для работы со строками (TrimPrefix, Split и т.д.) "sync" // Для безопасной работы с данными в многопоточной среде (RWMutex) "time" // Для таймеров, тиков, таймаутов "github.com/gorilla/websocket" // Популярная библиотека для работы с WebSocket ) // Config — структура, соответствующая JSON-файлу конфигурации. // Поля отображаются на ключи в config.json через теги `json:"..."`. type Config struct { Port string `json:"port"` // Порт, на котором будет слушать сервер (например, "8080") WebDir string `json:"web_dir"` // Путь к папке с HTML/CSS/JS файлами (например, "./web") MaxMessageSize int64 `json:"max_message_size"` // Макс. размер сообщения клиента в байтах ReadTimeoutSeconds int `json:"read_timeout_seconds"` // Таймаут чтения сообщений (в секундах) PingIntervalSeconds int `json:"ping_interval_seconds"` // Интервал отправки ping-сообщений клиентам MaxClients int `json:"max_clients"` // Максимальное число подключённых клиентов EnableLogging bool `json:"enable_logging"` // Включить/выключить логирование действий } // Глобальная переменная, хранящая загруженную конфигурацию. // Доступна во всём приложении. var config Config // loadConfig — загружает и проверяет корректность config.json. func loadConfig(filename string) error { // Проверяем, существует ли файл if _, err := os.Stat(filename); os.IsNotExist(err) { return fmt.Errorf("файл конфигурации %s не найден", filename) } // Открываем файл file, err := os.Open(filename) if err != nil { return err } defer file.Close() // Автоматически закроем файл после выхода из функции // Создаём JSON-декодер поверх буферизированного чтения decoder := json.NewDecoder(bufio.NewReader(file)) if err := decoder.Decode(&config); err != nil { return fmt.Errorf("ошибка парсинга JSON: %v", err) } // Валидация обязательных полей if config.Port == "" { return fmt.Errorf("в config.json отсутствует поле 'port'") } if config.WebDir == "" { return fmt.Errorf("в config.json отсутствует поле 'web_dir'") } if config.MaxMessageSize <= 0 { return fmt.Errorf("max_message_size должно быть > 0") } if config.ReadTimeoutSeconds <= 0 { return fmt.Errorf("read_timeout_seconds должно быть > 0") } if config.PingIntervalSeconds <= 0 { return fmt.Errorf("ping_interval_seconds должно быть > 0") } if config.MaxClients <= 0 { return fmt.Errorf("max_clients должно быть > 0") } return nil // Конфиг успешно загружен } // upgrader — объект Gorilla WebSocket, отвечающий за преобразование HTTP в WebSocket var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { // Разрешаем подключения с любого домена (небезопасно в продакшене!) return true }, } // Client — структура, представляющая подключённого пользователя type Client struct { Conn *websocket.Conn // Указатель на WebSocket-соединение Nick string // Никнейм пользователя (устанавливается через /nick) Send chan []byte // Канал для отправки сообщений клиенту (буферизированный) Server *Server // Ссылка на сервер, чтобы клиент мог регистрироваться и т.п. } // Server — центральное ядро чата: управляет клиентами и рассылкой type Server struct { Clients map[*Client]bool // Список активных клиентов Broadcast chan []byte // Канал для рассылки сообщений всем клиентам Register chan *Client // Канал для регистрации нового клиента Unregister chan *Client // Канал для удаления клиента Mutex sync.RWMutex // Мьютекс для потокобезопасного доступа к списку пользователей } // NewServer — конструктор сервера func NewServer() *Server { return &Server{ Clients: make(map[*Client]bool), // Инициализируем пустую карту клиентов Broadcast: make(chan []byte), // Канал для широковещательных сообщений Register: make(chan *Client), // Канал регистрации Unregister: make(chan *Client), // Канал отключения } } // Run — основной цикл сервера (запускается в горутине) func (s *Server) Run() { for { select { // Обработка регистрации нового клиента case client := <-s.Register: // Проверка лимита подключений if len(s.Clients) >= config.MaxClients { client.Send <- []byte("Сервер переполнен. Попробуйте позже.") close(client.Send) client.Conn.Close() continue } s.Clients[client] = true // Добавляем клиента if config.EnableLogging { log.Printf("%s вошёл в чат", client.Nick) } s.SendUserList() // Рассылаем обновлённый список пользователей // Обработка отключения клиента case client := <-s.Unregister: if _, ok := s.Clients[client]; ok { if config.EnableLogging { log.Printf("%s покинул чат", client.Nick) } delete(s.Clients, client) // Удаляем из списка close(client.Send) // Закрываем канал отправки s.SendUserList() // Обновляем список у всех } // Обработка широковещательного сообщения case message := <-s.Broadcast: for client := range s.Clients { select { case client.Send <- message: // Отправляем сообщение default: // Если канал переполнен — клиент "завис", удаляем его close(client.Send) delete(s.Clients, client) } } } } } // SendUserList — формирует и рассылает список активных пользователей func (s *Server) SendUserList() { s.Mutex.RLock() // Блокировка на чтение nicks := make([]string, 0, len(s.Clients)) for c := range s.Clients { if c.Nick != "" { nicks = append(nicks, c.Nick) } } s.Mutex.RUnlock() // Формируем сообщение: "/users ник1,ник2,..." listStr := "/users " + strings.Join(nicks, ",") msg := []byte(listStr) // Отправляем каждому клиенту for client := range s.Clients { client.Send <- msg } } // ReadPump — цикл чтения сообщений от клиента func (c *Client) ReadPump() { defer func() { c.Server.Unregister <- c // При выходе — уведомляем сервер c.Conn.Close() }() // Ограничиваем размер принимаемого сообщения c.Conn.SetReadLimit(config.MaxMessageSize) // Устанавливаем таймаут на чтение c.Conn.SetReadDeadline(time.Now().Add(time.Duration(config.ReadTimeoutSeconds) * time.Second)) // Обработчик pong-ответов (поддержание соединения) c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(time.Duration(config.ReadTimeoutSeconds) * time.Second)) return nil }) // Бесконечный цикл чтения for { _, message, err := c.Conn.ReadMessage() if err != nil { break // Соединение разорвано } msg := strings.TrimSpace(string(message)) // Убираем пробелы if msg == "" { continue } if config.EnableLogging { log.Printf("Получено от %s: %q", c.Nick, msg) } // Команда /nick — установка ника if c.Nick == "" && strings.HasPrefix(msg, "/nick ") { newNick := strings.TrimSpace(strings.TrimPrefix(msg, "/nick ")) if newNick != "" { c.Nick = newNick c.Send <- []byte(fmt.Sprintf("Добро пожаловать, %s!", newNick)) if config.EnableLogging { log.Printf("Установлен ник: %s", c.Nick) } c.Server.Register <- c // Регистрируем клиента только после установки ника } else { c.Send <- []byte("Имя не может быть пустым.") } continue } // Команда /getusers — запрос списка пользователей if msg == "/getusers" { c.Server.SendUserList() continue } // Личное сообщение: /msg <ник> <сообщение> if strings.HasPrefix(msg, "/msg ") { parts := splitMsg(strings.TrimPrefix(msg, "/msg ")) if len(parts) < 2 { c.Send <- []byte("Использование: /msg <ник> <сообщение>") continue } target := parts[0] body := parts[1] timestamp := time.Now().Format("15:04") // Время в формате ЧЧ:ММ var found bool // Поиск получателя по нику for client := range c.Server.Clients { if client.Nick == target { fullMsg := fmt.Sprintf("[%s %s]->[%s]: %s: %s", timestamp, c.Nick, target, c.Nick, body) client.Send <- []byte(fullMsg) if config.EnableLogging { log.Printf("%s → %s: %s", c.Nick, target, body) } found = true } } // Подтверждение отправителю if found { myMsg := fmt.Sprintf("[%s] Вы -> %s: %s", timestamp, target, body) c.Send <- []byte(myMsg) } else { c.Send <- []byte(fmt.Sprintf("Пользователь '%s' не найден.", target)) if config.EnableLogging { log.Printf("Не найден: %s (от %s)", target, c.Nick) } } continue } // Неизвестная команда c.Send <- []byte(fmt.Sprintf("Сервер: неизвестная команда: %s", msg)) } } // WritePump — цикл отправки сообщений клиенту func (c *Client) WritePump() { // Тикер для периодической отправки ping-сообщений (поддержание соединения) ticker := time.NewTicker(time.Duration(config.PingIntervalSeconds) * time.Second) defer func() { ticker.Stop() c.Conn.Close() }() for { select { // Есть сообщение для отправки case message, ok := <-c.Send: c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if !ok { // Канал закрыт — клиент отключился c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) return } // Получаем writer для отправки текстового сообщения w, err := c.Conn.NextWriter(websocket.TextMessage) if err != nil { return } w.Write(message) // Закрываем writer if err := w.Close(); err != nil { return } // Прошёл интервал — отправляем ping case <-ticker.C: c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } } // splitMsg — разделяет строку на две части по первому пробелу // Пример: "alice hello" → ["alice", "hello"] func splitMsg(s string) []string { for i, char := range s { if char == ' ' { return []string{s[:i], s[i+1:]} } } return []string{} // Если пробела нет — возвращаем пустой срез } // main — точка входа программы func main() { // Обрабатываем флаг --config configFile := flag.String("config", "config.json", "Путь к файлу конфигурации") flag.Parse() // Парсим аргументы командной строки // Загружаем конфиг if err := loadConfig(*configFile); err != nil { fmt.Fprintf(os.Stderr, "Ошибка конфигурации: %v\n", err) os.Exit(1) } // Создаём экземпляр сервера и запускаем его в фоне server := NewServer() go server.Run() // Настройка HTTP-сервера: раздача статики из config.WebDir fs := http.FileServer(http.Dir(config.WebDir)) http.Handle("/", fs) // Обработчик WebSocket-подключений http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { if config.EnableLogging { log.Print("Ошибка апгрейда до WebSocket:", err) } return } // Проверка лимита подключений if len(server.Clients) >= config.MaxClients { conn.WriteMessage(websocket.TextMessage, []byte("Сервер переполнен. Попробуйте позже.")) conn.Close() if config.EnableLogging { log.Printf("Подключение отклонено: достигнут лимит в %d пользователей", config.MaxClients) } return } // Создаём нового клиента client := &Client{ Conn: conn, Nick: "", Send: make(chan []byte, 256), // Буфер на 256 сообщений Server: server, } // Запускаем пумпы: чтение и запись go client.WritePump() go client.ReadPump() }) // Адрес сервера addr := ":" + config.Port fmt.Printf("WebSocket сервер запущен на ws://localhost%s/ws\n", addr) fmt.Printf("Веб-интерфейс доступен на http://localhost%s\n", addr) // Запуск HTTP-сервера. log.Fatal — выведет ошибку, если сервер упадёт. log.Fatal(http.ListenAndServe(addr, nil)) }