《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是全双工的,双向传递