URL := https://buga-chat.vercel.app/
РЕПО := https://github.com/kekda-py/buga-chat
BACKEND := https://github.com/kekda-py/buga-backend
На днях я изучал go
. И был поражен его каналами. И решил сделать что-нибудь в нем. Вообще-то я думал сделать это приложение до изучения go и написал его наполовину на python
, но решил сделать его на go
, потому что go — это круто.
Также я сделал эту штуку раньше ===> dotenv
посмотрите.
Итак, мой друг nexxel рассказал мне о библиотеке fiber
, это как quart
(async flask) для go. Просматривая их документацию, я обнаружил, что с их помощью можно делать websockets. После этого я буквально выбросил код на python и начал писать его на go
.
Websocket Hub
Поскольку я был поражен каналами go. Я использовал их для создания веб-сокетного хаба. Если вы не знаете, как работают каналы. Вот простое объяснение.
Каналы Go
Каналы — это типизированный канал, через который вы можете отправлять и получать значения с помощью оператора канала, <-
.
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
Как и карты и срезы, каналы должны быть созданы перед использованием: c := make(chan T)
.
Каналы с помощью Select
Оператор select
позволяет горутине ожидать несколько коммуникационных операций.
Оператор select
блокирует выполнение одного из своих случаев, а затем выполняет этот случай. Он выбирает один случайным образом, если готовы несколько.
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
Выход:
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
BOOM!
Я определенно не копировал все это из Go Tour. О чем вы говорите?
Используя это, я сделал websocket-концентратор.
Сначала я объявил три channels
для связи между хабом и вебсокетами и map
для хранения соединений
var connections = make(map[*websocket.Conn]client)
var register = make(chan *websocket.Conn)
var broadcast = make(chan message)
var unregister = make(chan *websocket.Conn)
и структура message
для вещания
type struct message {
content string
by *websocket.Conn
}
затем в хабе я сделал оператор select с каналами в качестве случаев :-
for {
select {
case c := <- register {}
case m := <- broadcast {}
case c := <- unregister {}
}
}
<- register
просто добавляет соединение в connections
case c := <-register:
connections[c] = client{}
log.Println("client registered")
<- broadcast
принимает сообщение type message
, которое имеет атрибут by
типа *websocket.Conn
. Он перебирает соединения
и проверяет, является ли пользователь
тем, кто отправил сообщение. Если да, то просто continue
s (переход к следующей итерации). Если нет, то отправляется сообщение.
Причина, по которой я сделал это таким образом, заключается в том, что если вы отправляете сообщение, то оно появляется через несколько секунд.
case m := <-broadcast:
for c := range connections {
if c == m.by {
continue
}
if err := c.WriteMessage(websocket.TextMessage, []byte(m.content)); err != nil {
log.Println("Error while sending message: ", err)
c.WriteMessage(websocket.CloseMessage, []byte{})
c.Close()
delete(connections, c)
}
}
<- unregister
просто удаляет соединение
из соединений
.
case c := <-unregister:
delete(connections, c)
log.Println("client unregistered")
теперь websocket hub готов, осталось только запустить его
go WebsocketHub()
теперь в websocket нам просто нужно register
и также defer unregister
register <- c
defer func() {
unregister <- c
c.Close()
}
и проверить наличие сообщения
for {
mt, m, err: = c.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Println("read error:", err)
}
return // Calls the deferred function, i.e. closes the connection on error
}
if mt == websocket.TextMessage {
// MakeMessage(string(m), c)
broadcast < -message {
content: string(m),
by: c,
}
} else {
log.Println("websocket message received of type", mt)
}
}
теперь backend
закончен, перейдем к frontend
Фронтенд
Для этого проекта я использовал Next.js
с chakra-ui
.
Для вебсокетного соединения я использовал react-use-websocket
.
Итак, сначала я добавил два состояния :-
const [messages, setMessages] = useState<Messages>({});
// ^^^ for the active messages
const [msg, setMsg] = useState<string>('');
// ^^^ value of text in the message input
интерфейс Messages
— это просто
interface Messages {
[key: string]: msg;
}
и msg
:-.
interface msg {
byU : boolean;
content : string;
}
теперь пора запустить ваш бэкенд
затем добавьте переменную окружения NEXT_PUBLIC_BACKEND_URL
с url вашего бэкенда в .env.local
. вы можете использовать
dotenv change NEXT_PUBLIC_BACKEND_URL the url --file .env.local
если у вас установлен dotenv
. то получите этот url по process.env.NEXT_PUBLIC_BACKEND_URL
и подключитесь к нему, используя
const { sendMessage, lastMessage, readyState} = useWebSocket(`wss://${BACKEND}/ws`, { shouldReconnect : (closeEvent) => true } );
обязательно импортируйте useWebsocket
вместе с ReadyState
.
import useWebSocket, { ReadyState } from 'react-use-websocket';
теперь connectionStatus
:-
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState];
Для сообщений я перебирал клавиши с помощью Object.keys
и использовал .map()
для рендеринга всех из них.
{Object.keys(messages).map((key: string) => {
if (messages[key] === undefined || messages[key] === null) return null;
if (messages[key].content === undefined || messages[key].content === null)
return null;
return (
<Box
key={key}
borderRadius="lg"
bg="teal"
color="white"
width="fit-content"
px="5"
py="2"
ml={messages[key].byU ? "auto" : "0"}
>
{messages[key].content}
</Box>
)
}
)}
если сообщение отправлено вами. marginLeft
устанавливается в auto
, что сдвигает его до правой стороны.
Теперь время для проверки сообщений. Мы просто используем хук useEffect
с lastMessage
в качестве зависимости.
useEffect(() => {
if (lastMessage !== undefined || lastMessage !== null) {
(function (m: string) {
setMessages((prev: Messages) => {
let id = getUID();
while (prev[id] !== undefined || prev[id] !== undefined) {
id = getUID();
}
setTimeout(() => {
deleteMessage(id);
}, 1000 * 60);
return {
...prev,
[id]: {
byU: false,
content: m,
},
};
});
if (mute) return;
new Audio("ping.mp3").play();
})(lastMessage?.data);
}
}, [lastMessage]);
Я использую Date.now()
для идентификаторов, а также устанавливаю timeout
на 1 минуту, который запускает функцию deleteMessage
:-.
function deleteMessage(id: string) {
setMessages((prev) => {
const newMessages = { ...prev };
delete newMessages[id];
return newMessages;
});
}
Теперь для отправки сообщений мы создаем другую функцию, которая просто отправляет сообщение, используя sendMessage
, которую мы получили из useWebsocket
хука :-.
function Send() {
if (
msg.length < 1 ||
connectionStatus !== "Open" ||
msg === undefined ||
msg === null
)
return;
sendMessage(msg);
newMessage(msg, true);
setMsg("");
}
и на Enter
мы запускаем его
это реквизит на элементе ввода.
и вот вы сделали полностью анонимное приложение для чата.
запустить
yarn dev
запустить приложение в режиме разработки
Buga-Chat