BIO 全称 Block-IO
是一种**同步且阻塞
**的通信模式。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
同步: 可以理解为干这件事中间,不能干其他事
阻塞: 可以理解为有事把游戏暂停了,干完事了再来继续游戏
概念不好理解,直接上Demo
public class SocketServer {
private static void handler(Socket clientSocket) throws Exception {
byte[] bytes = new byte[1024];
System.out.println("准备read。。。");
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕!");
if(read != -1){
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
}
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8001);
while(true){
System.out.println("等待连接。。。");
// 阻塞住了
Socket clientSocket = serverSocket.accept();
System.out.println("有客户端连接了。。。");
handler(clientSocket);
}
}
}
先理解一下这段代码里面的 Socket clientSocket = serverSocket.accept();
和 int read = clientSocket.getInputStream().read(bytes);
,这两端代码都是阻塞
的,也就是当执行到这里的时候,就会卡住了,暂时不会执行下面的东西了
启动的时候,这里控制台输出完等待连接以后,就会卡住了
然后这里使用一个Telnet的东西,百度一下即可
使用Telnet搭建一个客户端连接到上面的服务端中
我们这个时候再开一个Telnet客户端连接到上面的服务端
这时候再第一个Telnet中随便摁下键盘,你会发现控制台输出
我觉的这里有两点:
这感觉是同步
)这应该就是阻塞
)现在应该能体验到这种方式比较局限,它适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
稍微改进一些
public class SocketServer {
private static void handler(Socket clientSocket) throws Exception {
byte[] bytes = new byte[1024];
System.out.println("准备read。。。");
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕!");
if(read != -1){
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
}
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8001);
while(true){
System.out.println("等待连接。。。");
// 阻塞住了
Socket clientSocket = serverSocket.accept();
System.out.println("有客户端连接了。。。");
new Thread(() -> {
try {
handler(clientSocket);
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
}
修改的地方也就是多开了线程去连接下一个客户端,也就是每连接一个客户端就会新开一个线程(也就是打破了阻塞
),使用两个Telnet试一试
并且当你在两个Telnet嗯东西的时候,控制台也有反应
两种方式都是BIO:
这样通过通过多线程的方式,确实可以解决一些问题,但是还是会带来一些新的问题,所以要寻求更好的解决方法
Java NIO,全程 Non-Block IO ,是 Java SE 1.4 版以后,针对网络传输效能优化的新功能。是一种非阻塞同步的通信模式。
public class NioServer {
// 保存客户端连接
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) throws IOException {
// 创建NIO ServerSocketChannel,与BIO的serverSocket类似
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(8001));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
System.out.println("服务启动成功");
while (true) {
// 非阻塞模式accept方法不会阻塞,否则会阻塞
// NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
SocketChannel socketChannel = serverSocket.accept();
if (socketChannel != null) { // 如果有客户端进行连接
System.out.println("连接成功");
// 设置SocketChannel为非阻塞
socketChannel.configureBlocking(false);
// 保存客户端连接在List中
channelList.add(socketChannel);
}
// 遍历连接进行数据读取 10w - 1000 读写事件
Iterator<SocketChannel> iterator = channelList.iterator();
while (iterator.hasNext()) {
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 非阻塞模式read方法不会阻塞,否则会阻塞
int len = sc.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println(Thread.currentThread().getName() + " 接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
}
}
自己看懂就可以,看不懂,看我下面
怎么去理解这个非阻塞是什么意思呢?你Debug启动一下项目然后,在SocketChannel socketChannel = serverSocket.accept();
打一个断点,然后点下面那个按钮,你就会发现它会一直循环,这就是非阻塞了
这里就可以看出来,accept后,通过不断的轮询channelist中的连接,有则打印出来,没有就继续accept,没有中间阻塞的情况,这里也没有使用多线程,也就是说用一个线程,完成了BIO那里开多个线程完成的事情
这里可以同时开几个Telnet,然后Debug启动服务端,进行一下联调,差不多就能理解点了
这里会发现还有优化的空间,如果我们这里连接了10w个客户端,但是只有1w个客户端有真正的事件发生,我们的关注点应该在那1w个上面,如果我们每次都要去遍历这10w个客户端的话,很头疼的
这个就是解决了上面的那个问题,使用了多路复用器
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
int OP_ACCEPT = 1 << 4;
System.out.println(OP_ACCEPT);
// 创建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(8001));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功");
while (true) {
// 阻塞等待需要处理的事件发生 已注册事件发生后,会执行后面逻辑
selector.select();
// 获取selector中注册的全部事件的 SelectionKey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历SelectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = socketChannel.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println(Thread.currentThread().getName() + "接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
}
这个为了解决上面那个问题,加入了多路复用,为不让他做一些无用的循环遍历,抛弃了channellist集合,把连接都注册到多路复用器里面
我大致理解的就是这样子的,可能不太周到
这样子处理的话,如果连接了10w个连接,当有事件的连接过来的时候,就会去处理该连接,而不会全局的循环,也就避免了时间上的消耗
还有一个AIO,之后遇到了再总结吧!