Java服务端Socket

Scroll Down

概述

客户端就是向监听连接的服务器打开一个Socket的程序。不过,只有客户端Socket还不够,如果不能与服务器对话,客户端并没有什么用处。要创建一个Socket,需要知道希望连接哪个Internet主机。编写服务器时,无法预先了解哪个主机会联系你,即使确实知道,你也不清楚哪个主机希望何时与你联系。换句话说,服务器就像坐在电话旁等电话的接线员。他们不知道谁会打电话,或者什么时间打电话,只知道当电话铃响时,就必须拿起电话与之对话,而不管对方是谁。只用Socket类是做不到这一点的。
对于接受连接的服务器,Java提供了ServerSocket类表示服务器Socket。基本说来,服务器Socket的任务就是坐在电话旁边等电话。从技术上讲,服务器Socket在服务器远程主机上的一个客户端尝试连接这个端口时,服务器就被唤醒,协商建立客户端和服务器之间的连接,并返回一个常规的Socket对象,表示两台主机之间的Socket。换句话说,服务器Socket等待连接,而客户端Socket发起连接。一旦ServerSocket建立了连接,服务器会使用一个常规的Socket对象向客户端发送数据。数据总是通过常规Socket传输。

使用ServerSocket

ServerSocket类包含了使用Java编写服务器所需的全部内容。其中包含被创建新ServerSocket对象的构造函数,在指定端口监听连接的方法,配置各个服务器Socket选项的方法,以及其他一些常见的方法。
在Java中,服务器程序的基本生命周期如下:
1、使用一个ServerSocket()构造函数在一个特定端口创建一个新的ServerSocket。
2、ServerSocket使用其accept()方法监听这个端口的入站连接。accept()会一直阻塞,直到一个客户端尝试建立连接,此时accept()将返回一个连接客户端和服务器的Socket对象。
3、根据服务器的类型,会调用Socket的getInputStream()方法或getOutputStream()方法,或者这两个方法都调用,以获得与客户端通信的输入和输出流。
4、服务器和客户端根据已协商的协议交互,直到要关闭连接。
5、服务器或客户端关闭连接。
6、服务器返回步骤2,等待下一次连接。

ServerSocket server = new ServerSocket(port);
while (true) {
	try (Socket connection = server.accept()){
    	Writer out = new OutputStreamWriter(connection.getOutputStream());
        Date now = new Date();
        out.write(now.toString() + "\r\n");
        out.flush();
    } catch (IOException e) {
    	System.err.println(e.getMessage());
    }
}

这称为一个迭代服务器(iterative server)。这里有一个大循环,每次循环时分别处理一个连接。对于类似daytime这种非常简单的协议,只有很小的请求和响应,这种服务器可以很好的工作。不过,即使这么简单的一个协议,有可能会有一个速度很慢的客户端延迟其他地更快的客户端。接下来的例子会用多线程或异步IO解决这个问题。

public class MultithreadDaytimeServer {
    
    public final static int PORT = 13;

    public static void main(String[] args) {
        try (ServerSocket server = new ServerSocket(PORT)) {
            while (true) {
                try {
                    Socket connection = server.accept();
                    Thread task = new DaytimeThread(connection);
                    task.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
    
    private static class DaytimeThread extends Thread {
        private Socket connection;

        public DaytimeThread(Socket connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            } catch (IOException e) {
                System.err.println(e);
            } finally {
                try {
                    connection.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
        }
    }
}

上面的例子使用try-with-resource来自动关闭服务器Socket。不过,对于服务器接受的客户端socket这里有意没有使用try-with-resource。这是因为,客户端socket避开了try块,而放在一个单独的线程中。如果使用了try-with-resource,主线程一旦到达while循环末尾就会关闭socket,而此时新生成的线程可能还没有用完这个socket。
不过,这个服务器上确实有可能发生一种拒绝服务攻击。由于实例中为每个连接生成一个新线程,大量几乎同时的入站连接可能导致它生成大数量的线程,最终,Java虚拟机会耗尽内存而崩溃。一种更好的解决办法就是使用一个固定的线程池来限制可能的资源使用。

public class PooledDaytimeServer {

    public final static int PORT = 13;
    
    private static final ExecutorService POOL = Executors.newFixedThreadPool(50);

    public static void main(String[] args) {
        
        try (ServerSocket server = new ServerSocket(PORT)) {
            while (true) {
                try {
                    Socket connection = server.accept();
                    POOL.submit(new DaytimeThread(connection));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    private static class DaytimeThread implements Runnable {
        private Socket connection;

        public DaytimeThread(Socket connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Date now = new Date();
                out.write(now.toString() + "\r\n");
                out.flush();
            } catch (IOException e) {
                System.err.println(e);
            } finally {
                try {
                    connection.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
        }
    }
}

关闭服务器Socket

如果使用完一个服务器Socket,就应当将它关闭,特别是当程序还要继续执行一段时间更是如此。这会释放端口,使其他希望使用这个端口的程序可以使用。不要把关闭ServerSocket和关闭Socket混淆。关闭ServerSocket会释放本地主机的一个端口,允许另一个服务器绑定这个端口。它还会中断该ServerSocket已经接受的目前处于打开状态的所有Socket。