I’ve been using ObjectOutputStream. I made an Interface called GameMessage which has methods for various calls any message might need, like isHeartbeat() (a heartbeat message is not sent to client, but keeps the server from disconnecting the client) isEchoed() (an echoed message is immediately sent back to every client as long as it’s valid, useful for chatrooms etc.) isDisconnect() (sent from a client when they’ve disconnected) etc. Then I make any sort of class and as long as it’s Serializable and implements GameMessage it can be sent around no problem.
If you’re using bytes, the process is pretty much the same as sending objects or strings, you just need some way of determining what’s being sent and sending it in a reasonable way. Typically you have a certain byte value that indicates the start of a new message and/or have one to indicate the end of a message. As you receive bytes, you buffer them into a byte array, then when you’ve got a complete message you interpret what it is from your bytes. Complication here arises from needing to restrict data bytes from equaling the start/end byte values. If they do, you’re borked. To remedy this, You can also just assume that every message is X bytes long, read in that many bytes, then figure out what it means. In this case, however, you might as well just send an object or a string (in my opinion) because you’re wasting enough bytes that you’re losing the advantage of the byte stream.