网络编程从BIO到NIO

关键词:BIO    NIO    ServerSocket    ServerSocketChannel    

 


本文主要从BIO开始聊起,涉及到BIO的使用以及BIO存在的问题,并且根据问题提出解决方案。然后再聊聊NIO的使用。了解了BIO存在的问题以及为什么产生了NIO对理解两者有很大的帮助作用。


█ BIO

ServerSocket是一个BIO操作,BIO,blocking I/O。即阻塞IO,为什么说是阻塞的呢?在哪里阻塞呢?阻塞了什么呢?

先来看看BIO的使用:

  • 单线程使用ServerSocket

①创建服务端

package bio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO服务器。
 *
 */
public class BIOServer {

    public static void main(String[] args) throws Exception{
        // ServerSocket就是一个阻塞的IO
        ServerSocket serverSocket = new ServerSocket(8899);

        while (true) {
            System.out.println("waiting connection");
            Socket socket = serverSocket.accept();
            System.out.println("one client connection");
            InputStream socketInputStream = socket.getInputStream();
            System.out.println("waiting receive");
            byte[] bytes = new byte[1024];
            socketInputStream.read(bytes);
            System.out.println("receive:"+new String(bytes));
        }

    }
}

②创建两个客户端,用来连接服务端

package bio;

import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 *
 * 客户端1
 *
 */
public class Client1 {

    public static void main(String[] args) throws Exception{
        // 连接上服务端
        Socket socket = new Socket("localhost", 8899);
        OutputStream outputStream = socket.getOutputStream();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String nextLine = scanner.nextLine();
            // 像服务端发送数据
            outputStream.write(nextLine.getBytes());
        }
    }
}
package bio;

import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 *
 * 客户端2
 *
 */
public class Client2 {

    public static void main(String[] args) throws Exception{
        // 连接上服务端
        Socket socket = new Socket("localhost", 8899);
        OutputStream outputStream = socket.getOutputStream();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String nextLine = scanner.nextLine();
            // 像服务端发送数据
            outputStream.write(nextLine.getBytes());
        }
    }
}

③启动服务端,观察服务端BIOServer控制台输出。(控制台打印waiting connection,并没有one client connection,此时程序阻塞在Socket socket = serverSocket.accept();这里,等待客户端连接。)

④启动一个客户端1,观察服务端BIOServer控制台输出。(控制台继续打印,但没有打印出receive:,此时程序阻塞在socketInputStream.read(bytes);等待客户端1的输出内容。)

⑤在客户端1的控制台中输入几个内容,观察服务端控制台输出。(我在客户端1的控制台输入了hello world,此时服务端接收到内容,并打印到控制台,此时。一个while循环结束,继续下一个循环,所有继续打印了waiting connection)

⑥清空服务端的控制台内容,关闭客户端1的服务然后再将其启动,启动完成之后,再启动客户端2,在服务端2的控制台输入:hello world.

⑦观察服务端的控制台,并没有打印出hello world。为什么呢?其实,当客户端1连接上来之后,当前循环的代码已经执行到了socketInputStream.read(bytes);并阻塞在这里等待客户端1发来内容。于是,客户端2的连接和输出都被阻塞了。在客户端1控制台输入内容,此时再观察服务端控制台内容:

发现问题:ServerSocket会在两个方法阻塞,一个是accept(),一个是read()。从步骤6中可以发现,如果在单线程中使用ServerSocket,一次只能与一个客户端完成连接,如果此时这个已经连接的客户端没有发送任何数据,就会一直阻塞在read方法,其他的客户端的连接也会被阻塞了。

解决思路:为了使ServerSocket能够在一个客户端连接上来之后,继续连接上其他的客户端,可以使用多线程解决问题。一个客户端连接上来之后,开辟一个线程去等待该客户端的内容,主线程还能继续处理其他客户端的连接。

  • 多线程使用ServerSocket

①改造服务端代码,客户端代码不变

package bio;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO服务器。使用多线程。一个连接对应一个线程
 *
 */
public class BIOServer2 {

