在学习客户端和服务器时,关于ServerSocket的相关内容

    技术2022-07-10  153

    **

    ServerSocket详解

    在客户/服务器通信模式中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户连接请求,并生成与客户端连接的Socket。

    1、构造ServerSocket ServerSocket的构造方法有以下几种重载形式:

    ServerSocket()throws IOException ServerSocket(int port) throws IOException ServerSocket(int port, int backlog) throws IOException ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

    在以上构造方法中,参数port指定服务器要绑定的端口(服务器要监听的端口),参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。

    1.1 、绑定端口 除不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端口由参数port指定。如果端口被其他服务进程占用,或是,在某些系统中,若没有以超级用户身份运行服务器程序,操作系统不允许服务器绑定到1-1023的端口时,会抛出BindException。

    1.2、设定客户连接请求队列的长度 当服务器进程运行时,可能会同时监听到多个客户的连接请求。管理客户端连接请求的任务是由操作系统来完成的。操作系统将连接请求存储在一个先进先出队列中。许多操作系统限定了队列的最大长度,一般为50。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。

    ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。

    在一下集中情况,仍然采用操作系统限定的队列最大长度:

    backlog参数的值大于操作系统限定的队列的最大长度; backlog参数的值小于或等于0;

    在ServerSocket构造方法中没有设置backlog参数。 1.3、设定绑定的IP地址 若主机只有一个地址,则服务器默认绑定该地址;若主机有多个地址,则可以调用ServerSocket(int port, int backlog, InetAddress bindAddr)构造方法设置主机ip地址。

    1.4、默认构造方法的作用 ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。

    这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。

    2、接收和关闭与客户的连接 ServerSocket的accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。

    服务器从Socket对象中获得输入流和输出流

    ,就能与客户交换数据。当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:

    java.net.SocketException: Connection reset by peer。

    3、关闭ServerSocket ServerSocket的close()方法使服务器释放占用的端口,并且断开与所有客户的连接。当一个服务器程序运行结束时,即使没有执行ServerSocket的close()方法,操作系统也会释放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行ServerSocket的close()方法。

    在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。

    ServerSocket的isClosed()方法判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false。

    ServerSocket的isBound()方法判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true。

    4、获取ServerSocket的信息

    public InetAddress getInetAddress():获取服务器绑定的ip地址; public int getLocalPort():获取服务器绑定的端口;

    在构造ServerSocket时,如果把端口设为0,那么将由操作系统为服务器分配一个端口(称为匿名端口),程序只要调用getLocalPort()方法就能获知这个端口号。多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放。

    5、ServerSocket选项 ServerSocket有以下3个选项。

    SO_TIMEOUT:表示等待客户连接的超时时间。 SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。 SO_RCVBUF:表示接收数据的缓冲区的大小。

    5.1、SO_TIMEOUT选项 设置该选项:

    public void setSoTimeout(int timeout) throws SocketException

    读取该选项:

    public int getSoTimeout () throws IOException

    SO_TIMEOUT表示ServerSocket的accept()方法等待客户连接的超时时间,以毫秒为单位。 如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。

    当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。

    5.2、SO_REUSEADDR选项 设置该选项:

    public void setResuseAddress(boolean on) throws SocketException

    读取该选项:

    public boolean getResuseAddress() throws SocketException

    这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。

    当ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口

    许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException。

    为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket.setResuseAddress(true)方法

    5.3、SO_RCVBUF选项 设置该选项:

    public void setReceiveBufferSize(int size) throws SocketException

    读取该选项:

    `public int getReceiveBufferSize() throws SocketException`

    SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。

    5.4、设定连接时间、延迟和带宽的相对重要性

    public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

    该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。

    6、创建多线程服务器 许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。

    可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:

    能同时接收并处理多个客户连接; 对于每个客户,都会迅速给予响应。 用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。

    以下将按照3中方式来实现EchoServer,它们都使用多线程。

    为每个客户分配一个工作线程。 创建一个线程池,由其中的工作线程来为客户服务。 利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。 6.1、 为每个客户分配一个线程 服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。

    代码示例:

    public static void start(){ try{ ServerSocket serverSocket = new ServerSocket(PORT); System.out.println("server listen on port:" + PORT); while (true){ try { Socket client = serverSocket.accept(); System.out.println("receive client connect, localPort=" + client.getPort()); new Thread(new EchoServer.HandlerServer(client)).start(); }catch (Exception e){ System.out.println("client exception,e=" + e.getMessage()); } } }catch(Exception e){ System.out.println("server exception,e=" + e.getMessage()); } }

    以上工作线程执行HandlerServer的run()方法,其负责与单个客户端通信,通信完毕后断开连接,线程自然终止。

    6.2、创建线程池 对每个客户都分配一个新的工作线程。当工作线程与客户通信结束,这个线程就被销毁。这种实现方式有以下不足之处:

    服务器创建和销毁工作线程的开销(包括所花费的时间和系统资源)很大。如果服务器需要与许多客户通信,并且与每个客户的通信时间都很短,那么有可能服务器为客户创建新线程的开销比实际与客户通信的开销还要大。 除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本身都会占用一定的内存(每个线程需要大约1M内存),如果同时有大量客户连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致系统的内存空间不足。 如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是相对固定的。不同操作系统有不同的切换周期,一般在20毫秒左右。这里所说的线程切换是指在Java虚拟机,以及底层操作系统的调度下,线程之间转让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切换周期,切换线程的开销甚至比创建及销毁线程的开销还大。 线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下优点

    减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务。 可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃。 6.3、使用JDK类库提供的线程池 java.util.concurrent包提供了现成的线程池的实现,其比自己实现的线程池更加健壮,且功能也更加强大。

    Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务。Executor的子接口ExecutorService中声明了管理线程池的一些方法,比如用于关闭线程池的shutdown()方法等。Executors类中包含一些静态方法,它们负责生成各种类型的线程池ExecutorService实例。

    6.4、使用线程池注意事项 虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。

    参考博客:

    http://expert.51cto.com/art/200702/40196_all.htm

    https://blog.csdn.net/lin49940/article/details/4398364

    Processed: 0.010, SQL: 9