《Java网络编程》中chargenServer与chargenClient的NIO执行过程

《Java网络编程》中chargenServer与chargenClient的执行过程

1. chargenServer

 

package jnp4.nio.SocketIO;

import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.io.IOException;

/**
 * 选择器支持一个线程查询一组socket,找出哪些socket已经准备就绪可以读/写数据,然后顺序地处理这些准备好的socket。
 */
public class ChargenServer {

    public static int DEFAULT_PORT = 2036;

    public static void main(String[] args) {
        int port;
        try {
            port = Integer.parseInt(args[0]);
        } catch (RuntimeException ex) {
            port = DEFAULT_PORT;
        }
        System.out.println("Listening for connections on port " + port);


        byte[] rotation = new byte[95 * 2];
        //ASCII  文本行有74个ASCII字符长( 72个可打印字符,后面是回车/换行对)

        //此数据在初始化之后只用于读取,所以可以重用于多个通道
        // space对应32  ~对应126 可显示字符就是从32到126 一共是126-32+1=95个字符
        for (byte i = ' '; i <= '~'; i++) {
            rotation[i - ' '] = i;
            rotation[i - ' ' + 95] = i;
        }

        ServerSocketChannel serverSocketChannel;
        Selector selector;
        try {
            //调用静态工厂方法ServerSocketChannel . open ()创建一个新的ServerSocketChannel对象。
            serverSocketChannel = ServerSocketChannel.open();
            ServerSocket ss = serverSocketChannel.socket();
            /*开始时,这个通道并没有具体监听任何端口。
            要把它绑定到一个端口,可以用socket()方法获取其ServerSocket对等端( peer )对象,
            然后使用bind ()方法绑定到这个对等端*/
            InetSocketAddress address = new InetSocketAddress(port);
            ss.bind(address);
            /*你可能还希望ServerSocketζhannel也处于非阻塞模式。默认情况下,这个accept ()方邑
            会阻塞,直到有一个人站连接为止,这与ServerSocket的accept ()方法类似。为了改变
            这一点,只需在调用accept ()之前调用configureBlocking(false):*/
            serverSocketChannel.configureBlocking(false);


            /*可以创建一个Selector ,允许程序迭代处理所有准备好的连接。
            要构造一个新的Selector ,只需调用Selector.open ()静态工厂方法:*/
            selector = Selector.open();

            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException ex) {
            ex.printStackTrace();
            return;
        }

        /*为了检查是否有可操作的数据,可以调用选择器的select ()方法。对于长时间运行的服
        务器,这一般要放在一个无限循环中:*/
        while (true) {
            try {
                //选择一组keys,其相应的通道准备好进行I/O操作
                //该方法执行阻塞选择操作。
                //只有在至少选择一个通道之后,才会返回此选择器的唤醒方法,或者当前线程中断,以先到者为准。
                selector.select();
            } catch (IOException ex) {
                ex.printStackTrace();
                break;
            }

            /*假定选择器确实找到了一个就绪的通道,其selectedKeys()方法会返回一个java.util.Set,
            其中对应各个就绪通道分别包含一个SelectionKey对象。否则它会返回一个空集。
            在两种情况下,都可以通过一个java.util.Iterator循环处理*/

            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {

                SelectionKey key = iterator.next();
                /*Removes from the underlying collection the last element returned by this iterator (optional operation).
                This method can be called only once per call to next().*/

                /*通过从集合中删除键,这就告诉选择器这个键已经处理过,这样Selector就不需要在每次调用select ()时再将这个键返回给我们了。
                再次调用select ()时,如果这个通道再次就绪, Selector就会把该通道再增加到就绪集合中。*/

                iterator.remove();
                try {
                    //就绪的通道是服务器通道,程序就会接受一个新Socket通道,将其添加到选择器。
                    if (key.isAcceptable()) {
                        //返回创建此key的通道 这里是服务器socket通道
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();

                        /*服务器Socket通道现在在端口19监听入站连接。要接受连接,可以调用accept (),
                        它会返回一个SocketChannel对象:*/
                        SocketChannel clientSocketChannel = server.accept();
                        System.out.println("Accepted connection from " + clientSocketChannel);
                        //在服务器端,你肯定希望客户端通道处子非阻塞模式,以允许服务器处理多个并发连接
                        clientSocketChannel.configureBlocking(false);
                        //使用每个通道的register()方法向监视这个通道的选择器进行注册。
                        //在注册时,要使用SelectionKey类提供的命名常量指定所关注的操作。
                        SelectionKey clientSelectionKey = clientSocketChannel.register(selector, SelectionKey.
                                OP_WRITE);

                        //服务器SocketChannel建立缓冲器...
                        ByteBuffer buffer = ByteBuffer.allocate(74);
                        buffer.put(rotation, 0, 72);
                        buffer.put((byte) '\r');
                        buffer.put((byte) '\n');
                        //position = 1, limit = 74
                        buffer.flip();
                        //把buffer附加到通道的SelectionKey的attachment object中:
                        clientSelectionKey.attach(buffer);
                        //如果就绪的通道是客户端Socket通道,
                        //程序就会向客户端Socket通道写入之前附加到其clientSelectionKey中attachment成员变量中的数据。
                        //如果没有通道就绪,选择器就会等待。一个线程(主线程)可以同时处理多个连接。
                    } else if (key.isWritable()) {
                        /*向客户端SocketChannel写入数据很简单。首先获取键的附件,将它转换为ByteBuffer ,
                        调用has Remaining ()检查缓冲器中是否还剩余未写的数据。
                        如果有,就写入到客户端通道。
                        否则,用rotation数组中的下一行数据重新填充缓冲区,并写入到客户端通道。*/
                        //返回创建此key的通道 这里是客户端socket通道
                        SocketChannel clientSocketChannel = (SocketChannel) key.channel();
                        //获取键的附件,将它转换为ByteBuffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        //如果buffer没有内容了,已经排空了(即已经写到ClientSocketChannel了,position==limit
                        // 当下一次循环时,会进入到此if语句中)
                        if (!buffer.hasRemaining()) {
                            // position = 1
                            buffer.rewind();
                            // 确定最后一行从哪里开始

                            //以回车/换行作为行分隔符的72字符循环文本行(其中包含95个可打印ASCII字符)
                            //要写入客户端SocketChannel循环文本行

            /*要确定从哪里获取下一行数据,这个算法依赖于以ASCII字符顺序存储在rotation数组中的字符。
            buffer.get ()从缓冲区中读取第一个数据字节。这个数字要减去空格字符(32),因为空格是rotation数组中的第一个字符。
            由此可以知道缓冲区当前从数组的哪个索引开始。要加l来得到下一行的开始索引,并重新填充缓冲区。*/
                            // Get the old first character
                            int first = buffer.get();  // 执行完这条语句 position = 2
                            // reset position = 1
                            buffer.rewind();
                            // Find the new first characters position in rotation
                            int position = first - ' ' + 1;
                            // copy the data from rotation into the buffer
                            //以用现有的子数组填充一个ByteBuffer
                            buffer.put(rotation, position, 72);
                            // Store a line break at the end of the buffer
                            buffer.put((byte) '\r');
                            buffer.put((byte) '\n');
                            // Prepare the buffer for writing
                            // limit = 74 position = 1
                            buffer.flip();
                        }
                        //将缓冲区内容写到ClientSocketChannel
                        clientSocketChannel.write(buffer);
                    }
                    /*在chargen协议中,服务器永远不会关闭连接。它等待客户端中断Socket。
                    当Socket中断时,会抛出一个异常。取消这个键,并关闭对应的通道:*/
                } catch (IOException ex) {
                    key.cancel();
                    try {
                        key.channel().close();
                    } catch (IOException cex) {
                    }
                }
            }
        }
    }
}

 

