Java网络编程(三)ServerSocket用法浅学

第三章 ServerSoclet用法解析

前言

在客户/服务器通信模式中,服务器需要创建监听特定端口的ServerSocket,它负责接受客户端的连接请求。本章的学习内容:

  • 1、ServerSocket的构造函数及其区别
  • 2、ServerSocket的成员函数的用法
  • 3、ServerSocket服务器是如何处理与多个客户通信的问题——多线程
  • 4、java.util.concurrent包中线程池类的用法

3.1 ServerSocket的构造器

构造器 描述
ServerSocket()throws IOException; 无参构造器,不会分配端口绑定和IP绑定,使用需要绑定端口和IP地址
ServerSocket(int port)throws IOException; 绑定指定的端口号port
ServerSocket(int port,int backlog)throws IOException; 绑定指定的端口号,同时指定客户请求连接队列长度
ServerSocket(int port,int backlog,InetAddress bindAddr)throws IOException; 绑定指定端口,指定队列长度,指定IP地址

3.1.1 端口绑定

从ServerSocket的构造器我们可以看出除了无参构造函数以外,其他的构造器都会和一个指定端口绑定。使用参数port指定。 例如:

//创建一个服务器,绑定到80端口
ServerSocket serverSocket = new ServerSocket(80);

创建过程中,如果无法绑定到该端口,则会抛出异常:IOException,更确切地说是抛出BindException,它是IOException的子类。抛出BindException一般是如下原因造成的:

  • 端口已经被其他的服务器进程使用
  • 某些操作系统中,如果没有以管理员的身份运行服务器程序,不允许服务器绑定到0~1023之间的端口

匿名端口

将port设置为0时,则表示由操作系统来给服务器随机分配一个可用的端口。由操作系统分配的端口叫做匿名端口。对于多数服务器一般使用指定的端口,而不是匿名端口,方便客户端连接。当然,某些场合下匿名端口有特殊用途。

3.1.2 如何设置客户端的连接请求队列长度

当服务器运行时,可能监听到多个客户的连接。例如:

Socket socket = new Socket("www.baidu.com",80)

每当一个这样的客户进程执行上面的代码,就会与远程服务器www.baidu.com的80端口连接,也就是说服务器监听到了一个客户连接。管理客户连接请求的任务是由操作系统来完成的,操作系统将这些请求连接存放于一个先进先出队列里边特别的,操作系统限定了连接队列的长度,一般为50。当队列中的连接请求达到了上限,则此时服务器是无法接受新的客户连接,进而只能拒绝新的连接请求。
我们知道使用accept()方法是从客户连接请求队列里边取出连接请求,来实现为该客户服务的

Socket socket = serverSocket.accept();

该方法可以将连接请求取出,从而给新的客户连接腾出空位。
对于客户端,如果发出的连接请求被加入了服务器的请求队列,也就是连接建立成功。成功返回socket对象。如果连接被拒绝,则会抛出异常:ConnectionException。
设置请求队列长度
使用ServerSocket构造方法的backlog参数。分三种情况:

  • 1、backlog值介于0到操作系统限定的上限,此时连接请求队列长度=backlog的值
  • 2、backlog<=0或者backlog>操作系统上限,backlog无效,则连接请求队列长度=操作系统的上限
  • 3、ServerSocket构造方法没有设置backlog参数,连接请求队列长度=操作系统上限

代码演示
1、客户端

package SocketProgram;

import java.io.IOException;
import java.net.Socket;

/**
 * 练习使用服务器的连接请求队列长度backlog指定
 * 创建100次客户连接,而服务器之监听3个客户。
 */

public class Chapter3_1_Client {
    public static void main(String[] ars) throws IOException, InterruptedException {
        final int length = 100;
        String host = "localhost";
        int port = 9090;
        Socket[]sockets = new Socket[length];
        for(int i=0;i<length;i++){
            sockets[i] = new Socket(host,port);//创建100次连接
            System.out.println("第"+(i+1)+"次连接成功!");
        }
        Thread.sleep(3000);//延时3s
        for(int i=0;i<length;i++){
            sockets[i].close();//关闭客户端
        }
    }
}

