本文导读
- 《 传统 BIO 编程》同步阻塞 I/O 一个链接需要一个线程处理,而在线程方面仍有优化的余地,Java JDK1.5 开始引入线程池,也叫 Executor 框架 或 Java 并发框架,使用线程池来替代单个的线程,其优点不言而喻。
- 线程池可以参考《 线程池理论 之 线程池饱和策略 与 工作队列排队策略》、《线程池(ThreadPoolExecutor) 创建与使用》
- 后台通过一个线程池来处理多个客户端的请求接入,形成 客户端个数 M : 线程池最大线程数 N 的比例关系,其中 M 可以远远大于 N。通过线程池可以灵活的调配线程资源,设置线程的最大数,以及每个线程的工作任务队列数,防止由于海量并发接入导致线程耗尽。
- 采用线程池和任务队列可以实现如上所示的伪异步 I/O 通信框架,当新的客户端接入时,将客户端的 Socket 封装成一个 Task(实现 java.lang.Runnable 接口的任务)传递到线程池中进行处理,JDK 线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。
- 由于线程池可以设置消息队列的大小和最大线程数,因此它的资源时可控的,无论多少客户端并发访问,都不会导致资源的耗尽和宕机。
伪异步I/O
- 仍然以《传统 BIO 编程》中的示例进行改写,客户端往服务器发送数据,服务器回复数据。
·服务端·
- 如下示例中线程池中的核心线程数、最大线程数、以及工作队列大小只是为了测试结果更加清晰,所以故意设置比较小,实际中应该根据服务器性能与需求适当调大一点,如 最大线程数 30,任务队列1000 等等。
package com.lct.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间服务器
*/
public class TimeServer {
public static void main(String[] args) {
tcpAccept();
}
public static void tcpAccept() {
ServerSocket serverSocket = null;
/**
* 构造线程池
* ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
* TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)
* 参数含义:2个核心线程数,最大线程数为5,空闲线程最大等待时间为 120秒,任务队列大小2个,线程饱和策略采用抛弃旧任务策略
* 应该根据实际场景进行合适调整
*/
ExecutorService executor = new ThreadPoolExecutor(2, 5,
120L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
new ThreadPoolExecutor.DiscardOldestPolicy());
try {
/**Tcp 服务器监听端口,ip 默认为本机地址*/
serverSocket = new ServerSocket(8080);
/**循环监听客户端的连接请求
* accept 方法会一直阻塞,直到 客户端连接成功,主线程才继续往后执行*/
Socket socket = null;
while (true) {
System.out.println("我是线程 " + Thread.currentThread().getName() + ",等待客户端连接..........");
socket = serverSocket.accept();
System.out.println("我是线程 " + Thread.currentThread().getName() + ",客户端连接成功..........");
/**线程池调度线程执行任务
* 为每一个客户端连接都新开线程进行处理
*/
executor.execute(new TimeServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
/**发生意外时,关闭服务端*/
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 对每个客户端连接新开线程单独处理:
package com.lct.bio;
import java.io.*;
import java.net.Socket;
import java.util.Date;
/**
* Created by Administrator on 2018/10/14 0014.
* 为每个 TCP 客户端新开线程进行处理
*/
public class TimeServerHandler implements Runnable {
private Socket socket = null;
/**
* 将每个 TCP 连接的 Socket 通过构造器传入
*
* @param socket
*/
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
DataInputStream dataInputStream = null;
DataOutputStream dataOutputStream = null;
try {
/**读客户端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println("我是线程 " + Thread.currentThread().getName() + " ,收到客户端消息:" + message);
/**往客户端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(new Date().toString());
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
/**操作完成,关闭流*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**操作完成,关闭连接,线程自动销毁*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
·客户端·
package com.lct.bio;
import java.io.*;
import java.net.Socket;
/**
* Created by Administrator on 2018/10/14 0014.
* 时间 客户端
*/
public class TtimeClient {
public static void main(String[] args) {
/**
* 5个线程模拟5个客户端
*/
for (int i = 0; i < 5; i++) {
new Thread() {
@Override
public void run() {
tcpSendMessage();
}
}.start();
}
}
/**
* Tcp 客户端连接服务器并发送消息
*/
public static void tcpSendMessage() {
Socket socket = null;
DataOutputStream dataOutputStream = null;
DataInputStream dataInputStream = null;
try {
/**
* Socket(String host, int port):
* host)被连接的服务器 IP 地址
* port)被连接的服务器监听的端口
* Socket(InetAddress address, int port)
* address)用于设置 ip 地址的对象
* 此时如果 TCP 服务器未开放,或者其它原因导致连接失败,则抛出异常:
* java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("127.0.0.1", 8080);
System.out.println("连接成功.........." + Thread.currentThread().getName());
/**往服务端写数据*/
OutputStream outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF("我是长城" + Thread.currentThread().getName());
dataOutputStream.flush();
/**读服务端数据*/
InputStream inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println("收到服务器消息:" + message);
} catch (IOException e) {
e.printStackTrace();
} finally {
/**关闭流,释放资源*/
if (dataOutputStream != null) {
try {
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dataInputStream != null) {
try {
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 操作完毕关闭 socket*/
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务端控制台输出:
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 main,客户端连接成功..........
我是线程 main,等待客户端连接..........
我是线程 pool-1-thread-3 ,收到客户端消息:我是长城Thread-1
我是线程 pool-1-thread-1 ,收到客户端消息:我是长城Thread-3
我是线程 pool-1-thread-2 ,收到客户端消息:我是长城Thread-0
我是线程 pool-1-thread-2 ,收到客户端消息:我是长城Thread-4
我是线程 pool-1-thread-3 ,收到客户端消息:我是长城Thread-2客户端控制台输出:
连接成功..........Thread-0
连接成功..........Thread-4
连接成功..........Thread-3
连接成功..........Thread-2
连接成功..........Thread-1
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
收到服务器消息:Mon Oct 15 17:30:11 CST 2018
总 结
- 熟悉基础 TCP 编程(TCP 理论详解)的应该知道,当对 Socket 的输入流进行读取操作时,它会一直阻塞下去,直到发生以下事件:
1)有数据可读
2)可用数据已经读取完毕
3)发生空指针或者 I/O 异常
4)socket.setSoTimeout 数据读取超时
扫描二维码关注公众号,回复: 3662981 查看本文章
- 这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要 60 秒才能将数据发送完成,则读取一方的 I/O 线程也将会被阻塞 60 秒,在此期间,其它接入消息只能在任务队列中排队。
- 同时 Socket 的输出流 OutputStream 写操作时,也是会阻塞的,直到所有要发送的字节全部写入完毕,或者发生异常。、
- 伪异步 I/O 实际上仅仅是对 BIO 线程模型的简单优化,无法从根本上解决同步 I/O 导致的通信线程阻塞问题。
·····下一篇《 NIO 理论 与 编程》