2. chargenClient

 

package jnp4.nio.SocketIO;

import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.io.IOException;

public class ChargenClient {

    public static int DEFAULT_PORT = 19;

    /**
     * 将服务器所发送连续的字符序列显示到system.out上
     *
     * 只有当你希望客户端
     有更多的功能时,即除了将所有输入复制到输出之外还要做其他一些工作,才会真正
     体现出新特性。
     *
     * @param args
     */
    public static void main(String[] args) {

        int port = Integer.parseInt("2036");

        try {
            /*要调用静态工厂方法SocketChannel.open()来创建一个新的java.nio.channels.SocketChannel对象。
            这个方法的参数是一个java.net.SocketAddress对象,指示要连接的主机和端口*/
            SocketAddress address = new InetSocketAddress("localhost", port);
            SocketChannel clientChannel = SocketChannel.open(address);

            //通道以阻塞模式打开

            /*利用通道,你可以直接写入通道本身。不是写入字节数组,而是要写入ByteBuffer对象。
            你已经很清楚,文本行有74个ASCil字符长( 72个可打印字符,后面是回车/换行对),
            所以要使用静态方法allocate ()创建一个容量为74字节的ByteBuffer*/
            ByteBuffer buffer = ByteBuffer.allocate(74);

            //利用Channels工具类(确切地讲是该工具类的newChannel ()方法),
            // 将OutputStream将System.out封装在一个通道中:
            WritableByteChannel out = Channels.newChannel(System.out);

            //将这个ByteBuffer对象传递给通道的read()

            /*在非阻塞模式下,即使没有任何可用的数据, read()也会立即返回。
            这就允许程序在试图读取前做其他操作。它不必等待慢速的网络连接*/
            while (clientChannel.read(buffer) != -1) {
                //回绕( flip )缓冲区,使得输出通道会从所读取数据的开头而不是末尾开始写入
                buffer.flip();
                out.write(buffer);
                /*请空将把缓冲区重置回初始状态(这实际上有点过于简化。老数据仍然存在,还没有被覆
                盖,但很快就会被从掘读取的新数据覆盖)*/
                buffer.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

 

3. 交互过程

TCP是全双工的,双向传递

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值