什么是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(() - > {
// socket连接处理
}).start();
}

线程池可以一定程度上缓解此问题,但无法根本改变:

1
2
3
4
5
6
while(true){
Socket socket = serverSocket.accept();
threadPool.execute(() -> {
// socket连接处理
});
}

无论如何优化,它底层还是同步阻塞的IO,无法从根本上解决问题。

同步和异步(消息通信机制)

  • 同步:发出一个调用时,在没有得到结果之前,该调用就不返回
  • 异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果

阻塞和非阻塞(等待调用结果时的状态)

  • 阻塞:调用结果返回之前,线程一直挂起
  • 非阻塞:调用没有返回结果之前,线程不会阻塞

再看NIO

NIO(Non-blocking IO)是一种同步非阻塞IO,在java1.4时引入,对应java.nio包。
NIO提供了与传统BIO中的SocketServerSocket相对应的SocketChannelServerSocketChannel两种不同套接字通道的实现,两种通道都支持阻塞和非阻塞两种模式:

  • 阻塞模式:基本不会使用。与传统网络编程一样,简单但性能和可靠性不好。
  • 非阻塞模式:对高负荷、高并发应用非常好,但是编程麻烦。这也是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();
// 获取当前选择器中所有注册的key(已就绪的监听事件)
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{
// 当远程SocketChannel断开连接时,Server端会触发一个read事件,此处会发生异常
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 {

// 获取key的监听通道
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);

//***

// 注册连接到selector, 绑定监听读数据
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

还在学习中…..