Search code examples
javasocketsserializationconcurrencydeserialization

What could cause fields in a Java object to change after being sent over a socket?


I am experiencing a very strange problem. I am sending an object that has two lists (in my case, synchronized ArrayLists) from a server to a client. One is a list of users connected to the server, and the other is a list of lobbies that exist on the server. Every lobby also has its own list of users that are connected to it. After creating the object that encapsulates all of these lists and their data on the server side, I walk through the user list and print out each user's username as well as the ID of the lobby they are currently in. I then send the object using an ObjectOutputStream over a socket.

On the client side I receive the object using an ObjectInputStream, cast it to my encapsulating object type, and then again walk through the user list, printing out each user's username and the ID of the lobby that they are in.

Here is where my problem occurs. Every user's username (and other data) is printed correctly, except the ID of the lobby that they are in. This is somehow null, even though the printing on the server side printed the IDs correctly.

Usernames are Strings, IDs are UUIDs, and users and lobbies each have their own respective class that I created.

I originally thought this could be some issue with passing by reference vs by value, or an issue with UUIDs, but even after changing IDs to be just a normal int, this still happens.

I then thought it could be my list structure. At the time I was using a ConcurrentHashMap to store users and lobbies. I changed it to a synchronized list, but the same thing kept happening.

Note that when a new user connects, they receive a correct copy of the lists. But any lobbies that they or others join/leave are again not reflected correctly. Everything on the server side tracks and prints correctly though.

This is a relatively big project for me, and everything is rather connected, so I am unsure what code examples or output I should include. Please feel free to suggest anything that should be included/specified in my question, and I will add it. I'm still new to StackOverflow.

Any and all help is greatly appreciated!

EDIT: Here is a minimal reproducible example, sorry that it is so long.

Server.java:

import java.io.IOException;
import java.net.ServerSocket;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class Server implements Runnable {
  private final ServerSocket serverSocket;
  private ConcurrentHashMap<UUID, User> users;

  public Server(int port) throws IOException {
    serverSocket = new ServerSocket(port);
    users = new ConcurrentHashMap<>();
  }

  public ConcurrentHashMap<UUID, User> getUsers() {
    return users;
  }

  public void addUser(User user) {
    users.put(user.getId(), user);
  }

  private void setUserCurrentLobbyId(User user, UUID lobbyId) {
    users.get(user.getId()).setCurrentLobbyId(lobbyId);
  }

  @Override
  public void run() {
    while (true) {
      try {
        (new Thread(new ClientHandler(this, serverSocket.accept()))).start();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public static void main(String[] args) throws IOException, InterruptedException {
    Server server = new Server(5050);
    (new Thread(server)).start();
    User user1 = new User("user1");
    server.addUser(user1);
    Thread.sleep(5000);
    server.setUserCurrentLobbyId(user1, UUID.randomUUID());
  }
}

ClientHandler.java:

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class ClientHandler implements Runnable {
  private final Server server;
  private final Socket socket;
  private ConcurrentHashMap<UUID, User> users;

  public ClientHandler(Server server, Socket socket) {
    this.server = server;
    this.socket = socket;
    users = new ConcurrentHashMap<>();
  }

  @Override
  public void run() {
    ObjectOutputStream out;
    try {
      out = new ObjectOutputStream(socket.getOutputStream());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    while (true) {
      updateMaps();
      Response response = new Response(users);

      System.out.println("Sent user list:");
      for (Map.Entry<UUID, User> entry : response.users().entrySet()) {
        System.out.println("\t" + entry.getValue().getUsername()
            + ", currentLobbyId: "
            + entry.getValue().getCurrentLobbyId());
      }

      try {
        out.writeObject(response);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }

      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public void updateMaps() {
    this.users = new ConcurrentHashMap<>(server.getUsers());
  }
}

Client.java:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.Map;
import java.util.UUID;

public class Client {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    Socket socket = new Socket("localhost", 5050);

    ObjectInputStream in = new ObjectInputStream(socket.getInputStream());

    while (true) {
      Response response = (Response) in.readObject();

      System.out.println("Received user list:");
      for (Map.Entry<UUID, User> entry : response.users().entrySet()) {
        System.out.println("\t" + entry.getValue().getUsername()
            + ", currentLobbyId: "
            + entry.getValue().getCurrentLobbyId());
      }
    }
  }
}

User.java:

import java.io.Serializable;
import java.util.UUID;

public class User implements Serializable {
  private final UUID id;
  private final String username;
  private int currentLobbyId;

  public User(String username) {
    this.id = UUID.randomUUID();
    this.username = username;
  }

  public UUID getId() {
    return id;
  }

  public String getUsername() {
    return username;
  }

  public int getCurrentLobbyId() {
    return currentLobbyId;
  }

  public void setCurrentLobbyId(int newLobbyId) {
    currentLobbyId = newLobbyId;
  }
}

Response.java util class (I use this in my project as well, along with some added features that I have omitted here):

import java.io.Serializable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public record Response(
    ConcurrentHashMap<UUID, User> users)
    implements Serializable {
}

To see the issue, run Server.java in one window and then Client.java in another, and compare their outputs. After 5 seconds a simulated lobby change happens. The sender prints the updated list correctly, but the receiver does not. Sorry if this MRE is a bit long, I wasn't too sure how far I could cut without removing important context.


Solution

  • I have found a solution to my problem! The problem occurred even with primitives, not just UUIDs. I came upon the idea to call out.reset() after out.writeObject() on the server side. This fixed my issue.

    It seems that ObjectOutputStreams (or perhaps just streams in particular, unsure) are funky when it comes to writing the same object type over and over again, with minimally varying data. Whenever a new client connected, their first update was fine and was received correctly, hinting that something funky was happening only on occasion. Reading reset()'s documentation said it reset the stream as though it was a new ObjectOuputStream.

    So, the main takeaway seems to be that, in the case of repeatedly writing the same object to an ObjectOutputStream, with little to no variation on the previously written object, it could cause issues. This is probably due to a quirk of, or perhaps a misunderstanding and/or ignorance regarding, the inner workings of an ObjectOuputStream.

    What an interesting problem! Time to go write some Rust xD