2、服务器端

package SocketProgram;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Chapter3_1_Server {
    private  int port = 9090;
    private ServerSocket serverSocket;
    public Chapter3_1_Server()throws IOException{
        serverSocket = new ServerSocket(port,3);//指定请求连接队列长度
        System.out.println("服务器启动...完成!");
    }
    public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                System.out.println("new connection accepted"+socket.getInetAddress()+
                        ":"+socket.getPort());
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[]ars) throws InterruptedException, IOException {
        Chapter3_1_Server server = new Chapter3_1_Server();
        Thread.sleep(60000);//睡眠60秒
        //server.server();
    }
}

先运行服务器,在运行客户端,结果如下:
在这里插入图片描述
我们使用了backlog指定了3次连接,就只能三个客户连接,其他的客户会被拒绝而抛出异常。

现在去除服务器的main方法中的server.server();注释,将Thread.sleep(60000)注释。再运行:
在这里插入图片描述
在这里插入图片描述
可见accept()方法可以取出连接请求,从而为新的客户连接腾出空位。

3.1.3 设定绑定的IP地址

当主机只有一个IP地址,那么默认情况下,服务器程序就会和这个IP地址绑定。但是当一个主机有多个IP地址时,我们需要显式指定服务器程序的IP地址。比如一个主机有两个网卡,一个网卡是连接到Internet,222.66.5.94,一个是连接到局域网192.168.3.4;如果我们的服务器仅仅被局域网的客户端访问,则应该使用第四个构造函数:

ServerSocket serverSocket = new ServerSocket(8000,10,InetAddress.getByName(192.168.3.4));//指定局域网的IP

3.1.4 默认构造函数的作用

默认构造函数没有参数,通过该构造器创建的ServerSocket对象不与任何端口绑定。所以我们接下来还要使用bind()方法将服务器与端口绑定。这个构造器的作用是:运行服务器在绑定到特定的端口之前设置一些ServerSocket选项。因为如果服务器绑定了特定端口,一些选项就无法改变。
如:

ServerSocket server = new ServerSocket();
server.setReuseAddress(true);//设置是否可以重用
server.bind(new InetSocketAddress(8000));//绑定端口。

此时设置的选项生效。但是如下代码则不生效:

ServerSocket server = new ServerSocket(8000);
server.setReuseAddress(true);//不生效

3.2 接收和关闭客户端的连接

1、接收客户端使用accept():
在这里插入图片描述
2、关闭客户端socket.close()
应该放于finally()块里边,它保证了不管与客户的通信是否是正常结束还是异常结束都关闭Socket,断开与客户的连接。

3.3 关闭ServerSocket

首先我们需要明确:
ServerSocket的close()方法使得服务器释放占用的端口,并且断开和所有客户连接。当一个服务器程序运行结束时,即使没有执行close方法,操作系统也会释放该服务器占用的端口。因此服务器不一定需要在结束之前执行其close()方法

不同:
有些情况我们需要及时释放服务器占用的端口,以便使得其他程序能占用该端口,则可以显示调用close方法。

如何判断ServerSocket已经关闭?
使用isClosed()方法判断ServerSocket是否已经关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true,否则即使ServerSocket还没有和端口绑定,isClosed()方法也会返回false
如何判断ServerSocket已经绑定端口
使用isBound()方法,可以判断。只要ServerSocket已经和一个端口绑定了,则isBound()方法会返回true,即使它已经关闭
如何判断ServerSocket已经绑定端口而且还没有关闭

boolean isOpen = serverSocket.isBound()&&!serverSocket.isClosed();

3.4 获取ServerSocket的信息

两个get方法可以分别获取其绑定的IP地址和端口:

  • 1、public InetAddress getInetAddress();返回绑定的IP地址对象
  • 2、public int getLocalPort();返回绑定的端口号,即使是匿名端口也能获知。

