Compare commits

...

2 Commits

Author SHA1 Message Date
Struchkov Mark 7183812a79
рефакторинг 2023-06-05 00:53:48 +03:00
Struchkov Mark 4c6b969f5d
simple chat 2023-06-04 22:00:31 +03:00
11 changed files with 2303 additions and 76 deletions

2048
arch.excalidraw Normal file

File diff suppressed because one or more lines are too long

30
pom.xml
View File

@ -6,17 +6,21 @@
<groupId>dev.struchkov.example</groupId>
<artifactId>quarkus-websocket</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.11.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.1.0.Final</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.0.0</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
@ -28,6 +32,7 @@
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
@ -37,20 +42,22 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-config-yaml</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
@ -109,6 +116,7 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
@ -123,4 +131,14 @@
</properties>
</profile>
</profiles>
<developers>
<developer>
<id>uPagge</id>
<name>Struchkov Mark</name>
<email>mark@struchkov.dev</email>
<url>https://mark.struchkov.dev</url>
</developer>
</developers>
</project>

View File

@ -0,0 +1,36 @@
package dev.struchkov.example;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.server.HandshakeRequest;
import jakarta.websocket.server.ServerEndpointConfig;
import java.util.List;
import java.util.Map;
public class CustomConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
final Map<String, List<String>> headers = request.getHeaders();
final List<String> cookies = headers.get("cookie");
String sessionId = parseCookies(cookies); // ваша реализация парсинга кук
config.getUserProperties().put("sessionId", sessionId);
}
public String parseCookies(List<String> cookies) {
if (cookies != null) {
for (String cookie : cookies) {
String[] singleCookie = cookie.split(";");
for (String part : singleCookie) {
part = part.trim();
if (part.startsWith("sessionId")) {
return part.substring("sessionId".length() + 1).trim();
}
}
}
}
return null;
}
}

View File

@ -1,12 +0,0 @@
package dev.struchkov.example;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithName;
@ConfigMapping(prefix = "greeting")
public interface GreetingConfig {
@WithName("message")
String message();
}

View File

@ -1,17 +0,0 @@
package dev.struchkov.example;
import org.eclipse.microprofile.graphql.DefaultValue;
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Query;
@GraphQLApi
public class HelloGraphQLResource {
@Query
@Description("Say hello")
public String sayHello(@DefaultValue("World") String name) {
return "Hello " + name;
}
}

View File

@ -1,41 +0,0 @@
package dev.struchkov.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.websocket.EncodeException;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import static java.util.Objects.requireNonNull;
@ServerEndpoint("/start-websocket/{name}")
@ApplicationScoped
public class StartWebSocket {
@OnOpen
public void onOpen(Session session, @PathParam("name") String name) {
System.out.println("onOpen> " + name);
}
@OnClose
public void onClose(Session session, @PathParam("name") String name) {
System.out.println("onClose> " + name);
}
@OnError
public void onError(Session session, @PathParam("name") String name, Throwable throwable) {
System.out.println("onError> " + name + ": " + throwable);
}
@OnMessage
public void onMessage(String message, @PathParam("name") String name) {
System.out.println("onMessage> " + name + ": " + message);
}
}

View File

