1

I'm trying to make a Client/Server chat application using java. I'm pretty new to using sockets to communicate between applications. I've decided to use ObjectInput/ObjectOutput streams to send objects between the client and server.

I'm trying to send user data to the server when the client connects to the socket. Here is the code.

Server:

private void startServer() {
        try {
            this.server = new ServerSocket(port);
            this.socket = server.accept();
            ChatUtils.log("Accepted a new connection!");
            this.output = new ObjectOutputStream(socket.getOutputStream());
            this.input = new ObjectInputStream(socket.getInputStream());
            
            try {
                User user = (User) input.readObject();
                ChatUtils.log(user.getDisplayName() + " (" + user.getUsername() + ") has connected!");
            } catch (ClassNotFoundException e) {
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Client:

public void connectToServer(int port) {
        try {
            server = new Socket("127.0.0.1", port);
            this.port = port;
            
            this.objectOutput = new ObjectOutputStream(server.getOutputStream());
            System.out.println("Connected to a server on port " + port + "!");
            objectOutput.writeObject(user);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Everything works fine, but I'm looking for some clarification as to how the methods ObjectOutputStream#writeObject() and ObjectInputStream#readObject() work.

  1. When I write the line User user = (User) input.readObject();, it reads the object as a User object. Would this only attempt to convert "User" objects that are send from the client's ObjectOutputStream?

  2. As this method is only called once, can I cast the input stream to other objects if I send those objects to the server from the output stream? Ex: String message = (String) input.readObject();.

  3. What would happen if I sent multiple objects to the server from the output stream at once?

4)In example one, I try to read the "user" object. What happens if there are two or more objects waiting to be read? How do I determine which object is which? Ex:


// Client

public void connectToServer() {
    String message = "Hello server!"
    User user = new User("John Doe", "jdoe123");
    output.writeObject(user);
    output.writeObject(message);
}

If someone could answer these questions, that'd be great. Thanks so much!
user207421
  • 305,947
  • 44
  • 307
  • 483
ElliottV4
  • 51
  • 7
  • 1. No, it will attempt to cast *any* object read from the stream to `User`, and throw a `ClassCastException` if it isn't. 2. Yes. 3. Failure. You would have to synchronize your writes, preferably on the `Socket` itself. You've forgotten to close your streams. – user207421 Aug 06 '20 at 04:30

1 Answers1

0

Every time you call .writeObject, java will take the object you specified and will serialize it.

This process is a hacky, not-recommended strategy.

Java will first attempt to break down the object you passed into its constituent parts. It will do this, hopefully, with some assistance from the class definition (the class that the object is, i.e. the one returned by theObjectWritten.getClass(). any class def that implements Serializable claims to be designed for this and gets some additional help, but the mechanism will try with reflection hacks if you don't.

Then, the constituent parts are sent along the wire (that is, take the object, and any fields that are primitives can just be sent; ObjectOutputStream knows how to send an int intrinsically, for example. Any other types are sent by, in turn, asking THAT object's class to do so). For each object, java also sends the so called 'serial version uid', which is a calculated number and changes any time any so-called signature changes anywhere in the class. It's a combination of the class's package, name, which class it extends, which interfaces it implements, and every name and type of every field (and possibly every name, return type, param types, and exception types thrown for every method).

So, now we have a bundle, consisting of:

  • The name of the class (e.g. com.foo.elliott.User)
  • The serialversionUID of the class
  • the actual data in User. If User contained any non-primitive fields, apply this process recursively.

Then this is all sent across the wire.

Then on receipt, the receiving code will take all that and pack it back into a User object. This will fail, unless the receiving end actually has com.foo.elliott.User on the classpath, and that def has the same serial version UID.

In other words, if you ever update this class, the transport fails unless the 'other side' also updates.

You can manually massage this stuff by explicitly declaring the serialVersionUID, but note that e.g. any created fields just end up being blank, even if the constructor ordinarily would ensure they could never be.

You can also fully manually manage all this by overriding some specific 'voodoo' methods (a method with a specific name. Java is ordinarily not structurally typed, but these relics of 25 years in the past, such as psv main and these methods, are the only structurally typed things in all of java).

In addition, the binary format of this data is more or less 'closed', it is not obvious, not easy to decode, and few libraries exist.

So, the upshot is:

  • It is a finicky, error ridden process.
  • Updating anything you serialize is a pain in the behind.
  • You stand no chance of ever reading this wire protocol with any programming language except java.
  • The format is neither easy to read, nor easy to work with, nor particularly compact.

This leads to the inevitable conclusion: Don't use ObjectOutputStream.

Instead, use other serialization frameworks that weren't designed 25 years ago, such as JSON or XML marshallers like google's GSON or Jackson.

NB: In addition your code is broken. Whenever you make a resource, you must also close it, and as code may exit before you get there, the only solution is a special construct. This is how to do it:

try (OutputStream out = socket.getOutputStream()) { .. do stuff here .. }

note that no matter how code 'escapes' from the braces, be it normally (run to the end of it), or because you return/break/continue out of it, or an exception is thrown, the resource is closed.

This also means assigning resources (anything that implements AutoClosable, like Socket, InputStream, and OutputStream, does so) to fields is broken, unless you make the class itself an AutoClosable, and whomever makes it, does so in one of these try-with blocks.

Finally, don't catch exceptions unless you can actually handle them, and 'printStackTrace' doesn't count. If you have no idea how to handle it, throw it onwards; declare your methods to 'throws IOException'. main can (and should!) generally be declared as throws Exception. If truly you can't, the 'stand in', forget-about-it correct way to handle this, and update your IDE to generate this instead of the rather problematic e.printStackTrace(), is this:

catch (ThingICantHandleException e) {
    throw new RuntimeException("unhandled", e);
}

Not doing so means your code continues whilst the process is in an error state, and you don't want that.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Thanks so much! So from what I read, you’re suggesting to scrap the ObjectInput/Output streams. Instead, I should use a serialization framework such as xml or json. How would I store these objects somewhere that can be accessed by two separate java apps? Would I need to use a database like MySQL? – ElliottV4 Aug 06 '20 at 02:01
  • any serialized format is basically just bytes/characters. You can toss those into a db blob, into a file, across a network, etc. But, if db is where you want to go, there are different libraries to translate objects into db table rows, such as hibernate. – rzwitserloot Aug 06 '20 at 02:30
  • Okay, thanks. Also, do I need to do this every single time I want to send an object? What if I just want to send a string from the client to the server? – ElliottV4 Aug 06 '20 at 02:39
  • What does the sentence 'Java is ordinarily not structurally typed, but these relics of 25 years in the past, such as psv main and these methods, are the only structurally typed things in all of Java' mean? – user207421 Aug 06 '20 at 04:32
  • @MarquisofLorne 'structural typing' means that anything that has a shoot() method can be treated as a Gun. Nominal typing means that things aren't a Gun unless you say they are. Java is nominal, except for psv main: A thing is runnable if it has a main method (the structure matches) - proper java would be e.g. that you declare 'public class Main extends Application { public void start(String[] args) {}}', with 'main' an abstract method in j.l.Application. Languages like python and javascript are structural (would let you blow someone's face off if you mistake a camera for a gun). Java isn't. – rzwitserloot Aug 06 '20 at 14:36