匿名端口的应用场景
匿名端口一般适用于服务器和客户端之间的临时通信,通信结束就断开连接。并且ServerSocket占用的临时端口也被释放。如FTP(文件传输协议)就使用了匿名端口。FTP协议用于本地文件系统和远程文件系统之间传送文件:在这里插入图片描述
FTP使用了两个TCP连接:

  • 控制连接:用于在客户和服务器之间发送控制信息,如用户名和口令、改变远程目录命令、上传和下载文件的命令。TCP服务器在21端口监听控制连接
  • 数据连接:用于传输文件

过程
1、TCP服务器在21端口上监听到了用户的需求,就另外创建一个数据连接,通过它来传输文件。
2、建立数据连接,其创建方式有两种:

  • 方式一,创建的数据连接端口是指定的,如TCP服务器在20端口监听数据连接,TCP客户主动请求建立和该端口的连接。在这里插入图片描述
  • 方式二,先由TCP客户创建一个监听匿名端口的ServerSocket,然后,把这个匿名端口通过getLocalPort方法得到,然后发送给TCP服务器,接着TCP服务器主动请求建立和客户端的连接。在这里插入图片描述
    以上的第二种方式就使用了匿名端口,而且是在客户端使用,用于和服务器建立临时数据连接。在实际应用中,服务器端也可以使用匿名端口。

3.5 ServerSocket选项

ServerSocket有如下三个选项:

  • 1、SO_TIMEOUT:表示等待客户连接的超时时间。
  • 2、SO_RESUEADDR:表示是否允许重用端口服务器绑定的IP地址。
  • 3、SO_RCVBUF:表示接收数据的缓冲区大小。

3.5.1 SO_TIMEOUT选项

在这里插入图片描述

3.5.2 SO_RESUEADDR选项

在这里插入图片描述在这里插入图片描述

3.5.3 SO_RCVBUF选项

在这里插入图片描述

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

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

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

3.6 创建多线程的服务器

3.6.1 单线程客户端服务器通信的不足

我们服务器的服务方法如下:

public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                System.out.println("new connection accepted"+socket.getInetAddress()+
                        ":"+socket.getPort());
                        //+服务器的服务
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

服务器通过accept方法监听客户的连接请求,每次只能服务一个客户,其他的客户只能排队等候。带来了一个问题:无法同时给多个客户服务,然后实际的应用需求就是要满足多客户服务
。例如HTTP服务器,就是最明显的例子,任何时刻HTTP服务器都可能接收到大量客户的请求,每个客户都希望能快速得到服务器的响应,这个时候单线程毫无疑问无法解决我们的需求。

并发性
可以使用并发性来衡量一个服务器同时响应多个客户的能力,一个具有好的并发性能服务器应该具备:

  • 同时能接收多个客户的请求连接并处理请求
  • 对每个客户的请求都能快速响应

同时处理的客户数越多,并且对每个客户响应速度越快,则并发性能越高。而提高服务器的并发性能就是我们需要解决的问题!

如何提高服务器的并发性能?
最常用的手段是多个线程同时为客户服务这里介绍3种方式重新实现上面的服务器程序:

  • 1、为每个客户分配一个工作线程
  • 2、创建一个线程池,由其中的工作线程来为客户提供服务、
  • 3、利用JDK的java类库现成的线程池,由它的工作线程来为客户服务。

3.6.2 为每个客户分配一个线程

这个比较容易:就是没连接一个客户,就创建一个线程用于运行对应的服务。
步骤
1、创建一个线程子类实现Runnable接口,然后将服务封装为线程任务,以3.1节中的服务器程序为例:

class Handler implements Runnable{
	private Socket socket;
	public Handler(Socket socket){
		this.socket = socket;
	}
	public void run(){
	//+服务器的服务
	}
	

2、原服务端的server方法修改为:

public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                new Thread(new Handler(socket)).start();//每个客户都创建一个线程来处理其请求
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

3.6.3 创建线程池

每个客户分配一个线程的不足之处
在上面的方式里边,为每个客户都分配一个新的工作线程。当工作线程和客户通信完毕这个线程就会被销毁。但是却有如下的不足

