- BIO : 同步阻塞I/O(Block IO)
服务器实现模式为每一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,此处可以通过线程池机制进行优化。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 多人聊天室 - 服务端
*/
public class BioServer {
static List<Socket> clientList = new ArrayList<>();
public static void main(String[] args) throws Exception {
int port = 8080;
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket client = serverSocket.accept();
System.out.println("客户端: " + client.getPort() + " 连接成功!");
clientList.add(client);
forwardProcess(client);
}
}
/**
* 转发处理
*/
public static void forwardProcess(Socket socket) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
forwardMsg(socket);
}
}
}).start();
}
/**
* 换发消息
*/
public static void forwardMsg(Socket socket) {
try {
String msg = readMsg(socket);
System.out.println(msg);
for (Socket client : clientList) {
if (client != socket) {
writeMsg(client, msg);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 写入消息
*/
public static void writeMsg(Socket socket, String msg) throws IOException {
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(msg);
ps.flush();
}
/**
* 读取消息
*/
public static String readMsg(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg;
if ((msg = br.readLine()) != null) {
msg = socket.getPort() + " 说: " + msg;
}
return msg;
}
}
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class BilClient {
public static void main(String[] args) throws IOException {
String ip = "127.0.0.1";
int port = 8080;
Socket client = new Socket(ip, port);
readProcess(client);
OutputStream os = client.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner scanner = new Scanner(System.in);
while (true) {
String input = scanner.nextLine();
ps.println(input);
ps.flush();
}
}
/**
* 读取处理
*/
public static void readProcess(Socket socket) {
new Thread(() -> {
while (true) {
try {
System.out.println(readMsg(socket));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
/**
* 读取消息
*/
public static String readMsg(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg;
if ((msg = br.readLine()) != null) {
}
return msg;
}
}
- NIO: 同步非阻塞式IO,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
- Channel,翻译过来就是“通道”,就是数据传输的管道,类似于“流”,但是与“流”又有着区别。
- 既可以从Channel中读取数据,又可以写数据到Channel,但流的读写通常是单向的——输入输出流
- 通道可以异步读写
- 通道中的数据总是先读取到buffer(缓冲区),或者总是需要从一个buffer写入,不能直接访问数据
- 非阻塞特性:Channel在设计上采用了非阻塞的特性,它不会像传统的流一样在读写操作上阻塞线程,而是立即返回结果,告诉调用者当前的状态。这使得程序可以在等待数据准备的过程中同时进行其他操作,实现了非阻塞IO。
- 事件通知机制:Channel通常搭配选择器(Selector)来使用,选择器能够检测多个Channel的就绪状态,如是否可读、可写等,并通过事件通知(例如轮询或回调)及时地通知程序哪些Channel处于就绪状态,从而可以进行相应的读写操作。这种机制支持程序实现异步IO模型。
- 操作系统底层支持:Channel的异步读写也依赖于操作系统底层的异步IO支持。Java NIO中的Channel实际上是对操作系统底层异步IO的封装和抽象,利用了操作系统提供的异步IO机制来实现其自身的异步读写功能。
- Buffer是一个对象,里面是要写入或者读出的数据,在java.nio库中,所有的数据都是用缓冲区处理的。
- 在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是直接写到缓冲区中,任何时候访问Channel中的数据,都是通过缓冲区进行操作的。
- 缓冲区实质上是一个数组,通常是一个字节数组ByteBuffer,当然也有其他类型的:
- Selector被称为选择器,Selector会不断地轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就被判定处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取到就绪Channel的集合,进行后续的I/O操作。
- 一个多路复用器Selector可以同时轮询多个Channel,JDK使用了epoll()代替了传统的select实现,所以并没有最大连接句柄的限制,这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Set;
/**
* NIO 聊天室 服务端
*/
public class NioServer {
private Integer port;
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writerBuffer = ByteBuffer.allocate(1024);
private Charset charset = Charset.forName("UTF-8");
public NioServer(Integer port) {
this.port = port;
}
public static void main(String[] args) {
NioServer nioServer = new NioServer(8080);
nioServer.start();
}
private void start() {
try {
// 开启socket
ServerSocketChannel server = ServerSocketChannel.open();
// 设置非阻塞
server.configureBlocking(false);
// 绑定端口
server.socket().bind(new InetSocketAddress(port));
// 开启通道, 得到 Selector (选择器)
Selector selector = Selector.open();
// 注册 selector 监听事件
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("启动服务器, 监听端口:" + port + "...");
while (true) {
// 阻塞监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入 集合内部并返回事件数量
selector.select();
// 返回存有SelectionKey的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
handle(selectionKey, selector);
}
// 处理后清理 selectionKeys
selectionKeys.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 事件处理
*/
private void handle(SelectionKey key, Selector selector) throws IOException {
// SelectionKey 常用方法
//isAcceptable() 是否是连接继续事件
//isConnectable() 是否是连接就绪事件
//isReadable() 是否是读就绪事件
//isWritable() 是否是写就绪事件
// SelectionKey 常用事件
//SelectionKey.OP_ACCEPT 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
//SelectionKey.OP_CONNECT 连接就绪事件,表示客户端与服务器的连接已经建立成功
//SelectionKey.OP_READ 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
//SelectionKey.OP_WRITE 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
//处理连接
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println(client.socket().getPort() + " 已建立连接 ...");
}
// 读取消息
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
String msg = client.socket().getPort() + " 说: " + readMsg(client);
System.out.println(msg);
forwardMsg(msg, client, selector);
}
}
/**
* 读取通道消息
*/
private String readMsg(SocketChannel client) throws IOException {
readBuffer.clear();
while (client.read(readBuffer) > 0) ;
readBuffer.flip();
return String.valueOf(charset.decode(readBuffer));
}
/**
* 转发
*/
private void forwardMsg(String msg, SocketChannel client, Selector selector) throws IOException {
for (SelectionKey key : selector.keys()) {
Channel connectedClient = key.channel();
if (connectedClient instanceof ServerSocketChannel) {
continue;
}
if (key.isValid() && !client.equals(connectedClient)) {
writerBuffer.clear();
writerBuffer.put(charset.encode(msg));
writerBuffer.flip();
while (writerBuffer.hasRemaining())
((SocketChannel) connectedClient).write(writerBuffer);
}
}
}
}
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.Scanner;
import java.util.Set;
/**
* NIO 聊天室 客户端
*/
public class NioClient {
private String ip;
private Integer port;
private ByteBuffer writerBuffer = ByteBuffer.allocate(1024);
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private Charset charset = Charset.forName("UTF-8");
public NioClient(String ip, Integer port) {
this.ip = ip;
this.port = port;
}
public static void main(String[] args) {
NioClient nioClient = new NioClient("127.0.0.1", 8080);
nioClient.start();
}
public void start() {
try {
// 开启通道
SocketChannel client = SocketChannel.open();
// 设置非阻塞
client.configureBlocking(false);
Selector selector = Selector.open();
client.register(selector, SelectionKey.OP_CONNECT);
client.connect(new InetSocketAddress(ip, port));
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
handle(selectionKey, selector);
}
selectionKeys.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void handle(SelectionKey key, Selector selector) throws IOException {
// 处理连接事件
if (key.isConnectable()) {
SocketChannel client = (SocketChannel) key.channel();
if (client.isConnectionPending()) {
client.finishConnect();
// 处理用户输入
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
String msg = scanner.nextLine();
writerBuffer.clear();
writerBuffer.put(charset.encode(msg));
writerBuffer.flip();
while (writerBuffer.hasRemaining()) {
try {
client.write(writerBuffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}).start();
}
client.register(selector, SelectionKey.OP_READ);
}
// 读取消息信息
else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
String s = readMsg(client);
System.out.println(s);
}
}
private String readMsg(SocketChannel client) throws IOException {
readBuffer.clear();
while (client.read(readBuffer) > 0) ;
readBuffer.flip();
return String.valueOf(charset.decode(readBuffer));
}
}