The parentKey parameter should be set with the name of the chat room as the key name. This 37 | * is forced by only providing a single public constructor which has {@code String room} as the 38 | * first parameter. 39 | *
40 | */ 41 | @Entity 42 | public class ChatRoomParticipants { 43 | 44 | private static final Logger LOG = Logger.getLogger(ChatRoomParticipants.class.getName()); 45 | 46 | /* parentKey has a room name as the key name. */ 47 | @Parent 48 | private KeyThis method aggregates the participants list among multiple servers and return the 62 | * global list of the given chat room.
63 | * 64 | * @param room a name of the chat room. 65 | * @return the global list of participants of the given chat room. 66 | */ 67 | public static SetThe chat server requests us the following 2 things.
280 | *This thread watches the 2 queues on the ChatSocketServer instance, 285 | * and handles those requests in the main loop.
286 | *If this loop becomes the performance bottleneck, distribute these work loads into 287 | * multiple thread worker.
288 | */ 289 | public void run() { 290 | if (watcherThread != null) { 291 | throw new IllegalStateException("A watcherThread already exists."); 292 | } 293 | watcherThread = Thread.currentThread(); 294 | LOG.info("Namespace is set to " + namespace + " in thread " + watcherThread.toString()); 295 | NamespaceManager.set(namespace); 296 | // Store the environment for later use. 297 | backgroundEnvironment = ApiProxy.getCurrentEnvironment(); 298 | 299 | while (true) { 300 | if (Thread.currentThread().isInterrupted()) { 301 | LOG.info("ChatServerBridge is stopping."); 302 | return; 303 | } else { 304 | try { 305 | updateParticipantListAndDistribute(); 306 | propagateOneMessage(); 307 | Thread.sleep(100); 308 | } catch (InterruptedException e) { 309 | Thread.currentThread().interrupt(); 310 | break; 311 | } catch (IOException e) { 312 | LOG.warning(Throwables.getStackTraceAsString(e)); 313 | } 314 | } 315 | } 316 | } 317 | 318 | /** 319 | * Returns a Websocket URL of the chat server. 320 | * 321 | * @return a Websocket URL of the chat server. 322 | * @throws IOException when failed to get the external IP address from the metadata server. 323 | */ 324 | public String getWebSocketURL() throws IOException { 325 | return this.chatSocketServer.getWebSocketURL(); 326 | } 327 | } 328 | 329 | /** 330 | * Creates a ChatSoccketServer instance with the given network port. 331 | * 332 | * @param port a port number on which this chat server will listen. 333 | */ 334 | public ChatSocketServer(int port) { 335 | super(new InetSocketAddress(port)); 336 | metaInfoManager = new MetaInfoManager(); 337 | updateAndSendParticipantListQueue = new ConcurrentLinkedQueue<>(); 338 | propagateQueue = new ConcurrentLinkedQueue<>(); 339 | } 340 | 341 | /** 342 | * Records the incoming connection to the log. 343 | * @param conn a websocket connection object. 344 | * @param handshake a websocket handshake object. 345 | */ 346 | @Override 347 | public void onOpen(WebSocket conn, ClientHandshake handshake) { 348 | LOG.info(conn.getRemoteSocketAddress().getAddress().getHostAddress() + " entered the room!"); 349 | } 350 | 351 | /** 352 | * Removes a ConnectionInfo object associated with a given websocket connection from the 353 | * MetaInfoManager. 354 | * 355 | * @param conn a websocket connection object. 356 | * @param code an integer code that you can look up at 357 | * {@link org.java_websocket.framing.CloseFrame}. 358 | * @param reason an additional information. 359 | * @param remote Returns whether or not the closing of the connection was initiated by the remote 360 | * host. 361 | */ 362 | @Override 363 | public void onClose(WebSocket conn, int code, String reason, boolean remote) { 364 | LOG.info(conn + " has left the room!"); 365 | MetaInfoManager.ConnectionInfo connectionInfo = metaInfoManager.getConnectionInfo(conn); 366 | if (connectionInfo != null) { 367 | this.sendToClients(new ChatMessage(OutgoingMessage.MessageType.LEAVE, 368 | connectionInfo.getName(), connectionInfo.getRoom(), null)); 369 | metaInfoManager.removeConnection(conn); 370 | if (! updateAndSendParticipantListQueue.contains(connectionInfo.getRoom())) { 371 | updateAndSendParticipantListQueue.add(connectionInfo.getRoom()); 372 | } 373 | } 374 | } 375 | 376 | /** 377 | * Handles incoming messages. 378 | * 379 | * If the type of the incoming message is MessageType.ENTER, we need to check the username 380 | * against the current participant list and change the requested name with trailing underscores. 381 | * Regardless of the type, we invoke sendToClient method with every incoming messages. 382 | * 383 | * @param conn a websocket connection object. 384 | * @param rawMessage a raw message from the clients. 385 | */ 386 | @Override 387 | public void onMessage(WebSocket conn, String rawMessage) { 388 | // TODO: Make it threadsafe 389 | LOG.info(conn + ": " + rawMessage); 390 | ApiProxy.setEnvironmentForCurrentThread( 391 | ChatServerBridge.getInstance().getBackgroundEnvironment()); 392 | ChatMessage message = GSON.fromJson(rawMessage, ChatMessage.class); 393 | if (message.getType().equals(OutgoingMessage.MessageType.ENTER)) { 394 | // Check if there's a participant with the same name in the room. 395 | Set