  • 1、服务器创建和销毁的工作线程开销(包括所花费的时间和系统资源)很大!比如很多客户和服务器连接,但是每个客户的通信时间很短,那么创建线程很多,并且一会儿通信就结束了。也就是创建线程开销比与客户通信的开销大
  • 2、每个线程的创建都是需要内存的,客户过多的时候,要是都分配一个线程(一个线程大约1MB),那么就会大量消耗内存造成系统空间不足。
  • 3、如果线程的数目固定,且每个线程的生命周期都很长,那么线程在CPU中切换相对固定,不同的系统切换周期不一,大约都在20ms。但是如果频繁地创建线程,导致了线程的不稳定,CPU需要频繁切换和销毁线程,那么这个时候,CPU处理线程之间的切换周期时间不再固定,进而可能导致切换线程的开销比创建和销毁线程的开销还要大。

解决方案
线程池为线程的生命周期开销问题和系统资源不足问题提供了解决方案。线程池里边预先创建了一些工作线程,它们不断地从工作队列里边取出任务,执行任务。执行完一个就取一个再执行。

线程池的优点

  • 减少了创建和销毁线程的次数,每个工作线程可以被一直重用,能执行多个任务。
  • 可以根据系统的负载能力,方便地调整线程池里边的线程数目,防止因为消耗过量的系统资源导致系统崩溃。

ThreadGroup

public class ThreadGroup
extends Object
implements Thread.UncaughtExceptionHandler
/*一个线程组表示一组线程。此外,一个线程组还可以包括其他线程组。线程组形成一个树,其中每一个线程组除了初始线程组有一个父。 
允许一个线程访问它自己的线程组的信息,但不允许访问它的线程组的父线程组或其他任何线程组的信息。

从以下版本开始: 
JDK1.0 */

该类表示线程组,提供了一系列操作和管理线程组中线程的方法。如:

  • interrupt方法相当于调用线程组中所有活着的线程的interrupt方法,对所有的工作线程进行中断处理
    该方法可能造成一下的影响:1、如果一个工作线程正在执行wait方法而阻塞,则会抛出InterruptedException;2、如果此时一个工作线程正在执行任务而且这个任务不会被阻塞,那么该线程会执行完这个任务再中断。

自定义线程池类ThreadPool

package SocketProgram;

import java.util.LinkedList;

/**
 * 为了解决每个客户都要分配一个线程来服务所带来的缺点,我们应该使用线程池对象。
 * public class ThreadGroup
 * extends Object
 * implements Thread.UncaughtExceptionHandler
 * 一个线程组表示一组线程。此外,一个线程组还可以包括其他线程组。线程组形成一个树,其中每一个线程组除了初始线程组有一个父。
 * 允许一个线程访问它自己的线程组的信息,但不允许访问它的线程组的父线程组或其他任何线程组的信息。
 *
 * 从以下版本开始:
 * JDK1.0
 * 下面使用自定义线程池类 ThreadPool
 */

public class Chapter3_6_ThreadPool extends ThreadGroup {
    private boolean isClosed = false;//判断线程池是否关闭
    private LinkedList<Runnable> workQueue;//表示工作队列
    private static int threadPoolID;//表示线程池ID
    private static int threadID;//工作线程的ID号

    /**
     *
     * @param poolSize 指定线程池中工作线程的数量
     */
    public Chapter3_6_ThreadPool(int poolSize) {
        super("ThreadPool-"+(threadPoolID++));
        setDaemon(true);//守护线程组,后台运行
        workQueue = new LinkedList<>();//创建工作队列
        for(int i=0;i<poolSize;i++){
            new WorkThread().start();//创建并启动工作线程,该WorkThread是一个内部类,线程子类
        }
    }

    /**
     * 向工作队列加入一个新任务,由工作线程去执行该任务。
     * @param task 线程任务,Runnable的子类对象
     */
    public synchronized void execute(Runnable task){
        if(isClosed){//如果线程池已经关闭,则抛出异常
            throw new IllegalStateException();
        }
        if(task!=null){
            workQueue.add(task);
            notify();//唤醒正在getTask()方法里边等到的工作线程
        }
    }