    public static void main(String[] args) throws Exception{
        // ServerSocket就是一个阻塞的IO
        ServerSocket serverSocket = new ServerSocket(8899);

        while (true) {
            System.out.println("waiting connection");
            Socket socket = serverSocket.accept();

            new Thread(()->{
                try {
                    System.out.println("one client connection");
                    InputStream socketInputStream = socket.getInputStream();
                    System.out.println("waiting receive");
                    byte[] bytes = new byte[1024];
                    socketInputStream.read(bytes);
                    System.out.println("receive:"+new String(bytes));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}

②启动服务端,先启动客户端1,再启动客户端2,再客户端2的控制台输入数据,此时观察服务端控制台输出:

此时解决了使用单线程造成连接阻塞的问题。

发现问题:多线程虽然能够解决多个连接的问题,但每一个连接都会开启一个线程,这样不仅会产生太多的线程资源,即使使用线程池也会造成线程资源的浪费。一般客户端连接是有两个步骤的,一个是连接,一个是发送数据。假设有10000个连接上来了,但只有100个连接会发送数据,这样就会造成线程去处理9900个连接的浪费。

思考:多线程解决了连接阻塞的问题,但是增加了线程资源。单线程呢,没有线程资源的浪费,但又会阻塞连接。要是能够在单线程下,不产生阻塞就好了。即serverSocket.accept()和socketInputStream.read(bytes)两个方法不会阻塞。比如:

// 提供一个api,设置这里不要阻塞
Socket socket = serverSocket.accept();

// 提供一个api,设置这里不要阻塞
socketInputStream.read(bytes);

解决思路:ServerSocket是JDK提供的类,我们不能修改源代码。好在对于上面的问题,JDK的开发人员早已经发现了问题,并提供了SocketChannel,实现类不阻塞的功能。可以把SocketChannel就理解成是一个ServerSocket,在其基础上解决了阻塞的问题。(毕竟ServerSocket已经被广泛使用,不能直接修改它的源代码,那就新写一个类)


█ NIO

NIO不再使用ServerSocketSocket,引出了ServerSocketChannelSocketChannel。ServerSocketChannel对应ServerSocket,SocketChannel对应ServerSocket。

①编写服务端代码(注意是单线程处理请求的哦)

package bio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 *
 * NIO服务器,ServerSocketChannel
 *
 */
public class NIOServer {

    public static void main(String[] args) throws Exception{
        // 下面这两个相当于ServerSocket serverSocket = new ServerSocket(8899);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8899));

        // 设置不阻塞,相当于设置serverSocket.accept()不会阻塞
        // 不设置false,接收连接还是阻塞的
        serverSocketChannel.configureBlocking(false);

        while (true) {
            // serverSocket.accept()
            SocketChannel socketChannel = serverSocketChannel.accept();

            System.out.println("等待客户端连接:"+socketChannel);

            if (socketChannel!=null) {
                System.out.println("客户端连接成功,等待接收数据...");
                // 设置不阻塞,相当于设置socketInputStream.read(bytes)不会阻塞
                // 不设置false,读取数据还是阻塞的
                socketChannel.configureBlocking(false);

                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int read = socketChannel.read(byteBuffer);

                if (read>0) {
                    byteBuffer.flip();
                    System.out.println("接收客户端数据:"+byteBuffer.toString());
                }
            }
        }

    }

}

(特别注意serverSocketChannel.configureBlocking(false); socketChannel.configureBlocking(false);)

②运行服务端,观察控制台输出

在没有客户端连接的时候,serverSocketChannel.accept()会返回null,不像serverSocket.accept()是一直阻塞等待。

③注释掉代码:System.out.println("等待客户端连接:"+socketChannel); (输出太多,影响观感),重启服务端,然后运行客户端1,观察服务端控制台输出。

服务端接收到客户端的请求,其实此时的代码并没有阻塞在等待客户端1的内容,而是一直在一次次循环中。

④在客户端1输入内容发送给服务端,观察服务端控制台输出。

发现问题:客户端1发送的内容呢?服务端并没有打印出来?这是为什么呢。其实,看看服务端代码,就能发现,在接收到客户端1的连接之后,代码继续运行,到了下一个循环里,此时serverSocketChannel.accept()==null了,相当于把客户端1的连接信息覆盖掉了,导致客户端1的连接信息丢失。

解决思路:要是能够提供一个集合能够存储每一次连接Socket,这样就不会弄丢以前的连接信息。

⑤改造代码

package bio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * NIO服务器,ServerSocketChannel
 *
 */
public class NIOServer2 {

    // 创建一个集合,用于记录每一个连接信息
    private static List<SocketChannel> socketChannelList = new ArrayList<>();

    public static void main(String[] args) throws Exception{
        // 下面这两个相当于ServerSocket serverSocket = new ServerSocket(8899);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8899));

        // 设置不阻塞,相当于设置serverSocket.accept()不会阻塞
        // 不设置false,接收连接还是阻塞的
        serverSocketChannel.configureBlocking(false);

        while (true) {
            // serverSocket.accept()
            SocketChannel socketChannel = serverSocketChannel.accept();

            if (socketChannel!=null) {
                // 客户端连接成功了,加入集合中
                socketChannelList.add(socketChannel);
            }
            // 遍历集合,看看各个客户端是否发送了数据
            for (SocketChannel channel : socketChannelList) {
                // 设置不阻塞,相当于设置socketInputStream.read(bytes)不会阻塞
                // 不设置false,读取数据还是阻塞的
                channel.configureBlocking(false);

                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int read = channel.read(byteBuffer);

                if (read>0) {
                    byteBuffer.flip();
                    System.out.println("接收客户端数据:"+new String(byteBuffer.array()));
                }
            }
        }

    }

}

⑥依次启动服务端、客户端1、客户端2;用客户端2发送数据,客户端1发送数据,观察服务端控制台输出。

可以发现,解决了连接丢失的问题。

发现问题:ServerSocketChannel解决了ServerSocket阻塞的问题,使用单线程也能处理了多个连接的问题。可是,上面的代码就可以了吗?并没有。在上面的代码里,我使用了一个集合去记录所有的客户端连接,然后一遍遍循环集合看看客户端是否有内容。想想假设有10000个客户端连接了,集合里面就有10000个客户端数据,然而只有100个客户端会发送数据,这样在遍历集合的时候,其实只有100个是有效的数据,其余的都是无效的遍历。这样是不是也会造成资源的浪费了。

解决思路:这些问题,JDK的开发人员也帮我们解决了,于是就引出了“IO多路复用”的概念了,其中有包括“select”、“poll”、“epoll”,可以说多路复用解决的就是循环遍历造成的资源浪费问题。关于多路复用,会另起一篇来写的。

关于BIO到NIO就到这里了吧!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值