分布式(二)——基于消息方式实现系统间通信

通信主要是两个要点:消息处理消息传输

  • 消息处理:读取数据或者写入数据(同步信息采用BIONIO异步信息采用AIO
  • 消息传输:借助网络协议(TCP/IPUDP/IP)来实现消息传输

四种方法实现基于消息方式实现系统间通信:

  • TCP/IP+BIO
  • TCP/IP+NIO
  • UDP/IP+BIO
  • UDP/IP+NIO

一、 术语解释

  • TCP/IP:
    一种可靠的网络数据传输协议。要求通信双方先建立连接,再进行通信。(保证数据传输的可靠性,会牺牲一些性能
  • UDP/IP:
    一种不可靠的网络数据传输协议。并不直接给通信双方建立连接,而是发送到网络上通信。(不能保证数据传输的可靠,性能较好
  • BIO:
    同步阻塞IO,数据的读取写入必须阻塞在一个线程内等待其完成。(一旦有请求链接,则新建一个线程,来处理这次请求,效率很低,无法实行高并发场景
  • NIO:
    同步非阻塞IO,通过通道缓冲区来实现非阻塞,当有流可读或者可以写时,操作系统会通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。(不再是一个连接就要对应一个处理线程)
  • AIO:
    异步非阻塞I/O,应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

二、 TCP/IP+BIO

基于Socket、ServerSocket来实现TCP/IP+BIO的系统通信。

  • Socket:用于实现建立连接
  • ServerSocket:用于实现服务器端口的监听

为了满足服务端可以同时接受多个请求,最简单的方法是生成多个Socket,会产生两个问题:

  1. 生成太多Socket会消耗过多资源
  2. 频繁地创建Socket还会影响性能。

为了解决上面的问题,通常采用连接池维护Socket。

1. 服务端

  • 服务端使用ServerSocket监听端口
  • 等待客户端连接,只有连接上时才运行后面的操作,否则一直堵塞
  • 连接成功后,通知客户端连接成功
  • 读取客户端发送的信息,没有发送则一直堵塞,读取到信息时通知客户端:服务端已收到信息
try {
    //服务端监听端口8086
    ServerSocket serverSocket = new ServerSocket(8086);
    while (true){
        //同步阻塞,直到建立连接
        Socket client = serverSocket.accept();
        //客户端连接成功时,发送信息给客户端
        OutputStream outputStream = client.getOutputStream();
        outputStream.write(("你【"+client.getPort()+"】"+"成功连接到服务端端口8086").getBytes());
        outputStream.flush();
        //服务端控制台打印连接成功信息
        System.out.println("客户端【" + client.getPort()+"】已连接");

        try {
            //获取客户端输入的信息
            InputStream inputStream = client.getInputStream();
            byte[] bytes = new byte[1024];
            while (inputStream.read(bytes)!=-1){
                //收到信息时,通知客户端,服务端已收到信息
                outputStream.write(("服务端已收到你的信息"+new String(bytes)).getBytes());
                outputStream.flush();
                //将接收到的信息打印到控制台
                System.out.println("收到信息:"+new String(bytes));
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }

} catch (Exception e) {
    e.printStackTrace();
}

2. 客户端

  • 客户端使用Socket连接服务端
  • 启动线程实时监听服务端发送来的信息
  • 在控制台输入信息,发送到服务端
  • 当输入exit命令时,关闭连接
try {
    //控制台输入信息(用于发送信息给服务端)
    Scanner scanner = new Scanner(System.in);
    //创建连接
    Socket socket = new Socket("127.0.0.1",8086);
    //启动线程监听服务端发来的信息
    new Thread(()->{
        //读取服务端发来的信息
        InputStream inputStream = null;
        try {
            inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024 * 4];
            while ((inputStream.read(bytes)) != -1) {
                System.out.println("收到服务端消息:" + new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }).start();

    OutputStream outputStream = socket.getOutputStream();
    while(true){
        //读取控制台输入的内容
        String content = scanner.nextLine();
        //输入exit,结束通讯
        if(content.equals("exit")){
            //关闭输出流
            outputStream.close();
            //断开连接
            socket.close();
        }else{
            outputStream.write(content.getBytes());
            outputStream.flush();
        }
    }

} catch (IOException e) {
    e.printStackTrace();
}

3. 测试

启动上述的服务端、客户端,控制台提示连接成功。

服务端:
在这里插入图片描述

客户端
在这里插入图片描述

客户端发送信息到服务端。

客户端:
在这里插入图片描述

服务端:
在这里插入图片描述

输入exit命令,客户端关闭连接

客户端:
在这里插入图片描述

三、TCP/IP+NIO

基于Clannel(SocketClannelServerSocketChannel)和Selector的相关类来实现TCP/IP+NIO方式的系统间通信

  • SocketClannel: 用于建立连接、监听事件及操作读写。
  • ServerSocketClannel: 用于监听端口即监听连接事件。
  • Selecter: 获取是否有要处理的事件。

3.1 服务端

  • 开启通道用于监听端口
  • 创建选择器,将通道注册到选择器中
  • 等待客户连接,接入前阻塞
  • 选择器论查询,检测就绪情况
  • 获取就绪集合
  • 根据就绪事件类型,调用对应得业务处理方法
public class Main {

    public void start() throws IOException {
        //创建选择器
        Selector selector = Selector.open();
        //创建Channel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //为channel通道绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8086));
        //设置channel为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //将通道注册到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //监听连接事件
        System.out.println("服务器启动成功!");
        //循环等待接入
        while(true){//while(true)  c for;;
            //获取可用channel数量
            int readyChannels = selector.select();
            if (readyChannels == 0) continue;
            //获取可用channel的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // selectionKey实例
                SelectionKey selectionKey = (SelectionKey) iterator.next();
                //移除Set中的当前selectionKey
                iterator.remove();
                /**
                 * 根据就绪事件类型,调用对应业务处理方法
                 */

                //密钥的通道已准备好可接入
                if (selectionKey.isAcceptable()) {
                    acceptHandler(serverSocketChannel, selector);
                }

                // 密钥的通道已准备好可读取
                if (selectionKey.isReadable()) {
                    readHandler(selectionKey,selector);
                }
            }
        }
    }

    /**
     * 接入事件处理器
     */
    private void acceptHandler(ServerSocketChannel serverSocketChannel,
                               Selector selector) throws IOException {
        //创建socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        //设置为非阻塞工作模式
        socketChannel.configureBlocking(false);
        //将channel注册到selector上,监听可读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        //回复客户端提示信息
        socketChannel.write(ByteBuffer.wrap("已成功连接服务端:8086".getBytes()));
    }

    //可读事件处理器
    private void readHandler(SelectionKey selectionKey,
                             Selector selector) throws IOException{
        //创建socketChannel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        //设置为非阻塞工作模式
        socketChannel.configureBlocking(false);
        //创建按buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //读取客户端请求内容
        StringBuffer requestContent = new StringBuffer();

        if(socketChannel.read(byteBuffer)>0){
            //切换buffer为读模式
            byteBuffer.flip();
            requestContent.append(Charset.forName("UTF-8").decode(byteBuffer));
        }
        System.out.println("接收到客户端信息:"+requestContent);
    }

    public static void main(String[] args) throws IOException {
        Main main  = new Main();
        main.start();
    }
}

3.2 客户端

ClientHandler端

/**
 * 用于接收服务端的消息
 */
public class NioClientHandler implements Runnable{

    private Selector selector;

    public NioClientHandler(Selector selector){
        this.selector = selector;
    }
    @Override
    public void run() {

        try{
            while(true){
                //获取选择器中的可用通道数
                int readChannel = selector.select();
                if(readChannel==0){
                    return;
                }
                Set<SelectionKey> selectionKeys =selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();

                while(iterator.hasNext()){
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    //事件为可读事件时
                    if(selectionKey.isReadable()){
                        readHandler(selectionKey,selector);
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }

    //可读事件处理器
    private void readHandler(SelectionKey selectionKey,
                             Selector selector) throws IOException{
        //创建socketChannel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        //设置为非阻塞工作模式
        socketChannel.configureBlocking(false);
        //创建按buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //读取客户端请求内容
        StringBuffer requestContent = new StringBuffer();

        if(socketChannel.read(byteBuffer)>0){
            //切换buffer为读模式
            byteBuffer.flip();
            requestContent.append(Charset.forName("UTF-8").decode(byteBuffer));
        }
        //打印服务端发送来的信息
        System.out.println(requestContent);
    }
}

Client端

public class Main {

    public static void main(String[] args) {
        //控制台输入信息(用于发送信息给服务端)
        Scanner scanner = new Scanner(System.in);
        try {
            //打开一个通道
            SocketChannel channel = SocketChannel.open();
            //创建选择器
            Selector selector= Selector.open();
            //设为非阻塞模式
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
            new Thread(new NioClientHandler(selector)).start();
            //想服务端发起连接请求
            if(!channel.connect(new InetSocketAddress("127.0.0.1",8086))){
                //不断轮查连接状态,直到连接成功
                while (!channel.finishConnect()){
                    //非阻塞在等待连接成功的期间,可以做其他操作
                    System.out.println("还没连上,只能摸鱼....");
                }
            }
            System.out.println("连接成功,结束非阻塞");
            //将控制台输入的信息发送到服务端
            while(scanner.hasNextLine()){
                channel.write(ByteBuffer.wrap(scanner.nextLine().getBytes()));
            }

            //关闭信道
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

四、UDP/IP+BIO

Java对UDP/IP方式的网络数据传输同样采用Socket机制,只是UDP/IP下的Socket没有建立连接,因此无法双向通信。如果需要双向通信,必须两端都生成UDP Server。

Java中通过DatagramSocketDatagramPacket来实现UDP/IP+BIO方式和系统间通信。

  • DatagramSocket:负责监听端口和读写数据
  • DatagramPacket:作为数据流对象进行传输

由于UDP双端不建立连接,所以也就不存在竞争问题,只是最终读写流的动作是同步的。

4.1 代码

客户端和服务端都类似

//控制台输入信息(用于发送信息给目标)
Scanner scanner = new Scanner(System.in);
//监听端口8086
DatagramSocket socket = new DatagramSocket(8086);

new Thread(()->{
    try {
        //创建接收数据用的对象
        DatagramPacket receivePacket = new DatagramPacket(new byte[1024],1024);
        //接收信息
        socket.receive(receivePacket);
        System.out.println("接收的信息是:"+new String(receivePacket.getData(),0,receivePacket.getLength()));
    } catch (IOException e) {
        e.printStackTrace();
    }

}).start();

while(true){
    String content = scanner.nextLine();
    //创建发送数据用的对象,发送到端口为8087的服务端
    DatagramPacket sendPacket = new DatagramPacket(content.getBytes(),content.length(), InetAddress.getLocalHost(),8087);
    socket.send(sendPacket);
}

4.2 示例

启动两个UDP Server,分别监听端口:8086、8087。8086向8087发送信息,8087控制台打出接收到的信息。
在这里插入图片描述

五、UDP/IP+NIO

通过DatagramClannelByteBuffer来实现UDP/IP方式的系统间通信。

  • DatagramClannel:负责监听端口及进行读写
  • ByteBuffer:用于数据传输

客户端和服务端双方通信的方法都类似

5.1 发送信息

public  void send(){
    Scanner scanner= new Scanner(System.in);
    //创建监听通道
    DatagramChannel sendChannel=null;
    //创建选择器
    Selector selector = null;

    try {
        //开启监听通道
        sendChannel = DatagramChannel.open();
        //设置非阻塞
        sendChannel.configureBlocking(false);
        //设置监听地址
        SocketAddress target = new InetSocketAddress("127.0.0.1",8086);
        //通道监听地址
        sendChannel.connect(target);
        //开启选择器
        selector = Selector.open();
        //注册选择器
        sendChannel.register(selector, SelectionKey.OP_WRITE);
        while(true){
            if(selector.select()==0){
               continue;
            }
            Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();
            while(keyIter.hasNext()){
                SelectionKey key = keyIter.next();
                keyIter.remove();
                //事件为可写事件时
                if(key.isWritable()){
                    ByteBuffer buffer= ByteBuffer.wrap(scanner.nextLine().getBytes());
                    DatagramChannel channel = (DatagramChannel) key.channel();
                    //像通道写入数据
                    channel.write(buffer);
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关闭通道
        try {
            sendChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.2 接收信息

public static void receive(){
     //创建选择监听通道
     DatagramChannel receiveChannel = null;
     //创建选择器
     Selector selector = null;

     try {
         //开启监听通道
         receiveChannel = DatagramChannel.open();
         //设置非阻塞
         receiveChannel.configureBlocking(false);
         //开启监听端口8086
         DatagramSocket socket=receiveChannel.socket();
         socket.bind(new InetSocketAddress(8086));
         //开启选择器
         selector=Selector.open();
         //注册选择器
         receiveChannel.register(selector, SelectionKey.OP_READ);
         while(true){
             if(selector.select()==0){
                 continue;
             }
             Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();
             while(keyIter.hasNext()){
                 SelectionKey key = keyIter.next();
                 if(key.isReadable()){
                     DatagramChannel channel = (DatagramChannel) key.channel();
                     ByteBuffer buffer= ByteBuffer.allocate(2000);
                     channel.receive(buffer);//注意UDP/IP+NIO这个是receive
                     String msg=new String(buffer.array());
                     System.out.println("收到信息:"+msg);
                 }
             }
         }
     } catch (IOException e) {
         e.printStackTrace();
     } finally {
         try {
             //关闭通道
             receiveChannel.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值