    /**
     * 从工作队列中取出一个任务,工作线程会调用该方法。
     * @return 返回执行的工作任务
     * @throws InterruptedException 中断异常
     */
    protected synchronized Runnable getTask()throws InterruptedException{
        while(workQueue.size()==0){
            if(isClosed)return null;
            wait();//如果线程队列没有任务则等待任务
        }
        return workQueue.removeFirst();//执行任务同时工作队列任务减一。
    }
    /**
     * 关闭线程池
     */
    public synchronized  void close(){
        if(!isClosed){
            isClosed = true;
            workQueue.clear();//清空工作队列
            interrupt();//线程池终止
        }
    }
    /**
     * 等到工作线程将所有的任务执行完毕
     */
    public void join(){
        synchronized (this){
            isClosed = true;
            notifyAll();//唤醒所有还在getTask方法中等待的工作线程
        }
        Thread[]threads = new Thread[activeCount()];
        //enumerate方法继承自ThreadGroup类,获取线程组中当前存活的所有工作线程
        int count = enumerate(threads);
        for(int i=0;i<count;i++){//等待所有的工作线程结束
            try{
                threads[i].join();//等到工作线程结束
            }catch (InterruptedException ignored){}
        }
    }
    /**
     * 内部类:工作线程类
     */
    private class WorkThread extends Thread{
        public WorkThread(){
            //加入到当前的ThreadPool线程组中
            super(Chapter3_6_ThreadPool.this,"WorkThread-"+(threadID++));
        }
        public void run(){
            while(!isInterrupted()){
                Runnable task = null;
                try{
                    task = getTask();
                } catch (InterruptedException ignored) {}

                //如果getTask返回null或者执行时方法被中断,则结束该线程
                if(task==null) return;
                try{
                    task.run();
                }catch (Throwable t){//Throwable是异常体系的顶层
                    t.printStackTrace();
                }
            }
        }

    }
}

解析:
1、workQueue
在ThreadPool类里边,创建了一个LinkedList类型的workQueue成员变量,里边的存放的类型都是封装为Runnable的任务。它表示工作队列
2、execute方法
客户程序只要调用execute方法就能向线程池提交任务,该方法先判断线程池是否关闭,关闭则不再接收任务,否则就将任务添加到工作队列。并唤醒正在等待任务的工作线程。
3、构造方法
该类的构造方法只有一个参数——poolSize,表示工作线程的数目。
4、WorkThread
一个内部类,继承自Thread类,表示工作线程,用于从工作队列里边执行任务,取一个执行一个,执行完再取,如此重复。
5、工作线程如何取任务?
由getTask方法实现,处理逻辑如下

  • 如果队列为空且线程池关闭,则返回null,表示没有线程可以执行
  • 如果队列为空而线程池没有关闭,则等待,直到其他线程将其唤醒或者中断
  • 如果队列中有任务,则按照先进先出的原则,取出并返回第一个任务

6、join方法和close方法
join方法用于确保在关闭线程池之前,工作线程能够把工作队列的任务都执行完。close()方法则清空队列,并直接中断所有的工作线程。

测试自定义的线程池类
我们使用了线程池类,创建了3个工作线程(poolSize=3),5个线程任务(numTask=5),下面演示3个线程如何处理5个任务的

package SocketProgram;

/**
 * 测试自定义线程池类ThreadPool
 */
public class Chapter3_6_ThreadPoolTest {
    public static void main(String[] args) {
        /*if(args.length!=2){
            System.out.println("用法:java ThreadPoolTest numTasks poolSize"+
                    "\n numTask-integer:任务的数目"+
                    "\n numThreads-integer:线程池中线程数目");
            return;
        }*/
        int numTasks = 5;//Integer.parseInt(args[0]);
        int poolSize = 3;//Integer.parseInt(args[1]);
        Chapter3_6_ThreadPool threadPool = new Chapter3_6_ThreadPool(poolSize);//创线程池
        //运行任务
        for(int i=0;i<numTasks;i++){
            threadPool.execute(createTask(i));
        }
        threadPool.join();//必须等所有的线程都完成主线程才能退出。
        //threadPool.close();//关闭线程池

    }

