BIOEchoServer
package bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author
* @date 2021/7/20 19:03
*/
public class BIOEchoServer {
public static void main(String[] args) throws IOException {
// 启动服务端,绑定8001端口
ServerSocket serverSocket = new ServerSocket(8001);
System.out.println("server start");
// 服务端都要去 “接待”,就像路边摊的老板
while (true) {
// 开始接受客户端连接
Socket socket = serverSocket.accept();
System.out.println("one client conn: " + socket);
// 启动线程处理连接数据
// 接着,我们启动了一个线程来处理这个连接,为什么要启动线程呢?
//
new Thread(()->{
try {
// 读取数据
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg = reader.readLine()) != null) {
System.out.println("receive msg: " + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 但是,你有没有发现什么不对劲的地方呢?
// 每来一个顾客都要给他分配一个服务员,这个老板得雇多少服务员啊?!
// 是的了,BIO 编程也有一样的问题,每来一个客户端连接都要分配一个线程,如果客户端一直增加,服务端线程会无限增加,直到服务器资源耗尽。
// 那么,怎么解决线程无限增加的烦恼呢?让我们来看看 NIO 是否能解决这个问题。
}
}
}
NIOEchoServer
package bio;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* 那么,怎么解决线程无限增加的烦恼呢?让我们来看看 NIO 是否能解决这个问题。
* <p>
* IO多路复用的概念:多个IO操作共同使用一个selector(选择器)去询问哪些IO准备好了,selector负责同志哪一些数据准备好了的IO,
* 他们再自己去请求内核数据。
*
* @author
* @date 2021/7/20 19:08
*/
public class NIOEchoServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定8080端口
serverSocketChannel.bind(new InetSocketAddress(8002));
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 创建一个Selector
Selector selector = Selector.open();
// 将Channel注册到selector上,并注册Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 接着,Selector 轮询完一次之后会拿到一系列 Key,这些 Key 叫作 SelectionKey,每个 SelectionKey 里面都绑定了一个数据准备好了的 Channel,
// 通过这个 Channel 我们就可以去取数据了。就像服务员去询问后厨哪些菜准备好了一样,后厨会告诉她哪些哪些号码的好了,然后她干嘛呢?
// 但是,如果连接非常多的情况下,有可能一次 Select 拿到的 SelectionKey 非常多,而且取数据本身还要把数据从内核空间拷贝到用户空间,这是一个阻塞操作,
// 这时候都放在主线程中来遍历所有的 SelectionKey 就会变得非常慢了,
// 当然,我们也可以把处理数据的部分扔到线程池中来处理,那么,除了这种方式有没有更高效的方式了呢?让我们来看看 AIO 是否能解决这个问题。
System.out.println("server start");
while (true) {
// 阻塞在select上(第一阶段阻塞)
selector.select();
// 如果使用的是select(timeout)或selectNow()需要判断返回值是否大于0
// 有就绪的Channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
// 强制转换为ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
// 将SocketChannel注册到Selector上,并注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 如果是读取事件
// 强制转换为SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 创建Buffer用于读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中(第二阶段阻塞)
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
// 换行符会跟着消息一起传过来
String content = new String(bytes, "UTF-8").replace("\r\n", "");
System.out.println("receive msg: " + content);
}
}
iterator.remove();
}
}
}
}
AIOEchoServer
package bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Future;
/**
* AIO,异步 IO,相对于 AIO,其它的 IO 模型本质上都是同步 IO。
* AIO 的概念:用户进程发起读取请求后立马返回,当数据完全拷贝到用户空间后通知用户直接使用数据。
* 就像扫码点餐一样,点完之后坐等饭送到你面前,全程非阻塞,没有一丁点的阻塞操作。
*
* @author
* @date 2021/7/20 19:20
*/
public class AIOEchoServer {
public static void main(String[] args) throws IOException {
// 启动服务端
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8003));
System.out.println("server start");
// 监听accept事件,完全异步,不会阻塞
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
// 再次监听accept事件
serverSocketChannel.accept(null, this);
// 消息的处理
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
Future<Integer> future = socketChannel.read(buffer);
if (future.get() > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
// 换行符会当成另一条消息传过来
if (content.equals("\r\n")) {
continue;
}
System.out.println("receive msg: " + content);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed");
}
});
// 阻塞住主线程
System.in.read();
}
}
ChatServer
package bio;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author
* @date 2021/7/20 19:41
*/
public class ChatServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 将accept事件绑定到selector上
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 阻塞在select上
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
// 加入群聊
ChatHolder.join(socketChannel);
} else if (selectionKey.isReadable()) {
// 如果是读取事件
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
// 换行符会跟着消息一起传过来
String content = new String(bytes, "UTF-8").replace("\r\n", "");
if (content.equalsIgnoreCase("quit")) {
// 退出群聊
ChatHolder.quit(socketChannel);
selectionKey.cancel();
socketChannel.close();
} else {
// 扩散
ChatHolder.propagate(socketChannel, content);
}
}
}
iterator.remove();
}
}
}
private static class ChatHolder {
private static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();
/**
* 加入群聊
* @param socketChannel
*/
public static void join(SocketChannel socketChannel) {
// 有人加入就给他分配一个id
String userId = "用户"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
send(socketChannel, "您的id为:" + userId + "\n\r");
for (SocketChannel channel : USER_MAP.keySet()) {
send(channel, userId + " 加入了群聊" + "\n\r");
}
// 将当前用户加入到map中
USER_MAP.put(socketChannel, userId);
}
/**
* 退出群聊
* @param socketChannel
*/
public static void quit(SocketChannel socketChannel) {
String userId = USER_MAP.get(socketChannel);
send(socketChannel, "您退出了群聊" + "\n\r");
USER_MAP.remove(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + " 退出了群聊" + "\n\r");
}
}
}
/**
* 扩散说话的内容
* @param socketChannel
* @param content
*/
public static void propagate(SocketChannel socketChannel, String content) {
String userId = USER_MAP.get(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + ": " + content + "\n\r");
}
}
}
/**
* 发送消息
* @param socketChannel
* @param msg
*/
private static void send(SocketChannel socketChannel, String msg) {
try {
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put(msg.getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}