目录
目标
概述
实战
单线程版本的BIO
多线程版本的BIO
单线程NIO(简易版)
单线程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
* @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
* @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();
}
}
简介
将连接放入到集合中,通过循环集合的方式接收数据。监听不阻塞,读入数据也不阻塞。当客户端连接过多时,循环的次数也会很多,尤其是客户端没关闭,则客户但会一直存在于集合中,做了很多无用的循环。
List 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 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案例上加入了多路复用器,即将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 selectionKeys = selector.selectedKeys();
Iterator 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,查看各种命令。
第三步
向服务端发送数据。