NO.72 TCP/IP+BIO : 基于线程池的实现方式
Posted 代码荣耀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NO.72 TCP/IP+BIO : 基于线程池的实现方式相关的知识,希望对你有一定的参考价值。
这是代码荣耀的第 125 篇原创
00、引言
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 : 基于线程池的实现方式的主要内容,如果未能解决你的问题,请参考以下文章