Java NIO 网络编程学习小结(2)

Java NIO 网络编程学习小结(2)

附上笔者学习的慕课课程链接:

解锁网络编程之NIO的前世今生-慕课网 (imooc.com)

上述课程中项目代码如下(笔者进行了注释和一些改动):

本地实现聊天室程序

服务器端:

package TalkingHouse;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * @author 作者:Ali
 * 描述:   客户端的线程类,专门用于接收服务器端相应信息
 */
public class NioServer {
    //start:启动方法
    public void start() throws Exception{
        //创建selector
        Selector selector=Selector.open();
        //通过ServerSocketChannel创建channel通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        //为channel通道绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8000));
        //设置channel为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //将channel注册到selector上,监听连接事件,接受连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动成功!");
        //循环等待新接入的连接
        while(true){
            //获取可用的channel数量
            int readyChannel=selector.select();
            //TODO 为什么要加这句话 --> 防止selector的空轮询
            if(readyChannel==0) continue;
            //获取可用的channel集合
            Set<SelectionKey> selectionKeys=selector.selectedKeys();
            Iterator iterator=selectionKeys.iterator();
            while(iterator.hasNext()){
                //取出selectionKey的实例
                SelectionKey selectionKey=(SelectionKey) iterator.next();
                /**
                 * 移除set中的当前selectionKey
                 * 原因:selector 监听到一个channel就绪之后,将其加入单独的set集合,通过selectedKeys方法进行取集合;
                 *       在下一次检测到channel就绪事件之后,还会把之前的channel放入集合中,所以对selectionKey进行处理
                 *       的时候,就要先将其移出集合,避免set集合中的元素堆积过多。
                 */
                iterator.remove();
                //根据就绪状态,调用对应的方法处理业务
                //1.是接入事件
                if(selectionKey.isAcceptable()) {
                    acceptHandler(serverSocketChannel, selector);
                }
                //2.是可读事件
                if (selectionKey.isReadable()){
                    readHandler(selectionKey,selector);
                }
            }
        }
    }
    /**
     * 一个Channel只能被注册到Selector上一次,如果将Channel注册多次到Selector上,
     * 其实相当于是在更新。所以,如果对Channel感兴趣的事件没有变化,是不需要重新注册的。
     * 以下两事件处理方法中把再次注册的代码去掉也是不影响运行的
     * */
    //接入事件处理器
    private void acceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) throws Exception{
        //服务器接受到客户端的连接之后,再生成一个Socket或SocketChannel与此客户端通信
        SocketChannel socketChannel=serverSocketChannel.accept();
        socketChannel.configureBlocking(false);//设置非阻塞channel
        socketChannel.register(selector,SelectionKey.OP_READ);
        //回复客户端提示消息
        socketChannel.write(Charset.forName("UTF-8").encode("提示:网络仅为虚拟世界,请注意个人言行以及安全。"));
    }
    //可读事件处理器
    private  void readHandler(SelectionKey selectionKey,Selector selector) throws Exception{
        //从selectionKey中获取到已经就绪的channel
        SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
        //创建buffer
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //循环读取客户端请求信息
        String request="";
        while (socketChannel.read(byteBuffer) >0 ){
            //切换buffer为读模式
            byteBuffer.flip();
            //读取内容
            request+=Charset.forName("UTF-8").decode(byteBuffer);
        }
        //将channel再次注册到selector上,监听他的可读事件
        socketChannel.register(selector,SelectionKey.OP_READ);
        //将客户端发送的消息进行 广播到其他客户端
        if(request.length()>0){
            //TODO 广播信息到其他客户端
            broadCast(selector,socketChannel,request);
        }
    }

    /**
     * 广播给其他客户端:
     *  1.获取到已接入的客户端channel
     *  2.循环向所有channel广播信息
     */
    private void broadCast(Selector selector,SocketChannel sourceChannel,String request){
        Set<SelectionKey> selectionKeys=selector.keys();
        selectionKeys.forEach(
                selectionKey -> {
                    Channel targetChannel=selectionKey.channel();
                    //除去发送消息的客户端
                    if (targetChannel instanceof SocketChannel && targetChannel != sourceChannel){
                        try {
                            //发送信息到targetChannel客户端
                            ((SocketChannel)targetChannel).write(Charset.forName("UTF-8").encode(request));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
        );
    }

    public static void main(String[] args) throws Exception {
        new NioServer().start();
    }
}

客户端:

package TalkingHouse;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * @author 作者:Ali
 * 描述:   客户端的线程类,专门用于接收服务器端相应信息
 */
public class NioClient {
    /**
     * 启动客户端:新开线程用于接收服务器端的响应数据
     *            新开线程的代码应当置于向服务器端发送请求之前
     *            否则相应信息无法打印
     */
    public void start(String name) throws Exception{
        //连接服务器端
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
        /**
         * 接收服务器端响应
         * 新开一个线程、专门负责接收服务器端的响应数据
         * 主线程返回 继续处理下一个任务 新线程去处理服务器端的响应
         * 做到了非阻塞服务
         * selector、socketChannel设置非阻塞、注册、新开线程
         */
        Selector selector=Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        new Thread(new NioClientHandler(selector)).start();

        //向服务器端发出请求数据
        Scanner scanner=new Scanner(System.in);
        while (scanner.hasNextLine()){
            String request=scanner.nextLine();
            if (request!=null && request.length()>0){
                socketChannel.write(Charset.forName("UTF-8").encode(name+" : "+request));
            }
        }
    }
}

客户端新开线程处理服务器端响应:

package TalkingHouse;

import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * @author 作者:Ali
 * 描述:   客户端的线程类,专门用于接收服务器端相应信息
 */
public class NioClientHandler implements Runnable {
    private Selector selector;
    public NioClientHandler(Selector selector){
        this.selector=selector;
    }
    @Override
    public void run() {
        try {
            while (true) {
                int readyChannel = selector.select();
                if (readyChannel == 0) continue;
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = (SelectionKey) iterator.next();
                    iterator.remove();
                    if (selectionKey.isReadable()) {
                        readHandler(selectionKey, selector);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //可读事件处理器
    private  void readHandler(SelectionKey selectionKey,Selector selector) throws Exception{
        //从selectionKey中获取到已经就绪的channel
        SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
        //创建buffer
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        //循环读取服务器端响应信息
        String response="";
        while (socketChannel.read(byteBuffer) >0 ){
            //切换buffer为读模式
            byteBuffer.flip();
            //读取内容
            response+= Charset.forName("UTF-8").decode(byteBuffer);
        }
        //将channel再次注册到selector上,监听他的可读事件
        socketChannel.register(selector,SelectionKey.OP_READ);
        //将客户端发送的消息进行 广播到其他客户端  
        if(response.length()>0){
            System.out.println(response);
        }
    }
}

创建多个客户端:

public class AClient {
    public static void main(String[] args) throws Exception {
        new NioClient().start("Aclient");
    }
}
public class BClient {
    public static void main(String[] args) throws Exception {
        new NioClient().start("Bclient");
    }
}
public class CClient {
    public static void main(String[] args) throws Exception {
        new NioClient().start("Cclient");
    }
}

执行效果图如下:

首先启动服务器端,再启动多个客户端:

服务器将每个客户端输入的数据广播到其他所有客户端。

并且因为可以为不同客户端定义不同name属性,所以可以数据发出的客户端名称。




(本文用于学习分享)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值