英文:
What could cause fields in a Java object to change after being sent over a socket?
问题
这个问题看起来可能是与多线程同步相关的问题。在你的代码中,ConcurrentHashMap
和线程可能会导致一些竞争条件,尤其是在 updateMaps()
方法中,你重新创建了一个新的 ConcurrentHashMap
来复制用户列表。这可能导致不同线程之间的不一致性。
你可以尝试使用更精确的同步措施来确保线程安全性,例如使用 synchronized
块或其他并发工具来同步访问 users
集合。
另外,确保在多线程环境下正确处理对象的状态更新,特别是在 setCurrentLobbyId
方法中。
最好的方法是通过仔细地同步和排除竞争条件来确保数据的一致性,以解决这个问题。
英文:
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 String
s, IDs are UUID
s, 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 UUID
s, 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.
答案1
得分: 2
我找到了解决办法!问题甚至出现在原始数据上,不仅仅是UUID
。我想到在服务器端在out.writeObject()
之后调用out.reset()
可能有解决办法。这解决了我的问题。
似乎ObjectOutputStreams
(或者可能只是特定情况下的流,不确定)在重复写入相同对象类型时有些奇怪,即使数据略有不同也是如此。每当新客户端连接时,他们的第一个更新都是正常的,并被正确接收,这暗示了偶尔会发生奇怪情况。阅读reset()
的文档说它会重置流,就像是一个新的ObjectOuputStream
一样。
所以,主要的要点似乎是,在重复将相同的对象写入ObjectOutputStream
时,之前写入对象几乎没有变化,可能会引起问题。这可能是由于对ObjectOuputStream
内部工作的怪癖,或者可能是对其内部工作的机制有误解和/或无知所致。
多么有趣的问题!是时候去写一些Rust了xD
英文:
I have found a solution to my problem! The problem occurred even with primitives, not just UUID
s. 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
答案2
得分: -2
Re, "传引用","传值"。这个问题已经在这里讨论得非常透彻。(查看 https://stackoverflow.com/q/40480/801894)
这是它的核心:
void PBR_test() {
String a = "Yellow";
String b = "Black";
foo(a,b);
if (a == b) {
System.out.println("same color");
}
}
是否有foo(a,b)
的定义可以使PBR_test()
打印出 "same color"?
如果可能的话,那么传递给foo
的参数是按引用传递的。如果不可能,则foo
的参数是按值传递的。在Java中,这是不可能的。Java函数参数始终按值传递。
实际上,多年来还发明了一些其他奇怪的传递方式,(搜索 "Jensen's Device"),但它们中没有一个能达到传递引用和传递值所达到的受欢迎程度。
英文:
Re, "pass-by-reference," "pass-by-value". That's a question that has been absolutely beaten to death here. (See https://stackoverflow.com/q/40480/801894)
Here's the heart of it:
void PBR_test() {
String a = "Yellow";
String b = "Black";
foo(a,b);
if (a == b) {
System.out.println("same color");
}
}
Is there any definition of foo(a,b)
that can make PBR_test()
print, "same color?"
If it's possible, then the args to foo
are passed by reference.* If it's not possible, then the args to foo
are passed by value. In Java, it's not possible. Java function arguments always are passed by value.
* Actually, there are some other esoteric pass-by-xxxxx that have been invented over the years, (Google for "Jensen's Device") but none of them has achieved anywhere near the level of popularity that pass-by-reference and pass-by-value have achieved.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论