    private static Runnable createTask(final int taskID) {
        return () -> {//lambda表达式
            System.out.println("Task"+taskID+": start!"+"线程"+Thread.currentThread().getName());
            try{
                Thread.sleep(500);//增加执行一个任务的时间

            } catch (InterruptedException ignored){}
            System.out.println("Task"+taskID+": end..."+"线程"+Thread.currentThread().getName());
        };
    }
}

运行结果:在这里插入图片描述
可以观察到,线程1执行了任务1、4,线程2执行了任务2,线程0执行了任务0和任务3.从而解决了每个客户都分配一个线程来服务带来的问题。

3.6.4 使用自定义线程池类实现客户端和服务器的通信

package SocketProgram;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 将服务器的服务用线程池处理
 */

public class Chapter3_6_EchoServer_ThreadPool {
    private int port = 9000;
    private ServerSocket serverSocket;
    private Chapter3_6_ThreadPool threadPool;
    private final int POOL_SIZE = 4;//单CPU时线程池中工作线程的数目

    public Chapter3_6_EchoServer_ThreadPool()throws IOException{
        serverSocket = new ServerSocket(port);
        //创建线程池
        threadPool = new Chapter3_6_ThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
        //Runtime.getRuntime().availableProcesses返回当前系统的CPU数目,我们根据该数目创建工作线程数目
        System.out.println("服务器启动!");
    }
    public void service(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                //每来一个线程就执行一次execute方法判断从而添加线程任务到工作队列。
                threadPool.execute(new Chapter3_5_Handler(socket));//Handler是将服务器的任务处理封装为了对象
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[]args) throws IOException {
        new Chapter3_6_EchoServer_ThreadPool().service();
    }
}

我这里模拟3个客户连接,第一个客户连接上又退出,第二个客户正常发送信息,第三个客户是dos命令打开,因为其是GBK编码,这里出现乱码。第二个客户第三个客户都发送了bye,则服务器接受到了两个bye。通信结束。
在这里插入图片描述

3.6.5 使用JDK类库提供的线程池

java.util.concurrent包提供了现成的线程池实现,它相比上面的线程池实现更加健壮,而且功能更加强大!。体系结构:(图片来自——点击链接
在这里插入图片描述
java.util.concurrent.Executor : 负责线程的使用与调度的根接口

  • ExecutorService:Executor的子接口,线程池的主要接口
    • ThreadPoolExecutor:ExecutorService的实现类
    • ScheduledExecutorService:ExecutorService的子接口,负责线程的调度
      • ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor实现了ScheduledExecutorService

Executor接口表示线程池,它只有一个方法——execute(Runnable task),用来执行Runnable类的任务,子接口ExceutorService中声明了一些管理线程池的方法,如关闭线程池方法shutdown()等。Executors类中包含一些静态方法,负责生成各种类型的线程池ExecutorService实例。如图:在这里插入图片描述
现在我们将利用上述的线程池类来实现服务器与客户通信的任务
对比上面的代码,只需要修改以下三个地方:

1private Chapter3_6_ThreadPool threadPool;

2//创建线程池
        threadPool = new Chapter3_6_ThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);

3、 threadPool.execute(new Chapter3_5_Handler(socket));//Handler是将服务器的任务处理封装为了对象

修改为:

1private ExecutorService executorService;
2、executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
//创建了固定工作线程的数目的线程池

3、executorService.execute(new Chapter3_5_Handler(socket));//将任务提交给线程池执行

3.6.6 使用线程池的注意事项总结

虽然使用来线程池能提高服务器的并发性能,但也存在一定的风险:

在这里插入图片描述在这里插入图片描述
也就是大量线程消耗资源,容易导致系统资源不足在这里插入图片描述在这里插入图片描述在这里插入图片描述

所以我们使用线程池需注意:

在这里插入图片描述在这里插入图片描述在这里插入图片描述

3.7 关闭服务器

在服务器里边添加一个线程类,用于关闭线程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章