目录
目标
概述
实战
单线程版本的BIO
多线程版本的BIO
单线程NIO(简易版)
单线程NIO(多路复用版)
客户端发送数据的方法
目标
- 了解BIO和NIO的区别和应用场景。
- 分析BIO和NIO的线程模型,利用Socket实现BIO和NIO的核心功能。
概述
BIO(Blocking I/O)
同步阻塞式IO。JDK1.4以前的IO模型。当客户端连接到服务端以后,服务端可以用单线程处理客户端连接,也可以用线程池处理客户端连接,但是它们都是一个线程以同步阻塞的方式处理一个客户端连接。
缺点:
- 以同步阻塞的方式处理连接意味着一个线程需要处理一个客户端的连接事件和读写事件,且连接事件和读写事件都是阻塞的,即客户端与服务端建立连接后其他客户端的连接无法被处理(同一个线程下)。即使客户端与服务端建立连接后,客户端迟迟没有读写操作,这个线程也会继续阻塞等待。
优点:
- 编程简单。
- 适用于客户端较少的传统项目。
NIO(New I/O或Non Blocking I/O)
同步非阻塞式IO。JDK1.4(含)以后的IO模型。
普通的NIO只是将连接事件和读写事件设置为非阻塞,即将连接好的客户端放到一个集合里面,通过循环遍历所有客户端连接的方式处理客户端的请求。
NIO配合多路复用器(Selector)以后,客户端的连接都会注册到Selector上,客户端的连接操作和读写操作会通过反应堆模式(Reactor)触发连接事件和读写事件。这些有连接操作和读写操作的客户端都会存在于一个集合中,通过循环遍历这个集合来针对性地处理客户端请求。
缺点:
- 编程复杂。
优点:
- 通过非阻塞的模式处理客户端的连接操作、读写操作和针对性地循环处理客户端请求,极大地提高了服务端的并发量。
- 减少了不必要的线程建立。
实战
单线程版本的BIO
简介
一次只能处理一个连接,每个客户端都要发送消息才能轮到下一个客户端操作。
/**
* 单线程版本的BIO
* @throws IOException
*/
public void oneThreadBio() throws IOException {
ServerSocket serverSocket = new ServerSocket(8099);
for (; ; ) {
log.info("这里会阻塞,等待客户端连接……");
Socket socketClient = serverSocket.accept();
log.info("客户端连接成功:{}", socketClient.getRemoteSocketAddress());
byte[] bytes = new byte[1024];
//这里会阻塞,等待客户端发送消息。
//将客户端发过来的数据放入bytes
int read = socketClient.getInputStream().read(bytes);
if (read != -1) {
String msg = new String(bytes, 0, read);
log.info("收到消息:{}", msg);
}
}
}
多线程版本的BIO
简介
一次处理多个连接,但是无限制地创建线程、客户端长时间不发送数据导致线程无法被销毁,会导致服务器崩溃。
/**
* 多线程版本的BIO
* @throws IOException
*/
public void multiThreadBio() throws IOException {
ServerSocket serverSocket = new ServerSocket(8099);
for (; ; ) {
log.info("这里会阻塞,等待客户端连接……");
Socket socketClient = serverSocket.accept();
log.info("客户端连接成功:{}", socketClient.getRemoteSocketAddress());
new Thread(() -> {
byte[] bytes = new byte[1024];
//这里会阻塞,等待客户端发送消息。
//将客户端发过来的数据放入bytes
int read = 0;
try {
read = socketClient.getInputStream().read(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (read != -1) {
String msg = new String(bytes, 0, read);
log.info("收到消息:{}", msg);
}
}).start();
}
}
单线程NIO(简易版)
简介
将连接放入到集合中,通过循环集合的方式接收数据。监听不阻塞,读入数据也不阻塞。当客户端连接过多时,循环的次数也会很多,尤其是客户端没关闭,则客户但会一直存在于集合中,做了很多无用的循环。
List<SocketChannel> socketChannelList = new ArrayList();
/**
* 单线程NIO(简易版)
* @throws IOException
*/
public void simpleNio() throws IOException {
//创建socket服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8099));
//false=非阻塞;true=阻塞。
serverSocketChannel.configureBlocking(false);
for (; ; ) {
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
log.info("连接成功。{}", socketChannel.getRemoteAddress());
//false=非阻塞;true=阻塞。
socketChannel.configureBlocking(false);
socketChannelList.add(socketChannel);
}
if (CollectionUtils.isEmpty(socketChannelList)) {
continue;
}
Iterator<SocketChannel> iterator = socketChannelList.iterator();
while (iterator.hasNext()) {
ByteBuffer bb = ByteBuffer.allocate(16);
SocketChannel channel = iterator.next();
//把数据读入到ByteBuffer中
int read = channel.read(bb);
if (read > 0) {
//切换到读模式
bb.flip();
log.info("收到消息:{}", StandardCharsets.UTF_8.decode(bb));
bb.clear();
} else if (read == -1) {
iterator.remove();
log.info("客户端退出。{}", channel.getRemoteAddress());
}
}
}
}
单线程NIO(多路复用版)
简介
在上一个NIO案例上加入了多路复用器,即将Channel注册到Selector上。客户端的连接操作和读写操作,会通过反应堆模式(Reactor)触发连接事件和读写事件(如果Channel注册了连接事件、读事件、写事件)。服务端会针对性地遍历客户端操作,减少了不必要的空循环,使得一个线程也能处理多个客户端连接。
/**
* 单线程NIO(多路复用版)
* @throws IOException
*/
public void selectorNio() throws IOException {
//创建socket服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8099));
//false=非阻塞;true=阻塞。
serverSocketChannel.configureBlocking(false);
//这是JDK提供的选择器(用于选择事件)
Selector selector = Selector.open();
//将服务端的serverSocketChannel注册到Selector上,关注的事件:连接事件。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
for (; ; ) {
//这里会阻塞,一旦客户端和服务器有了数据传递,则向下运行。
selector.select();
//
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyiterator = selectionKeys.iterator();
while (selectionKeyiterator.hasNext()) {
SelectionKey selectionKey = selectionKeyiterator.next();
if (selectionKey.isAcceptable()) {//发生了连接事件
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
//false=非阻塞;true=阻塞。
socketChannel.configureBlocking(false);
//读事件(如果服务端还想要向客户端发消息,可以再多注册一个写事件。)
socketChannel.register(selector, SelectionKey.OP_READ);
log.info("连接建立成功:{}", socketChannel.getRemoteAddress());
} else if (selectionKey.isReadable()) {//发生了读事件
ByteBuffer bb = ByteBuffer.allocate(16);
SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
int read = socketChannel.read(bb);
if (read > 0) {
//切换到读模式
bb.flip();
log.info("收到消息:{}", StandardCharsets.UTF_8.decode(bb));
bb.clear();
}else if(read ==-1){
socketChannel.close();
log.info("客户端连接断开。");
}
}
selectionKeyiterator.remove();
}
}
}
客户端发送数据的方法
简介
大家可以写客户端代码与服务端交互。这里我通过cmd.exe窗口模拟客户端向服务端发送数据。一个黑窗口就是一个客户端,启动2个黑窗口向服务端发送数据可以明显看出BIO和NIO的区别。
第一步
打开cmd.exe窗口,根据服务端绑定的端口,使用telnet向服务端发起连接。
第二步
按Ctrl+]组合键,输入help,查看各种命令。
第三步
向服务端发送数据。