BIO: Blocking IO(阻塞IO),同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
缺点:阻塞式I/O模型,弹性伸缩能力差,多线程耗资源
多人聊天室例子:
大致步骤:
- 创建ServerSocket,绑定端口8080
- 调accept()方法监听连接,并返回套接字Socket
- 获取输入流,并通过InputStreamReader转为字符,缓存在BufferdReader中
- 获取输出流,通过OutputStreamWriter将BufferedWriter中的字符转换为字节,并通过PrintWriter格式化输出,同时自动flush
- 根据输入流读取的字符,如果是END则结束会话
- 关闭套接字和ServerSocket
/**
* BIO 服务端
*/
public class BIOServer {
public static final int PORT = 8888;
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(PORT);
Map<String, Socket> map = new HashMap<>();
System.out.println("开始: " + server);
while (true) {
Socket socket = server.accept();//阻塞在这里等待客户端连接
System.out.println("Connection socket: " + socket);
//一个客户端开一个线程
new Thread(new Runnable() {
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
String username = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//Output is automatically flushed by PrintWrite
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
out.println("欢迎来到聊天室。。。");
} catch (IOException e) {
e.printStackTrace();
}
int i = 0;
//接收客户端发的消息
while (true) {
String str = null;
try {
str = in.readLine();//阻塞在这里等待接收消息
if (str != null) {
if (i == 0) {
map.put(str, socket);
username = str;
i++;
}
if ("END".equals(str))
break;
System.out.println(username + ": " + str);
//转发消息给其他客户端
Set<String> names = map.keySet();
if (names.size() > 1) {
for (String s:
names) {
if(map.get(s) != socket) {
PrintWriter out1 = new PrintWriter(new BufferedWriter(new OutputStreamWriter( map.get(s).getOutputStream())), true);
out1.println(username + ": " + str);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
/**
* BIO客户端
*/
public class BIOClient1 {
public static void main(String[] args) throws Exception {
//服务器端信息,address和8080;后台连接服务器,还会绑定客户端
InetAddress address = InetAddress.getByName(null);
System.out.println("address = " + address);
Socket socket = new Socket(address, 8888);
System.out.println("Socket = " + socket);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
//开一个线程接收服务端发的消息
new Thread(new Runnable() {
@Override
public void run() {
String str = null;
try {
while (true) {
str = in.readLine();
if (str != null) {
System.out.println(str);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
//向服务端发送消息
out.println("李四");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String str = scanner.nextLine();
if(str != null && str.length() > 0) {
out.println(str);
}
}
}
}
NIO:non-blocking IO(同步非阻塞IO)(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO)
通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
优点:非阻塞式I/O模型,弹性伸缩能力强,单线程节省资源
NIO网络编程缺陷:
- 麻烦 :NIO类库和API繁琐
- 心累:可靠性能力补齐,工作量和难度都非常大
- 有坑:Selector空轮询,导致CPU100%
核心类:selector, channel, buffer
通道Channel:
NIO的通道类似于流,但有些区别如下:
-
通道可以同时进行读写,而流只能读或者只能写
-
通道可以实现异步读写数据
-
通道可以从缓冲区读数据,也可以写数据到缓冲区
Channel实现:
- 文件类:FileChannel
- UDP类:DatagramChannel
- TCP类:ServerSocketChannel/SocketChannel
Bufffer:
本质:一块内存区域,底层是有一个数组
作用:读写Channel中数据
属性:
- Capacity:容量,可存储的字节数
- Position:位置,写模式时表示开始写的位置,读模式时表示在哪开始读的位置
- Limit:上限,写模式下等于Capacity表示能存储的数据数量,读模式等于写模式时的Position表示能读取的数据数量
- Mark:标记,记录position的位置
//初始化长度为10的byte类型buffer
ByteBuffer.allocate(10);
//向byteBuffer中写入三个字节
byteBuffer.put("abc",getBytes(Charset.forName("UTF-8")));
//将byteBuffer从写模式切换为读模式
byteBuffer.flip();
//从byteBuffer读取一个字节
byteBuffer.get();
//调用mark方法记录下当前position的位置
byteBuffer.mark();
/**
* 先调用get方法获取下一个字节
* 再调用reset方法将position重置为mark值
*/
byteBuffer.get()
byteBuffer.reset()
//调用clear方法将所有属性重置
byteBuffer.clear()
Selector:
选择器(多路复用器),可注册多个Channel,可以检测多个NIO channel,看看读或者写事件是否就绪。实现一个线程处理多个连接
- 作用:I/O就绪选择
- 地位:NIO网络编程基础
NIO实现步骤:
- 创建Selector
- 创建ServerSocketChannel,并绑定监听端口
- 将Channel 设置为非阻塞模式
- 将Channel注册到Selector上,监听连接事件
- 循环调用Selector的select方法,检测就绪情况
- 调用selectionKeys方法获取就绪Channel集合
- 判断就绪事件类型,调用业务处理方法
- 根据业务需要决定是否再次注册监听事件,重复执行第四步
多人聊天室例子:
package com.example.springdemo.niotest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* NIO服务端
*/
public class NIOServer {
/**
* 启动
*/
public void start() throws IOException {
//1.创建Selector
Selector selector = Selector.open();
//2.通过ServerSocketChannel创建channel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//3.为channel通道绑定监听端口
serverSocketChannel.bind(new InetSocketAddress(8888));
//4.设置channel为非阻塞模式
serverSocketChannel.configureBlocking(false);
//5.将channel注册到selector上,监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
//循环等待新接入的连接
while (true) {
/**
* TODO 获取可用channel数量
*/
int readyChannels = selector.select();
/**
* TODO ??
*/
if (readyChannels == 0) continue;
/**
* 获取所有已就绪channel的集合
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
/**
* selectionKey实例
*/
SelectionKey selectionKey = iterator.next();
/**
* 移除Set中的当前selectionKey
*/
iterator.remove();
/**
* 7.根据就绪状态,调用对应方法处理业务逻辑
*/
//如果是接入事件
if (selectionKey.isAcceptable()) {
acceptHandler(serverSocketChannel, selector);
}
//如果是可读事件
if (selectionKey.isReadable()) {
readHandler(selectionKey, selector);
}
}
}
}
/**
* 接入事件处理器
*/
private void acceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
/**
* 如果是接入事件,创建socketChannel,为与客户端的连接
*/
SocketChannel socketChannel = serverSocketChannel.accept();
/**
* 将socketChannel设置为非阻塞工作模式
*/
socketChannel.configureBlocking(false);
/**
* 将channel注册到selector上,监听可读事件
*/
socketChannel.register(selector, SelectionKey.OP_READ);
/**
* 回复客户端提示消息
*/
socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室..."));
}
/**
* 可读事件处理器
*/
private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
/**
* 要从selectionKey中获取到已就绪的channel
*/
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
/**
* 创建buffer
*/
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
/**
* 循环读取客户端请求信息
*/
String request = "";
while (socketChannel.read(byteBuffer) > 0) {
/**
* 切换buffer为读模式
*/
byteBuffer.flip();
/**
* 读取buffer中的内容
*/
request += Charset.forName("UTF-8").decode(byteBuffer);
}
/**
* 将channel再次注册到selector上,监听它的可读事件
*/
socketChannel.register(selector, SelectionKey.OP_READ);
/**
* 将客户端发送的请求信息,广播给其他客户端
*/
if (request.length() > 0) {
/**
* 获取到所有已接入的客户端channel
*/
Set<SelectionKey> keys = selector.keys();
/**
* 循环向所有channel广播消息
*/
String finalRequest = request;
keys.forEach(key -> {
Channel targetChannel = key.channel();
//剔除发消息的客户端
if(targetChannel instanceof SocketChannel && targetChannel != socketChannel ) {
try {
((SocketChannel) targetChannel).write(Charset.forName("UTF-8").encode(finalRequest));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) throws IOException {
new NIOServer().start();
}
}
package com.example.springdemo.niotest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* NIO客户端
*/
public class NIOClient {
public void start() throws IOException {
/**
* 连接服务器端
*/
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("127.0.0.1", 8888));
/**
* 接收服务器端响应信息
*/
//新开线程,专门负责来接收服务器端的响应数据
//selector, socketChannel,注册
Selector selector = Selector.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
/**
* TODO 获取可用channel数量
*/
int readyChannels = selector.select();
if (readyChannels == 0) continue;
/**
* 获取可用channel集合
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
/**
* selectionKey实例
*/
SelectionKey selectionKey = iterator.next();
/**
* 移除Set中的当前selectionKey
*/
iterator.remove();
/**
* 7.根据就绪状态,调用对应方法处理业务逻辑
*/
//如果是可读事件
if (selectionKey.isReadable()) {
readHandler(selectionKey, selector);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
/**
* 向服务器端发送数据
*/
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
String request = scanner.nextLine();
if(request != null && request.length() >0) {
socketChannel.write(Charset.forName("UTF-8").encode(request));
}
}
}
/**
* 可读事件处理器
*/
private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
/**
* 要从selectionKey中获取到已就绪的channel
*/
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
/**
* 创建buffer
*/
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
/**
* 循环读取服务器端响应信息
*/
String response = "";
while (socketChannel.read(byteBuffer) > 0) {
/**
* 切换buffer为读模式
*/
byteBuffer.flip();
/**
* 读取buffer中的内容
*/
response += Charset.forName("UTF-8").decode(byteBuffer);
}
/**
* 将channel再次注册到selector上,监听它的可读事件
*/
socketChannel.register(selector, SelectionKey.OP_READ);
/**
* 将服务器端响应信息打印到本地
*/
if (response.length() > 0) {
System.out.println(":: " + response);
}
}
public static void main(String[] args) throws IOException {
new NIOClient().start();
}
}