NIO编程-网络编程(socket)

1.通过NIO实现网络编程

概述和核心API
前面在进行文件IO 时用到的FileChannel 并不支持非阻塞操作,学习NIO 主要就是进行
网络IO,Java NIO 中的网络通道是非阻塞IO 的实现,基于事件驱动,非常适用于服务器需
要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等....
在Java 中编写Socket 服务器,通常有以下几种模式:
 一个客户端连接用一个线程,优点:程序编写简单;缺点:如果连接非常多,分配的线
程也会非常多,服务器可能会因为资源耗尽而崩溃。
 把每一个客户端连接交给一个拥有固定数量线程的连接池,优点:程序编写相对简单,
可以处理大量的连接。确定:线程的开销非常大,连接如果非常多,排队现象会比较严

使用Java 的NIO,用非阻塞的IO 方式处理。这种模式可以用一个线程,处理大量的客
户端连接。
1. Selector(选择器),能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获
取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也
就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,
就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并
且避免了多线程之间的上下文切换导致的开销。

该类的常用方法如下所示:
 public static Selector open(),得到一个选择器对象
 public int select(long timeout),监控所有注册的通道,当其中有IO 操作可以进行时,将
对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
 public Set<SelectionKey> selectedKeys(),从内部集合中得到所有的SelectionKey
2. SelectionKey,代表了Selector 和网络通道的注册关系,一共四种:
 int OP_ACCEPT:有新的网络连接可以accept,值为16
 int OP_CONNECT:代表连接已经建立,值为8
 int OP_READ 和int OP_WRITE:代表了读、写操作,值为1 和4
该类的常用方法如下所示:
 public abstract Selector selector(),得到与之关联的Selector 对象
 public abstract SelectableChannel channel(),得到与之关联的通道
 public final Object attachment(),得到与之关联的共享数据
 public abstract SelectionKey interestOps(int ops),设置或改变监听事件
 public final boolean isAcceptable(),是否可以accept
 public final boolean isReadable(),是否可以读
 public final boolean isWritable(),是否可以写
3. ServerSocketChannel,用来在服务器端监听新的客户端Socket 连接,常用方法如下所示:
 public static ServerSocketChannel open(),得到一个ServerSocketChannel 通道
 public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号

 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
取值false 表示采用非阻塞模式
 public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
 public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
4. SocketChannel,网络IO 通道,具体负责进行读写操作。NIO 总是把缓冲区的数据写入通
道,或者把通道里的数据读到缓冲区。常用方法如下所示:
 public static SocketChannel open(),得到一个SocketChannel 通道
 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
取值false 表示采用非阻塞模式
 public boolean connect(SocketAddress remote),连接服务器
 public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成
连接操作
 public int write(ByteBuffer src),往通道里写数据
 public int read(ByteBuffer dst),从通道里读数据
 public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置
监听事件,最后一个参数可以设置共享数据
 public final void close(),关闭通道

示例代码:

1.服务端代码:

//聊天室服务端
public class ChatServer {

    private final int POST = 9999;  //服务端端口号
    private ServerSocketChannel serverSocketChannel; //服务端监听管道对象
    private Selector selector;  //选择器

    public ChatServer() {
        try {
            //1.创建监听管道对象
            serverSocketChannel = ServerSocketChannel.open();
            //2.设置非阻塞
            serverSocketChannel.configureBlocking(false);
            //绑定端口
            serverSocketChannel.bind(new InetSocketAddress(POST));
            //4.创建选择监控对象
            selector = Selector.open();
            //5.将监听管道对象注册到选择器中,并监听accept事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("----服务端已经就绪-----");

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

    }

    /**
     * 6开始干活
     * */
    public void start() throws Exception {
        //循环工作
        while (true) {
            //6.1通过selector查看是否有客户端连接
            if (selector.select(2000) == 0) {   //没有客户端连接服务端
                System.out.println("-----server:目前没有客户端链接到服务端-------");
                continue;
            }
            //6.2有客户端连接到服务端,获取所有的客户端连接SelectionKey
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                //通过key,可以获取到相关的三种事件
                if (key.isAcceptable()) {   //连接请求事件
                    //获取客户端的网络通道
                    SocketChannel channel = serverSocketChannel.accept();
                    //设置非阻塞
                    channel.configureBlocking(false);
                    //注册到selector选择器中,并注册为读事件
                    channel.register(selector,SelectionKey.OP_READ);
                    System.out.println("---服务端监听到客户端:"+channel.getRemoteAddress().toString().substring(1)+"上线了");
                }
                if (key.isValid() && key.isReadable()) { //如果监听到的是读事件
                    readMsg(key);
                }
                if (key.isValid() && key.isWritable()) { //如果监听到是写事件

                }
                //处理完要把当前的SelectionKey移除,防止反复处理
                keyIterator.remove();
            }

        }
    }

