NO.72 TCP/IP+BIO : 基于线程池的实现方式

Posted 代码荣耀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NO.72 TCP/IP+BIO : 基于线程池的实现方式相关的知识,希望对你有一定的参考价值。



NO.72 TCP/IP+BIO : 基于线程池的实现方式
碎片时间|体系学习
NO.72 TCP/IP+BIO : 基于线程池的实现方式

今天是
2018 年的第 82 
这是代码荣耀的第 125 篇原创


00、引言


NO.72 TCP/IP+BIO : 基于线程池的实现方式


Fig.1  基于线程池实现TCP/IP+BIO的网络通信系统


今天,我们一起来看看该种方式如何实现;并讨论“一连接一线程模型”(TCP/IP+BIO)适合的业务场景及其注意事项。


01、实现过程


上文的实现模式是当服务器端接收到客户端请求后,会创建一个新的线程“一心一意”为其服务,主线程则立即返回继续监听其他客户端的连接请求。此处,则将创建线程的过程修改为线程池的方式。即:在服务器端首先创建一个固定线程数目的线程池,当外部有请求到来的时候,则直接通过线程池的方式来运行任务,当线程池中的可用线程用完时,则新到来的客户端请求则会被忽略掉。以下是服务器端基于线程池方式实现的TCP/IP+BIO网络通信。

package weixin.test.network;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

//基于线程池的方式实现网络服务端提供时间服务
public class ThreadPoolDaytimeServer {

   // 服务的端口
   public final static int PORT = 8899;
   
   public static void main(String[] args) {
   
       // 固定线程数目的线程池,最多允许同时50个网络连接
       // 超过服务的极限就会拒绝连接
       ExecutorService pool = Executors.newFixedThreadPool(50);
       
       try (ServerSocket server = new ServerSocket(PORT)) {
           System.out.println("Started server ... ");
           
           // 永不消逝的监听          
           while (true) {
               try {
                   // 接受客户端建立连接的请求,并返回Socket对象,以便和客户端进行交互
                   Socket connection = server.accept();
                   // 开启一个线程对新接入的连接进行处理,这是“一线程一连接”实现的关键
                   // 注意:这里是通过线程池的方式进行提交
                   Callable<Void> task = new DaytimeTask(connection);
                   pool.submit(task);
               } catch (IOException ex) {
                   ex.printStackTrace();
               }
           }
           
       } catch (IOException ex) {
           System.err.println("Start Server Failed!");
       }
   }
   
   // 利用线程来处理连接
   private static class DaytimeTask implements Callable<Void> {
   
       private Socket connection;
       
       DaytimeTask(Socket connection) {
           this.connection = connection;
       }
       
       @Override
       public Void call() {
       
           try {
               Writer out = new OutputStreamWriter(
                       connection.getOutputStream());
               // 获取当前时间
               Date now = new Date();
               // 发送给客户端
               out.write(now.toString() + " ");
               out.flush();
           } catch (IOException ex) {
               System.err.println(ex);
           } finally {
               try {
                   connection.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
           
           return null;
       }
   }
}


客户端主要是向服务器发送请求,获取服务器端当前的时间。由于基于线程池的改进发生在服务器端,客户端代码与上文并无二致,但是为了保证代码的完整性,仍编写代码如下:

package weixin.test.network;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;

//客户端获取服务器端提供的时间服务
public class DayTimeClient {

   public static void main(String[] args) throws IOException {
     
       Socket client = null;
       BufferedReader reader = null;
     
       // 创建连接
       client = new Socket();
       try {
           // 连接服务器
           client.connect(new InetSocketAddress("localhost",8899));
           
           // 创建读取服务器端返回流的BufferedReader
           reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
           
           // 阻塞式获取服务器端返回来的信息
           System.out.println("Form Server : " + reader.readLine());
       
       } catch (IOException e) {
           e.printStackTrace();
       } finally{
           //非常重要,避免内存泄漏
           if(reader != null) reader.close();
           if(client != null) client.close();
       }  
   }
}


通过引入线程池,大大优化了线程的创建与使用方式,每次服务器端接收到新连接后从线程池中取得一空闲线程进行处理,处理完成后再放回池中,实现了线程的复用,解决了上文中多线程模型中出现的线程重复创建、销毁带来的开销与潜在的“拒绝服务攻击”等问题。


02、一线程一连接的特性


“一线程一连接”的工作原理,可以如图Fig.2所示,通过对该图的分析,我们可以得到该模型具有以下特点(局限):

  • 接收数据One by One。在服务器端,接收到数据后的处理可交给一个独立的线程进行处理,但是操系统通知accept的方式还是单线程方式。即:实际上服务器接收到数据报文后的处理过程可以采用多线程的方式,但是数据的接收方式还是一个接一个地进行。

  • 线程资源问题。虽然我们采用了Java线程池技术缓解了线程创建和切换的资源消耗问题,但是又会造成线程池中待处理任务的持续增加,同样消耗了大量的内存资源。此外,如果应用程序是采用长连接方式进行通信,那么线程池中的线程就被对应的任务持续占有,这样一来服务端处理的不同连接请求的数量上不去。

Fig.2  “一线程一模型”的工作原理


一句话“基于线程池实现的一线程一连接”

在大量短连接的业务场景中服务器端的性能会有所提升,因为不用每次都创建和销毁线程,而是重用线程池中的线程;但是在大量长连接的业务场景中,因为线程被连接长期占有,没有太大的优势。

该模型可以适用于小到中规模的客户端并发数场景,无法胜任大规模并发业务。



03、小结


本文对基于线程池方式实现“一连接一线程”网络通信模型(TCP/IP+BIO)进行了讨论,并给出了具体的实现方式,最后对该模型的特性及适用的业务场景进行了说明。其实,TCP/IP+BIO的网络通信系统问题的最关键之处不在于是否使用了多线程(包括线程池)技术处理并发请求,而在于accept、read的操作点都被阻塞了,这也是性能上不去的最重要的原因。后续,我们会对更加高级的网络通信模型进行介绍。敬请老铁们期待后续的姊妹篇。


欢迎老铁在留言区写下你的感悟,与大家共同交流。


本文延伸阅读

上文1:

上文2:

推荐1:

推荐2:



以上是关于NO.72 TCP/IP+BIO : 基于线程池的实现方式的主要内容,如果未能解决你的问题,请参考以下文章

JAVA BIO 编程

Java 网络编程--------------------基于TCP/IP(加入多线程)

BIONIOAIO三者的比较

纯Socket(BIO)长链接编程的常见的坑和填坑套路

07.存储引擎

mysql架构介绍