posted 11 years ago
Your run() method is almost 70 lines long, with lots of nested statements. It's no wonder you're getting exceptions that are hard to debug.
Make smaller methods, that perform smaller tasks.
You should really think about what a method is supposed to do, and not just patch code around until it looks like it works. Your exception handling looks very painful. Don't worry though, Java is difficult, and it takes a long time to figure out the general concepts.
Here's a very simple and generic server I wrote. It may overwhelm you at first, but it introduces a lot of concepts you may find useful. Use it as inspiration to write your own code (please don't copy it without understanding it):- My server uses ClientHandler to deal with incoming connections. When it accepts a connection, it passes the socket to a ClientHandler, which knows how to communicate with the Client. This way, you can use the same Server code for different applications.
- Since you want the server to go back to listening for more incoming connections as soon as possible, you handle clients in separate threads. However, you shouldn't work with raw threads. Instead, Java provides ExecutorService which manages threads for you. So my server requires an ExecutorService which it can pass jobs to (handling the clients), and then go back to its own job. You should think of an ExecutorService as a pool of threads that sit around doing nothing until you have another task for them.
- Server's constructor sets up the socket to listen for incoming connections, and initializes the client handler and the executor service. If it's not able to set up the socket, it just throws an IOException. There's no point in dealing with the exception within the constructor. If it fails, it fails.
- After creating a new server, it doesn't start listening immediately. You should do this in a separate method, because the act of listening is *not* required to create a server. Think about what a constructor is conceptually supposed to do. It only creates an object, it shouldn't perform other tasks.
- The server listens only once. After you close the server, it's done. Create a new server to start listening again. The started field keeps track of whether the server was already started once. The closed field makes sure that the server stops listening when another thread signals that the server should close. These fields are volatile, because they are updated and read from different threads, and not synchronized using the object monitor. If this went over your head, read up on Java's synchronization mechanism.
- When the server accepts a new connection, it tells the executor: "Try to handle the socket. If an exception occurs, don't deal with that exception just yet. Finally, try to close the socket. If an exception occurs while closing the socket, log that exception". It does this by passing the executor a an anonymous subclass of Callable. The call() method throws an exception if the handler fails. The reason we deal with the exception if closing the socket fails, is because you may *never* throw an exception from the finally-clause. The finally-clause should always terminate normally.
- At the end of the listen() method, we catch SocketException, and log it as an info message. The reason is that when we close the server, and the listening thread is blocked at the accept() method, it will throw a SocketException before exiting the loop. This is expected and normal behavior. We know it will happen and we thought about it.
So how do you use this server? Here's an example:What's happening here? I create the handler that will interact with all the clients, HANDLER. The pool of threads that will deal with all the various tasks is EXECUTOR. I create a new Server, and have a separate thread listen to incoming connections. The main thread waits for 10 seconds, automatically closes the server (using try-with-resource), and finally shuts down the executor. After the server has closed, and the executor has shut down, the program doesn't abruptly stop. The server simply doesn't accept new connections any longer, the executor won't perform any new tasks, but any threads that are already handling clients will keep on running normally until they are finished.
Note that I also use try-with-resources to declare the streams in and out. These will automatically be closed when the try-clause is done. I don't declare the ObjectInputStream and ObjectOutputStream as auto-closeable resources, because this will cause a subtle problem that may trigger deadlock.