BIO(Blocking io):
- 同步阻塞模型,一个客户端对应一个处理线程。
- 早期的jdk中,采用BIO通信模式:
通常有一个acceptor(消费者) 去负责监听客户端的连接。它接收到客户端的连接请求之后为每个客户端创建一个线程进行链路处理,处理完成之后,线程销毁。 - 一个客户端连接,对应一个处理线程。他们之间的对应关系是 1:1。
- 由于客户端连接和服务端的处理之间的对应关系是1:1,如果遇到任务比较大,处理比较慢。
或者并发量比较大的情况下,系统会创建大量的线程。从而导致服务器线程暴增,性能急剧下降,甚至宕机。
线程示意图如下:
由于 BIO read 操作时阻塞的,而且每一个请求都需要创建一个线程处理,存在资源浪费等问题,JDK 升级 版本之后引入了 NIO (Non Blocking IO) 来解决问题。
BIO 实现一个简单的聊天信息:
聊天场景设计:
SocketUtils 工具类:
- 提供从Socket 读取内容 和 写入内容的功能。
- 代码如下
import java.io.IOException;
import java.net.Socket;
public class SocketUtils {
/**
* 读取 Socket 消息
* @param socket
* @return
*/
public static String read(final Socket socket) throws IOException {
// 接收数据
String msg = null;
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
if (len != -1) {
msg = new String(bytes, 0, len);
}
return msg;
}
/**
* 向 Socket 中写消息
* @param socket
* @param msg
*/
public static void write(Socket socket, String msg) throws IOException {
socket.getOutputStream().write((msg).getBytes());
socket.getOutputStream().flush();
}
}
BIOServer 服务器:
- pool : 线程池,多线程处理Socket
- socketMap: 保存多个客户端的Socket,并按照客户端第一次传递的值作为名字
- 转发消息按照,客户端发送的消息, 例子客户端jack 发送消息: tom: Fuck you, 表示jack 发送给 tom 内容是 Fuck you
- 代码如下:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* BIO 服务端
*/
public class BIOServer {
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(10,10, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private static Map<String, Socket> socketMap = new HashMap<>();
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true){
final Socket socket = serverSocket.accept();
final String name = SocketUtils.read(socket);
socketMap.put(name , socket);
System.out.println(name +"上线了...");
SocketUtils.write(socket, "欢迎"+ name +"来到聊天系统...");
pool.execute(new Runnable() {
@Override
public void run() {
try {
while (true){
String msg = SocketUtils.read(socket);
// 发送数据给制定的客户端
String[] msgArr = msg.split(":");
Socket to = socketMap.get(msgArr[0]);
SocketUtils.write(to, name + ":" + msgArr[1]);
System.out.println(name + "发送数据给" + msgArr[0] + ", 内容是:" + msgArr[1]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
BIOClient 客户端:
- 通过 System.in 接受用户输入的消息,发送给服务端。
- 新启动一个线程打印服务器返回消息
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* BIO 客户端
*/
public class BIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9000);
System.out.println("第一次输入名字,下次以名字和内容输入,例:[tom:Hello]");
// 启动打印服务器返回 线程
printServerMsg(socket);
while (true){
// 向服务端发送数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
SocketUtils.write(socket, str);
}
}
/**
* 打印服务器返回数据
* @param socket
*/
private static void printServerMsg(final Socket socket){
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
// 接受服务器端回传的数据
String read = SocketUtils.read(socket);
System.out.println(read);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
演示效果:
流程:
- 启动 BIOServer
- 启动两次 BIOClient 作为 A 和 B
- A 中输入 tom 回车, 表示A 客户端名字为 tom
- B 中输入 jack 回车,表示B 客户端名字为 jack
- A中输入: jack: How are you 回车, B 中会打印: tom: How are you
- B中输入:tom: Fuck you 回车, A 中会打印: jack: Fuck you tom
- 聊天结束~
服务器显示:
客户端A 显示:
客户端B显示: