Socket 之 BIO、NIO、Netty 简单实现

一、概念

(1)Socket:套接字(Socket)是通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元,包含进行网络通信必须的五种信息:

  • 连接使用的协议
  • 本地主机的IP地址
  • 本地进程的协议端口
  • 远地主机的IP地址
  • 远地进程的协议端口

多个 TCP 连接或多个应用程序进程可能需要通过同一个 TCP 协议端口传输数据。为了区别不同的应用程序进程和连接,计算机操作系统为应用程序与 TCP/IP 协议交互提供了 套接字(Socket)接口。应用层可以和传输层通过Socket 接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

建立 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket,另一个运行于服务器端,称为 ServerSocket

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

Socket 可以支持不同的传输层协议(TCP/UDP),当使用 TCP 协议进行连接时,该 Socket 连接就是一个 TCP 连接, UDP 连接同理。

(2)IO:输入/输出(InputStream/OutPutStream),在 Java 中有三种方式:

  1. BIO:同步阻塞 IO(Blocking IO)。B 代表 Blocking。

  2. NIO:同步非阻塞 IO(Non-Nlocking IO / New IO)。集成在 JDK 1.4 及以上版本。

  3. AIO:异步非阻塞 IO(Asynchronize IO)。A 代表 Asynchronize。

(3)同步、异步、阻塞、非阻塞

  1. 同步:指用户进程触发 IO 操作后通过等待或者轮训的方式查看 IO 操作是否完成。

  2. 异步:当一个异步进程调用发出之后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态、通知来通知调用者,或者通过回调函数来处理这个调用。使用异步 IO 时,Java 将 IO 读写委托给 OSOperation System 即:操作系统) 处理,需要将数据缓冲区地址和大小传给 OS,OS 需要支持异步 IO 操作。

  3. 阻塞:进程在读取或写入数据时,会一直处于等待状态不能做其他事情,直到操作完成。

  4. 非阻塞:进程在读取或写入数据时,进程不会处于等待状态,可以做其他事情。

举例

故事:张三烧水。

演员:张三

道具:水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 同步阻塞(BIO):张三把水壶放到火上,并把自己关在厨房里,盯着壶里的水,等它烧开。
  2. 异步阻塞:张三把响水壶放到火上,并把自己关在厨房里,不用盯着壶里的水,靠汽笛声辨别是否烧开。(汽笛声:事件驱动)。
  3. 同步非阻塞(NIO):张三把水壶放到火上,然后就去书房学习,但是为了及时用上热水,他时不时的就得到厨房看一下烧水的状态。(时不时查看状态:轮询)
  4. 异步非阻塞(AIO):张三把响水壶放到火上,然后就去书房学习,不用时不时的到厨房看下烧水的状态,靠汽笛声辨别是否烧开。

二、基本介绍与实现

2-1、BIO 实现

SocketBIO 实现比较简单,也没有太多复杂的概念。但由于效率低下,故实际应用率不高。所以,鉴于以上两点,这里就直接上代码了。

MyBIOClient.java

import java.net.InetSocketAddress;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className MyClient
 * @description 客户端
 * @date 2020/12/20 20:52
 **/
public class MyBIOClient {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 创建 Socket 客户端
        Socket socket = new Socket();
        // 与服务端建立连接
        socket.connect(new InetSocketAddress("127.0.0.1", 8081));

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        int counter = 0;
        while (counter < 5) {
    
    
            String now = simpleDateFormat.format(new Date());
            // 发送请求
            socket.getOutputStream().write(now.getBytes("UTF-8"));
            socket.getOutputStream().flush();
            Thread.sleep(1000);
            counter++;
        }
        // 若方法运行结束后,不调用 close 函数,服务端则会报错:java.net.SocketException: Connection reset
        socket.close();
        System.out.println("客户端关闭了 Socket 连接~!");
    }
}

MyBIOService.java

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className MyService
 * @description 服务端
 * @date 2020/12/20 20:53
 **/
public class MyBIOService {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 创建 Socket 服务端,并设置监听的端口
        ServerSocket serverSocket = new ServerSocket(8081);
        // 创建线程池以执行客户端请求(防止因请求过多,而导致的阻塞)
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
        while (true) {
    
    
            // 阻塞方法,监听客户端请求
            Socket socket = serverSocket.accept();
            System.out.println("\r\n" + socket);
            // 创建自定义请求处理器
            SocketHandler handler = new SocketHandler(socket);
            // 处理客户端请求
            poolExecutor.execute(handler);
        }
    }
}

SocketHandler.java

import java.net.Socket;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className Handler
 * @description Socket 处理器
 * @date 2020/12/20 21:06
 **/
public class SocketHandler implements Runnable {
    
    
    private Socket socket;
    private static final byte[] BUFFER = new byte[1024];

    @Override
    public void run() {
    
    
        try {
    
    
            while (true){
    
    
                // 读取客户端 Socket 请求数据
                int read = socket.getInputStream().read(BUFFER);
                if (read != -1) {
    
    
                    System.out.println(new String(BUFFER, "UTF-8"));
                }else{
    
    
                    socket.close();
                    System.out.println("服务端关闭了 Socket 连接~!");
                    break;
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public SocketHandler(Socket socket) {
    
    
        this.socket = socket;
    }
}

2-2、NIO

2-2-1、NIO介绍

同步非阻塞 IONon-Blocking IO / New IO)出现在 JDK 1.4 及以上版本。

NIO 服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 IO 请求时才启动一个线程进行处理。

2-2-2、通道(Channels)

NIO 新引入的最重要的抽象是通道的概念。Channel 数据连接的通道。 数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中 。

2-2-3、缓冲区(Buffers)

顾名思义,数据缓冲器。通道 channel 可以向缓冲区 Buffer 中写数据,也可以像 Buffer 中存数据。

2-2-4、选择器(Selector)

使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实时监控和维护。

在这里插入图片描述

2-2-5、特点

BIO 不同,当 NIO 完成一个连接的建立后,不需要单独为当前连接创建一个线程。这个连接会被注册到多路复用器(Selector)上,所以一个连接只需要一个线程即可,且该线程所在的多路复用器会通过轮询,来监听线程请求,只有在发现连接有请求时,才开启一个线程进行请求处理。

在这里插入图片描述

如上图所示,BIO 模型中,一个连接来了,会创建一个线程,并执行无限循环,无限循环的目的就是不断监测这条连接上是否有数据可以读。假如某一时间段内,服务端接收到了1w 个连接请求,而里面同一时刻只有少量的连接有数据可读,因此,很多个执行无限循环的线程就都白白浪费掉了。

而在 NIO 模型中,他把这么多无限循环变成一个无限循环,这个无限循环由一个线程控制,那么他又是如何做到一个线程,一个无限循环就能监测 1w 个连接是否有数据可读的呢?

这就是 NIO 模型中 Selector 的作用,一个连接建立之后,先不创建无限循环去监听是否有数据可读了,而是直接把这条连接注册到 Selector 上。然后,通过检查这个 Selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 BIONIO 的区别:

  1. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100个小朋友就需要100个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是 BIO 模型,一个连接对应一个线程。
  2. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。

2-2-6、NIO 点对点通信

MyNIOService.java
/**
 * @author Supreme_Sir
 * @version 1.0
 * @className MyNIOService
 * @description 服务端
 * @date 2020/12/21 6:04
 **/

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;

public class MyNIOService extends Thread {
    
    
    //1.声明多路复用器
    private Selector selector;
    //2.定义读写缓冲区
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    //3.定义构造方法初始化端口
    public MyNIOService(int port) {
    
    
        init(port);
    }

    //4.main方法启动线程
    public static void main(String[] args) {
    
    
        new Thread(new MyNIOService(8888)).start();
    }

    //5.初始化
    private void init(int port) {
    
    
        try {
    
    
            System.out.println("服务器正在启动......");
            //1)开启多路复用器
            this.selector = Selector.open();
            //2) 开启服务通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //3)设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //4)绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));
            //5)注册,标记服务通标状态
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动完毕");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                //1.当有至少一个通道被选中,执行此方法
                this.selector.select();
                //2.获取选中的通道编号集合
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                //3.遍历keys
                while (keys.hasNext()) {
    
    
                    SelectionKey key = keys.next();
                    //4.当前key需要从集合中移出,如果不移出,下次循环会执行对应的逻辑,造成业务错乱
                    keys.remove();
                    //5.判断通道是否有效
                    if (key.isValid()) {
    
    
                        try {
    
    
                            //6.判断是否可读
                            if (key.isAcceptable()) {
    
    
                                accept(key);
                            }
                        } catch (CancelledKeyException e) {
    
    
                            //出现异常断开连接
                            key.cancel();
                        }
                        try {
    
    
                            //7.判断是否可读
                            if (key.isReadable()) {
    
    
                                read(key);
                            }
                        } catch (CancelledKeyException e) {
    
    
                            //出现异常断开连接
                            key.cancel();
                        }
                        try {
    
    
                            //8.判断是否可写
                            if (key.isWritable()) {
    
    
                                write(key);
                            }
                        } catch (CancelledKeyException e) {
    
    
                            //出现异常断开连接
                            key.cancel();
                        }
                    }
                }
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    private void accept(SelectionKey key) {
    
    
        try {
    
    
            //1.当前通道在init方法中注册到了selector中的ServerSocketChannel
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            //2.阻塞方法, 客户端发起后请求返回.
            SocketChannel channel = serverSocketChannel.accept();
            ///3.serverSocketChannel设置为非阻塞
            channel.configureBlocking(false);
            //4.设置对应客户端的通道标记,设置次通道为可读时使用
            channel.register(this.selector, SelectionKey.OP_READ);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    //使用通道读取数据
    private void read(SelectionKey key) {
    
    
        try {
    
    
            //清空缓存
            this.readBuffer.clear();
            //获取当前通道对象
            SocketChannel channel = (SocketChannel) key.channel();
            //将通道的数据(客户发送的data)读到缓存中.
            int readLen = channel.read(readBuffer);
            //如果通道中没有数据
            if (readLen == -1) {
    
    
                //关闭通道
                key.channel().close();
                //关闭连接
                key.cancel();
                return;
            }
            //Buffer中有游标,游标不会重置,需要我们调用flip重置. 否则读取不一致
            this.readBuffer.flip();
            //创建有效字节长度数组
            byte[] bytes = new byte[readBuffer.remaining()];
            //读取buffer中数据保存在字节数组
            readBuffer.get(bytes);
            System.out.println("收到了从客户端 " + channel.getRemoteAddress() +
                    " : " + new String(bytes, "UTF-8"));
            //注册通道,标记为写操作
            channel.register(this.selector, SelectionKey.OP_WRITE);
        } catch (Exception e) {
    
    
        }
    }

    //给通道中写操作
    private void write(SelectionKey key) {
    
    
        //清空缓存
        this.readBuffer.clear();
        //获取当前通道对象
        SocketChannel channel = (SocketChannel) key.channel();
        //录入数据
        Scanner scanner = new Scanner(System.in);
        try {
    
    
            System.out.println("即将发送数据到客户端..");
            String line = scanner.nextLine();
            //把录入的数据写到Buffer中
            writeBuffer.put(line.getBytes("UTF-8"));
            //重置缓存游标
            writeBuffer.flip();
            channel.write(writeBuffer);
            channel.register(this.selector, SelectionKey.OP_READ);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

MyNIOClient.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className MyNIOClient
 * @description 客户端
 * @date 2020/12/21 6:04
 **/

public class MyNIOClient {
    
    
    public static void main(String[] args) {
    
    
        //创建远程地址
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        SocketChannel channel = null;
        //定义缓存
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        try {
    
    
            //开启通道
            channel = SocketChannel.open();
            //连接远程远程服务器
            channel.connect(address);
            Scanner sc = new Scanner(System.in);
            while (true) {
    
    
                System.out.println("客户端即将给 服务器发送数据..");
                String line = sc.nextLine();
                if (line.equals("exit")) {
    
    
                    break;
                }
                //控制台输入数据写到缓存
                buffer.put(line.getBytes("UTF-8"));
                //重置buffer 游标
                buffer.flip();
                //数据发送到数据
                channel.write(buffer);
                //清空缓存数据
                buffer.clear();
                //读取服务器返回的数据
                int readLen = channel.read(buffer);
                if (readLen == -1) {
    
    
                    break;
                }
                //重置buffer游标
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                //读取数据到字节数组
                buffer.get(bytes);
                System.out.println("收到了服务器发送的数据 : " + new String(bytes, "UTF-8"));
                buffer.clear();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (null != channel) {
    
    
                try {
    
    
                    channel.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

2-3、Netty

2-3-1、认识 Netty

Netty 是由 JBOSS 提供一个异步的、 基于事件驱动的网络编程框架。

Netty 可以帮助你快速、 简单的开发出一 个网络应用, 相当于简化和流程化了 NIO 的开发过程。 作为当前最流行的 NIO 框架, Netty 在互联网领域、 大数据分布式计算领域、 游戏行业、 通信行业等获得了广泛的应用, 知名的 ElasticsearchDubbo 框架内部都采用了 Netty

2-3-2、为什么要用 Netty

NIO 缺点

  • NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 SelectorServerSocketChannelSocketChannelByteBuffer 等。

  • 可靠性不强,开发工作量和难度都非常大。

  • NIOBug。例如 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%

Netty 优点

  • 对各种传输协议提供统一的 API
  • 高度可定制的线程模型——单线程、一个或多个线程池
  • 更好的吞吐量,更低的等待延迟
  • 更少的资源消耗
  • 最小化不必要的内存拷贝

2-3-3、Netty 模型

在这里插入图片描述

Netty 抽象出两组线程池, BossGroup 专门负责接收客 户端连接, WorkerGroup 专门负责网络读写操作。
NioEventLoop 表示一个不断循环执行处理 任务的线程, 每个 NioEventLoop 都有一个 Selector, 用于监听绑定在其上的 Socket 网络通道。 NioEventLoop 内部采用串行化设计, 从消息的读取->解码->处理->编码->发送, 始终由 IO 线 程 NioEventLoop 负责。

2-3-4、Netty 聊天室

ChatGroupClient.java
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className ChatGroupClient
 * @description 聊天室,客户端
 * @date 2020/12/23 8:49
 **/
public class ChatGroupClient {
    
    
    private final String host;
    private final int port;

    public static void main(String[] args) throws Exception {
    
    
        ChatGroupClient chatGroupClient = new ChatGroupClient("127.0.0.1", 9999);
        chatGroupClient.start();
    }

    public void start() throws Exception {
    
    
        EventLoopGroup group = new NioEventLoopGroup();
        try {
    
    
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("MyDecoder", new StringDecoder())
                                    .addLast("MyEnCoder", new StringEncoder())
                                    .addLast(new SimpleChannelInboundHandler<String>() {
    
    
                                        @Override
                                        protected void channelRead0(ChannelHandlerContext ctx, String s) {
    
    
                                            System.out.println(s);
                                        }
                                    });
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            Channel channel = channelFuture.channel();
            Scanner scanner = new Scanner(System.in);
            while (true) {
    
    
                if (scanner.hasNext()) {
    
    
                    String message = scanner.next();
                    if (!"close".equals(message)) {
    
    
                        channel.writeAndFlush(message);
                    } else {
    
    
                        break;
                    }
                }
            }
        } finally {
    
    
            group.shutdownGracefully();
        }

    }

    public ChatGroupClient(String host, int port) {
    
    
        this.host = host;
        this.port = port;
    }
}
ChatGroupServer.java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author Supreme_Sir
 * @version 1.0
 * @className ChatGroupServer
 * @description 聊天室,服务端
 * @date 2020/12/23 8:49
 **/
public class ChatGroupServer {
    
    
    private final int port;

    public static void main(String[] args) throws Exception{
    
    
        ChatGroupServer chatGroupServer = new ChatGroupServer(9999);
        chatGroupServer.start();
    }

    public void start() throws Exception {
    
    
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ServerChannelInitializer());

            ChannelFuture channelFuture = bootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
    
    
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public ChatGroupServer(int port) {
    
    
        this.port = port;
    }
}

Tips:由于聊天室功能需要多个客户端才能更好的测试,则在进行代码调试时,最好执行多个 ChatGroupClient 客户端以更好的模拟聊天室效果。为了防止有些同学不知怎么运行多个客户端程序,这里做下说明:

  1. 找到 Edit Configurations

在这里插入图片描述

  1. 勾选 Allow Parallel run

在这里插入图片描述

源码

源码下载

-------------------------------- 既然认准一条路,就别去打听要走多久。 --------------------------------

猜你喜欢

转载自blog.csdn.net/Supreme_Sir/article/details/112725728
今日推荐