@ -0,0 +1,108 @@
package dev.struchkov.example;
import dev.struchkov.example.converter.ChatMessageDecoder;
import dev.struchkov.example.converter.ChatMessageEncoder;
import dev.struchkov.example.domain.ChatInputMessage;
import dev.struchkov.example.domain.ChatOutputMessage;
import io.vertx.ext.web.handler.HttpException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
@ServerEndpoint(
value = "/chat/{chatId}",
decoders = ChatMessageDecoder.class,
encoders = ChatMessageEncoder.class,
configurator = CustomConfigurator.class
)
@RequiredArgsConstructor
public class WebSocket {
private final Map<String, List<Session>> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("chatId") String chatId) {
System.out.println("onOpen> " + chatId);
final String authCookieValue = (String) session.getUserProperties().get("sessionId");
final UUID authUserId = getAuthUser(authCookieValue);
session.getUserProperties().put("userId", authUserId);
sessions.computeIfAbsent(chatId, key -> new ArrayList<>()).add(session);
}
private UUID getAuthUser(String authCookieValue) {
// your auth logic here
if (authCookieValue == null) throw new HttpException(401, "Не передан параметр авторизации.");
if (authCookieValue.equals("user1")) return UUID.fromString("09e429de-a302-40b6-9d10-6b113ab9e89d");
if (authCookieValue.equals("user2")) return UUID.fromString("f84dbae1-f9a9-4c37-8922-4eb207103676");
throw new HttpException(403, "Пользователь не авторизован.");
}
@OnError
public void onError(Session session, @PathParam("chatId") String chatId, Throwable throwable) {
if (throwable instanceof HttpException httpException) {
final int statusCode = httpException.getStatusCode();
if (statusCode == 401) {
session.getAsyncRemote().sendText("Вы не авторизованы.");
closeSession(session, chatId);
return;
}
if (statusCode == 403) {
session.getAsyncRemote().sendText("Доступ запрещен.");
closeSession(session, chatId);
return;
}
}
System.out.println("onError> " + chatId + ": " + throwable);
}
@OnClose
public void onClose(Session session, @PathParam("chatId") String chatId) {
System.out.println("onClose> " + chatId);
closeSession(session, chatId);
}
@OnMessage
public void onMessage(Session session, @PathParam("chatId") String chatId, ChatInputMessage message) {
System.out.println("onMessage> " + chatId + ": " + message);
sendMessage(session, chatId, message);
}
private void sendMessage(Session session, String chatId, ChatInputMessage message) {
final List<Session> chatSessions = sessions.get(chatId);
for (Session chatSession : chatSessions) {
if (session.getId().equals(chatSession.getId())) {
continue;
}
final UUID fromUserId = (UUID) session.getUserProperties().get("userId");
final ChatOutputMessage outputMessage = new ChatOutputMessage(fromUserId, message.getText());
chatSession.getAsyncRemote().sendObject(outputMessage);
}
}
private void closeSession(Session session, String chatId) {
final List<Session> chatSessions = sessions.get(chatId);
final Iterator<Session> sessionIterator = chatSessions.iterator();
while (sessionIterator.hasNext()) {
final Session chatSession = sessionIterator.next();
if (session.getId().equals(chatSession.getId())) {
sessionIterator.remove();
break;
}
}
}
}

View File

@ -0,0 +1,35 @@
package dev.struchkov.example.converter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.struchkov.example.domain.ChatInputMessage;
import jakarta.websocket.DecodeException;
import jakarta.websocket.Decoder;
import lombok.SneakyThrows;
public class ChatMessageDecoder implements Decoder.Text<ChatInputMessage> {
private final ObjectMapper jackson = ChatMessageDecoder.getJackson();
@Override
@SneakyThrows
public ChatInputMessage decode(String s) throws DecodeException {
return jackson.readValue(s, ChatInputMessage.class);
}
@Override
public boolean willDecode(String s) {
return s != null;
}
public static ObjectMapper getJackson() {
ObjectMapper om = new ObjectMapper();
om.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
om.registerModule(new JavaTimeModule());
return om;
}
}

View File

@ -0,0 +1,19 @@
package dev.struchkov.example.converter;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.struchkov.example.domain.ChatOutputMessage;
import jakarta.websocket.EncodeException;
import jakarta.websocket.Encoder;
import lombok.SneakyThrows;
public class ChatMessageEncoder implements Encoder.Text<ChatOutputMessage> {
private final ObjectMapper jackson = ChatMessageDecoder.getJackson();
@Override
@SneakyThrows
public String encode(ChatOutputMessage chatOutputMessage) throws EncodeException {
return jackson.writeValueAsString(chatOutputMessage);
}
}

View File

@ -0,0 +1,14 @@
package dev.struchkov.example.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ChatInputMessage {
private String text;
}

View File

@ -0,0 +1,19 @@
package dev.struchkov.example.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.UUID;
@Getter
@Setter
@ToString
@AllArgsConstructor
public class ChatOutputMessage {
private UUID fromUserId;
private String text;
}