前提
- 熟悉Java IO
- 了解socket
了解基础BIO
BIO(Block IO)也就是阻塞IO
白话:当一个客户端连接到服务端时,如果服务端仅仅是单线程处理该客户端的连接时,当服务端未处理完当前客户端的连接,另外一个新的客户端连接到当前服务端,该连接就会被阻塞(无法连接),仅仅当服务端处理完上一个客户端的连接后新的客户端才能连接成功。
BIO代码示例
arduino
/**
* BIO 示例
*/
public class BIOSocketServer {
public static void main(String[] args) {
//while---true 非处理单连接后结束线程
//创建一个服务端socket,监听系统9090端口
try {
ServerSocket serverSocket = new ServerSocket(9090);
while (true) {
//等待客户端连接
Socket socketServer = serverSocket.accept(); //阻塞
System.out.println("客户端:" + socketServer.getInetAddress().getHostName() + "连接成功");
//读取客户端发送的数据
InputStream inputStream = socketServer.getInputStream();
OutputStream outputStream = socketServer.getOutputStream();
while (true) {
//限定接收1024个字节
byte[] buffer = new byte[1024];
int read = inputStream.read(buffer); //等待阻塞
String s = "";
if (read != -1) {
s = new String(buffer, 0, read);
System.out.println("客户端:" + s);
}
//响应客户端,接收成功
outputStream.write("服务端:我已经收到你的数据----->".concat(s).getBytes(StandardCharsets.UTF_8));
//结束继续接收当前客户端数据
if (s.equals("exit")) break;
}
//释放资源
outputStream.close();
inputStream.close();
socketServer.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
这是每个客户端用来接收读取服务端发送数据线程
csharp
/**
* 读取客户端发送过来的数据
*/
public class ReadThread extends Thread{
private Socket client;
public ReadThread(Socket client){
this.client = client;
}
public void run() {
try {
while (true) {
//读取客户端发送过来的数据
byte[] buf = new byte[1024];
int len = client.getInputStream().read(buf);
if (len == -1) {
break;
}
System.out.println(new String(buf, 0, len, "utf-8"));
}
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}
测试
- 启动服务端程序,debug可以发现代码在这里阻塞了,一直等待有客户端连接进来
ini
Socket socketServer = serverSocket.accept(); //阻塞
- 模拟启动第一个客户端连接服务端
java
/**
* 客户端1
*/
public class ClientSocket1 {
public static void main(String[] args) {
try {
//本地主机+端口号
String host = "127.0.0.1";
int port = 9090;
//连接服务端
Socket socket = new Socket(host, port);
//开启一个线程,读取服务端返回的数据
new ReadThread(socket).start();
//发送数据
OutputStream outputStream = socket.getOutputStream();
//读取控制台数据,发送给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
outputStream.write(scanner.nextLine().getBytes(StandardCharsets.UTF_8));
//刷新缓冲区
outputStream.flush();
}
socket.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
debug发现上面执行到Socket socket = new Socket(host, port);
时,服务端的Socket socketServer = serverSocket.accept(); //阻塞
跑通了
- 当客户端不发送数据给服务端端时
debug时候可以发现服务端代码一直阻塞在int read = inputStream.read(buffer); //等待阻塞
中,服务端一直等待读取客户都发送数据
- 模拟启动第二个客户端程序,(这时服务端还在处理客户端1)
java
/**
* 客户端2
*/
public class ClientSocket2 {
public static void main(String[] args) {
try {
//本地主机+端口号
String host = "127.0.0.1";
int port = 9090;
//连接服务端
Socket socket = new Socket(host, port);
//开启一个线程,读取服务端返回的数据
new ReadThread(socket).start();
//发送数据
OutputStream outputStream = socket.getOutputStream();
//读取控制台数据,发送给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
outputStream.write(scanner.nextLine().getBytes(StandardCharsets.UTF_8));
//刷新缓冲区
outputStream.flush();
}
socket.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
启动这第二个客户端程序连接9090端口服务端,debug发现客户端2没有在Socket socket = new Socket(host, port);
这个位置阻塞,可是这个时候服务端处理的连接是客户端1
- 模拟两个客户端都发送数据给服务端
可以发现客户端2,发送数据给服务端,服务端根本没有收到,说明客户端2没有连接服务端成功,因为服务端正在处理客户端1的连接。
- 客户端1断开服务端连接
可以发现,当客户端1断开与服务端连接时,客户端2之前连接服务端阻塞被放开了,服务端就处理客户端2的连接,并且能接收到客户端2之前阻塞时发送给服务端的数据。
基础BIO存在问题
服务器在单线程的情况下,只能处理单个客户端器的连接,当新的客户端与服务端连接时就会被阻塞。
解决基础BIO存在的问题
上面服务端处理客户端连接都是单线程的,所以一次只能处理一个客户端连接,那每次连接过来一个客户端,服务端就开一个线程来处理与客户端的连接(或者使用线程池)。
java
public class ThreadPoolManager
{
private static ThreadPoolManager sThreadPoolManager = new ThreadPoolManager();
private static final int SIZE_CORE_POOL = 50;
private static final int SIZE_MAX_POOL = 50;
private static final int TIME_KEEP_ALIVE = 30;
public static ThreadPoolManager singleInstance() {
return sThreadPoolManager;
}
private final ThreadPoolExecutor mThreadPool = new ThreadPoolExecutor(50, 50, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
public void perpare() {
if (this.mThreadPool.isShutdown() && !this.mThreadPool.prestartCoreThread())
{
int i = this.mThreadPool.prestartAllCoreThreads();
}
}
//添加任务
public void addExecuteTask(Runnable task) {
if (task != null) {
this.mThreadPool.execute(task);
}
}
protected boolean isTaskEnd() {
if (this.mThreadPool.getActiveCount() == 0) {
return true;
}
return false;
}
public void shutdown() {
this.mThreadPool.shutdown();
}
}
对应Runnable
arduino
/**
* 处理客户端连接的任务
*/
public class HandleClientRunnable implements Runnable{
private Socket socketServer;
public HandleClientRunnable(Socket socketServer){
this.socketServer = socketServer;
}
@Override
public void run() {
try {
System.out.println("客户端:" + socketServer.getInetAddress().getHostName() + "连接成功");
//读取客户端发送的数据
InputStream inputStream = socketServer.getInputStream();
OutputStream outputStream = socketServer.getOutputStream();
while (true) {
//限定接收1024个字节
byte[] buffer = new byte[1024];
int read = inputStream.read(buffer); //等待阻塞
String s = "";
if (read != -1) {
s = new String(buffer, 0, read);
System.out.println("客户端:" + s);
}
//响应客户端,接收成功
outputStream.write("服务端:我已经收到你的数据----->".concat(s).getBytes(StandardCharsets.UTF_8));
//结束继续接收当前客户端数据
if (s.equals("exit")) break;
}
//释放资源
outputStream.close();
inputStream.close();
socketServer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
加入线程池后的BIO服务端程序
java
/**
* BIO 开线程处理
*/
public class BIOSocketServer {
public static void main(String[] args) {
//while---true 非处理单连接后结束线程
//创建一个服务端socket,监听系统9090端口
try {
ServerSocket serverSocket = new ServerSocket(9090);
ThreadPoolManager threadPoolManager = ThreadPoolManager.singleInstance();
while (true) {
//等待客户端连接,一个客户端连接过来后直接放开,第二个循环阻塞等待第二个连接过来,依次类推
Socket socketServer = serverSocket.accept(); //阻塞,
//使用线程池处理客户端连接
threadPoolManager.addExecuteTask(new HandleClientRunnable(socketServer));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
两个客户端程序不变,同时开启,并且两个都发送数据给服务端
可以发现,服务端可以同时处理多个客户端的连接,解决了基础BIO问题
引发新的问题
- C10K \ C10M问题
当开线程去处理新的连接,这个做法只能用在客户端极少的情况,因为开线程是极其消耗系统资源的,当客户端并发存在上万千万的时候,那么系统内存资源全部用来开线程了,还没等全部处理完,就玩球球了,开线程池处理虽然可以防止系统资源开销过大,但是处理并发量是很有限的。这就是BIO的弊端,解决这个弊端------>NIO(非阻塞IO)