什么是BIO
BIO(Blocked Input Output)是一种同步阻塞IO。早期Java网络通信通过Socket(套接字)进行通信,这是一种阻塞式的通信。
通过BIO实现网络通信,需要一对套接字:
- 运行于服务端的ServerSocket
- 运行于客户端的Socket
Socket通信方式如下图所示:
Java使用Socket进行网络通信过程:
服务端:
1.创建ServerSocket对象,绑定地址(ip)和端口(port):serverSocket.bind(new InetSocketAddress(host,port))
2.通过accept
方法监听客户端请求:accept
方法调用后会阻塞住,直到客户端请求出现
3.连接建立,通过输入输出流进行通信:read
方法调用会阻塞,直到数据可以读取
4.连接关闭,资源释放
客户端:
1.创建Socket
对象,设置连接服务器的地址(ip)及端口(port):socket.connect(new InetSocketAddress(host,port))
2.连接建立,通过输入输出流进行通信:read
方法调用会阻塞,直到数据可以读取
3.连接关闭,释放资源
Socket网络通信实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Server {
public static void main(String[] args) {
try{ ServerSocket serverSocket = new ServerSocket(8888);
while(true){ Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream(); DataInputStream dataInputStream = new DataInputStream(inputStream); String str = dataInputStream.readUTF(); System.out.println("server receive from client:" + str);
OutputStream outputStream = socket.getOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(outputStream); dataOutputStream.writeUTF("hello client, I am Server!");
socket.close(); } } catch (IOException e){ e.printStackTrace(); }
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class Client {
public static void main(String[] args) {
Socket socket; try{ socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(outputStream); dataOutputStream.writeUTF("hello server, I am client!");
InputStream inputStream = socket.getInputStream(); DataInputStream dataInputStream = new DataInputStream(inputStream); String str = dataInputStream.readUTF(); System.out.println("client receive from server:" + str);
socket.close(); } catch (IOException e){ e.printStackTrace(); }
}
}
|
Server端控制台会输出:
1
| server receive from client:hello server, I am client!
|
Client端控制台会输出:
1
| client receive from server:hello client, I am Server!
|
为什么不推荐使用BIO
资源耗费严重:一个线程只能处理一个客户端连接,如果要管理多个客户端的话,必须为每个客户端连接创建一个线程。
1 2 3 4 5 6
| while(true){ Socket socket = serverSocket.accept(); new Thread(() - > { }).start(); }
|
线程池可以一定程度上缓解此问题,但无法根本改变:
1 2 3 4 5 6
| while(true){ Socket socket = serverSocket.accept(); threadPool.execute(() -> { }); }
|
无论如何优化,它底层还是同步阻塞的IO,无法从根本上解决问题。
同步和异步(消息通信机制)
- 同步:发出一个调用时,在没有得到结果之前,该调用就不返回
- 异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果
阻塞和非阻塞(等待调用结果时的状态)
- 阻塞:调用结果返回之前,线程一直挂起
- 非阻塞:调用没有返回结果之前,线程不会阻塞
再看NIO
NIO(Non-blocking IO)是一种同步非阻塞IO,在java1.4时引入,对应java.nio
包。
NIO提供了与传统BIO中的Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同套接字通道的实现,两种通道都支持阻塞和非阻塞两种模式:
- 阻塞模式:基本不会使用。与传统网络编程一样,简单但性能和可靠性不好。
- 非阻塞模式:对高负荷、高并发应用非常好,但是编程麻烦。这也是Netty出现的重要原因。
NIO核心组件解读
NIO包含一下几个核心组件:
- Channel
- Buffer
- Selector
- Selector Key
它们之间的关系如下:
1.NIO通过Channel(通道)和Buffer(缓存区)来传输数据,数据总是从缓冲区写入通道,或从通道读取到缓冲区。在NIO中,所以数据都是通过Buffer处理的,Channel对应于JDK底层的Socket。
2.NIO通过Selector(选择器)来监视多个通道对象,如数据到达、连接打开等,单线程可以监视多个通道
3.将Channel注册到Selector时,会返回一个Selector Key,可以根据它获取那些IO事件已经就绪,也可以通过它获取对应的Channel进行操作。
Selector是NIO实现的关键,它使用了事件通知相关的API来选择已经就绪的通道。
简单来说,流程如下:
- 将Channel注册到Selector中
- 调用Selector的
select
方法,这个方法会阻塞,直到有就绪的Channel出现
- 注册到Selector的就绪态Channel会被轮询出来:新的连接、读就绪、写就绪
- 通过Selector Key获取就绪Channel的集和,进行后续的IO操作
使用NIO进行通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| public class NIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8888));
Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){ int select = selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()){ SelectionKey key = iterator.next(); if(key.isAcceptable()) { createChannel(key); } else if(key.isReadable()){ doRead(key); } else if(key.isWritable()){ doWrite(key); } iterator.remove(); } } }
private static void doWrite(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.wrap("send to Client".getBytes()); socketChannel.write(byteBuffer);
key.interestOps(SelectionKey.OP_READ); }
private static void doRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try{ int read = socketChannel.read(byteBuffer); byteBuffer.flip(); System.out.println("Server receive:" + new String(byteBuffer.array(), 0, read)); key.interestOps(SelectionKey.OP_WRITE); } catch (IOException e){ key.cancel(); } }
private static void createChannel(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("Accept connection from " + socketChannel);
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.wrap(("Welcome:" + socketChannel.getRemoteAddress() + " assigned to" + Thread.currentThread().getName()).getBytes()); socketChannel.write(byteBuffer);
socketChannel.register(key.selector(),SelectionKey.OP_READ ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(8888));
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("Hello I am Client".getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer);
byteBuffer.clear(); socketChannel.read(byteBuffer); byteBuffer.flip(); System.out.println(new String(byteBuffer.array())); } }
|
NIO为什么更好
- 使用比较少的线程就可以管理多个客户端连接,提高了并发量且减少了资源消耗
- 没有IO操作的时候,线程可以去执行其他任务,非阻塞。
使用NIO编写代码太难了
NIO非常难用,而且存在许多bug,开发和维护的成本比较大。一般情况下会使用Netty这个成熟的框架。
重要角色Netty
还在学习中…..