    /**
     * 读取客户端发送过来的数据,并广播出去
     * */
    private void readMsg(SelectionKey key) throws Exception {
        //1.通过key获取管道
        SocketChannel channel = (SocketChannel) key.channel();
        //2.获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //3,将接收到的数据保存到缓冲区中
        int write = 0;
        try {
            write = channel.read(buffer);
        } catch (IOException e) {
            key.cancel();
            channel.socket().close();
            //e.printStackTrace();
        }
        if (write > 0) {
            String msg = new String(buffer.array());
            printInfo(msg);
            //广播给所有的客户端,除了发送消息的客户端
            broadcast(channel, msg);
        }
    }

    /**
     * 广播所有的客户端
     * excet 消息的发送客户端
     * */
    private void broadcast(SocketChannel excet, String msg) throws Exception {
        System.out.println("---服务端开始广播----");
        //通过selector获取所有的注册的selectionkey
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key: keys) {
            Channel channel = key.channel();    //获取所有的对应管道,包括客户端管道还有其他的管道类型
            if (channel instanceof SocketChannel && channel != excet) { //如果该管道未客户端网络管道且不为发送消息的客户端网络管道,就执行一下的的代码
                //将管道强转为网络管道对象,方便调用api
                SocketChannel socketChannel = (SocketChannel) channel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                socketChannel.write(buffer);

            }
        }
    }

    /**
     *  控制台输出方法
     * @param str
     */
    private void printInfo(String str) { //往控制台打印消息
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }

    /*启动服务端*/
    public static void main(String[] args) {
        try {
            new ChatServer().start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.客户端代码:

//聊天室客户端
public class ChatClient {
    private final String HOST = "127.0.0.1";    //服务端地址
    private final int POST = 9999;   //socket 端口号
    private SocketChannel socketchannel;     //网络管道对象
    private String userName;        //聊天用户名
    private InetSocketAddress address;

    //构造方法
    public ChatClient() throws Exception {
        //1.初始化网络管道对象
        socketchannel = SocketChannel.open();
        //2.设置非阻塞
        socketchannel.configureBlocking(false);
        //3.连接到指定服务端
        address = new InetSocketAddress(HOST, POST);
        if (!socketchannel.connect(address)) {//如果没有连接上,则继续连接,直到连接成功
            while (!socketchannel.finishConnect()) {
                System.out.println("目前客户端还未连接到服务端。。。。。");
            }
        }
        //4.得到客户端IP地址和端口信息,作为聊天的用户信息
        userName = socketchannel.getLocalAddress().toString().substring(1);
        System.out.println("-------客户端:"+userName +"已经连接到服务端---------");
    }

    //向服务端发送信息
    public void sendMsg(String msg) throws Exception{
        //如果客户端发送的信息为bye,则说明客户端离开群聊,关闭当前的客户端网络管道
        if (msg.equalsIgnoreCase("bye")) {
            socketchannel.close();  //离开群聊,结束连接
            return;
        }
        msg = "客户端:"+ userName + "说:"+ msg;
        //创建缓冲区对象
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        try {
            if (socketchannel.isOpen()) {
                socketchannel.write(buffer);
            }else {
                //如果当前的网络管道已经失效,需要重新新建连接
                socketchannel = SocketChannel.open();
                //2.设置非阻塞
                socketchannel.configureBlocking(false);
                //3.连接到指定服务端
                address = new InetSocketAddress(HOST, POST);
                if (!socketchannel.connect(address)) {//如果没有连接上,则继续连接,直到连接成功
                    while (!socketchannel.finishConnect()) {
                        System.out.println("目前客户端还未连接到服务端。。。。。");
                    }
                }
                //4.得到客户端IP地址和端口信息,作为聊天的用户信息
                userName = socketchannel.getLocalAddress().toString().substring(1);
                System.out.println("-------客户端:"+userName +"已经连接到服务端---------");
            }
        } catch (IOException e) {
            socketchannel.close();
            e.printStackTrace();
        }

    }

    //接收服务端发送过来的信息
    public void receiveMsg() throws Exception {
        //1.获取缓冲区对象,
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //2.从缓冲区读取数据
        int read = 0;
        try {
            read = socketchannel.read(buffer);
        } catch (IOException e) {
            socketchannel.socket().close();
            //e.printStackTrace();
        }
        if (read > 0) { //大于零,说明缓冲区有数据
            //3.输出数及
            System.out.println("-------服务器发送了:"+new String(buffer.array()).trim());
        }

    }
}

3.开启客户端代码

public class ChatTest {

    public static void main(String[] args) throws Exception {
        //创建客户端对象
        ChatClient chatClient = new ChatClient();
        //开一个线程,专门接收服务端的广播消息
        new Thread() {
            public void run() {
                while (true) {
                    try {
                        chatClient.receiveMsg();
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //控制台模拟客户端输入消息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) { //如果有输入
            String msg = scanner.next();
            try {
                chatClient.sendMsg(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }


}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值