The NIO library was introduced with JDK 1.4. NIO was created to allow Java programmers to implement high-speed I/O without having to write custom native code. NIO moves the most time-consuming I/O activites ( namely, filling and draining buffers) back into the operating system, thus allowing for a great increase in speed. The most important distinction between the original I/O library and NIO is how data is packaged and transmitted. Original I/O deals with data in stream, whereas NIO deals with data in blocks
Part1 Buffer 和 Channel
Channel 和 Buffer 的基本使用
Buffer
和 Channel
是NIO中的重要对象,几乎所有的I/O操作都要用到这两个对象。Channel
意为通道,他的作用类似于流对象,所有发送和接受的数据都要通过Channel
。Buffer
实质上是一个容器对象。
所有从Channel
中读取的数据都读到了Buffer
里,所有写入Channel
的数据必须首先存放在Buffer
里。
例如:
/*从文件中读取
*1)从 FileInputStream得到 channel
*2)生成 Buffer 的对象
*3)从Channel中读入Buffer
*/
FileInputStream fin = new FileInputStream("read.txt");
FileChannel fc = fin.getChannel();
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
fc.read(buffer) //所有从Channel中读取的(比如:文件中的)数据都读到了Buffer里
/*向文件中读入
*1)从 FileOutputStream得到 channel
*2)生成 Buffer 的对象,填充Buffer对象
*3)从Buffer中读入Channel
*/
FileOutputStream fout = new FileOutputStream("read.txt");
FileChannel fc = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for(int i = 0; i < message.length; i++){
buffer.put(message[i]);
}
buffer.flip(); //flip() 方法,用于在将数据填入buffer和数据取出buffer之间的切换
fc.write(buffer) //所有从Channel中写入(比如:文件中)的数据都写到了Buffer里
Buffer中的状态量
在上一段代码中,我们看到在Buffer
切换状态时用到了filp()
方法,事实上Buffer
还有其他用于切换状态的函数,其中最常用的是filp()
和clear()
。
这两个方法通常的用法如下:
buffer.clear(); //在buffer填入数据之前调用
int r = fcin.read(buffer);
if(r == -1){
break;
}
buffer.flip();//在buffer填入数据之后,取出数据之前调用
fcout.write(buffer);
为什么会出现吧这样的情况呢?
这是因为Buffer
中有几个状态量,position
、limit
和 capacity
。这在对Buffer
进行操作时,这三个状态量不停变化,从而决定Buffer
的可操作范围。
//TODO 关于buffer的三个状态量的解释
//TODO advanced JNIO
Part2 网络编程中用到的Channel
译自:Java NIO指南
DatagramChannel
操作1:打开DatagramChannel
用open()
/*
*在这个例子中,我们打开了一个 DatagramChannel,并且可
*以从 9999 端口接收UDP数据报。
*/
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind( new InetSocketAddress(9999));
操作2:接收数据 receive()
/*
*receive()方法会将收到的packet中的数据拷贝到Buffer中
*如果Buffer不足以容纳接收到的数据,超出容纳空间的数据
*将被静默地丢弃
*/
ByteBuffer buf = ByteBuffer.allocate( 1024 );
buf.clear();
channel.recrive(buf);
操作3:发送数据send()
/*
*这个例子中我们向“jenkov.com”的80端口发送了一个字符串
*但是,我们得不到发送的数据被接受或者没有被接收的反馈
*因为UDP不对数据送达作出任何保证。
*/
String newData = "New String to write to file..."
+System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate( 1024 );
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf,
new InetSocketAddress("jenkov.com", 80));
操作4:链接到特定地址connect()
/*
*这个例子中的connect()不同于TCP中的链接,这个例子中的
*connect()并不建立真的链接,而是将channel有特定的地址
*绑定,但仍然不保证数据一定被送达。
*/
channel.connect(new InetSocketAddress("jenkov.com", 80));
//链接到特定地址后可以直接使用channel的read()和write()方法
int bytesRead = channel.read(buf);
int bytesWrriten = channel.write(buf);
SocketChannel
操作1:打开SocketChnnel
SocketChannel socketChannel = socketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenvok.com", 80));
操作2:关闭SocketChannel
socketChannel.close();
操作3:从SocketChannel
中读取数据
ByteBuffer buf = ByteBuffer.allocate( 1024 );
//返回值表示buf接受了多少字节的数据,-1表示链接断开
int bytesRead = socketChannel.read(buf);
操作4:向SocketChannel
中写入数据
String newData = "New String to write to file..."
+System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate( 1024 );
buf.clear();
buf.put(newData.getBytes());
buf.flip();
//注意:SocketChannel.write()在while循环内调用,因为不能
//保证write()方法会向channel中写入多少比特,因此我们反复
//调用write()方法,知道buf的内容全部被写入
while(buf.hasRemaining()){
channel.write(buf);
}
操作5:非阻塞模式下的connect()
、write()
和 read()
方法
connect()
方法:
如果 SocketChannel
是非阻塞的,当我们调用connect()
并返回时,链接可能还没有建立。因此,我们需要调用finishConnect()
方法进行检测。
socketChannel.configureBlocking(false);
socketChannel.connect(
new InetSocketAddress("http://jenlov.com",80));
while(! socketChannel.finishConnectt()){
//wait, or do something else...
}
write()
方法:
不需要特殊变化,因为write()
方法已经在循环中调用了。
read()
方法:
需要注意这个方法的返回值,因为返回值表示实际实际读到多少数据。
ServerSocketChannel
操作1:打开SeverSocketChannel
ServerScoketChannel serverSocketChannel = ServerSocketChannel.open();
操作2:关闭ServerSocketChannel
serverSocketChannel.close();
操作3:监听到来的连接
/*
*由于要监听多个链接,所以将accept()放在while循环之中当然在实际
*编程中会用其他条件替换while循环中的true
*/
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel
}
操作4:非阻塞模式
/*
*当没有连接到来时,accept()返回null
*/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket.bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocektChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
Part3 对象的序列化
为了硬盘存储和网络传输的需要,我们需要对Java对象序列化(Object->bytes);相反,在需要使用Java对象时,我们需要进行反序列化(bytes->Object)。
serialVersionUID的概念
serialVersionUID是在对象反序列化是用来确保版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现异常。
serialVersionUID的类型必须是如下形式:
ANY-ACCESS-MODIFIER static final long
序列化过程
需要进行序列化的对象必须实现Serializable接口。这是一个 maker interface,也就是一个空的接口。它的源码如下:
public interface Serializable{
}
反序列化机制
其他情况
- 如果需要序列化的对象中有其他对象的引用,其他对象也必须实现Serializable接口。
- TODO
- TODO
- TODO
- TODO
- TODO
- TODO
Part4 网络和异步I/O
异步I/O是一种非阻塞的I/O。在调用异步I/O之后,通过对关心的事件进行注册,当这些事件(如 :有可以读取的数据到达、一个新的socket连接,等)发生时会得到系统通知。异步I/O/的优势在于,可以监听任意数量的Channels的I/O事件,而不用使用轮询或者创建额外的线程。
Selectors
Sector
是异步I/O的核心组件。我们在Selector
上注册感兴趣的I/O事件,在这些事件发生的时候 Selector
就会通知我们。
操作1:打开Selector
Selector selector = Selector.open();
操作2:用非阻塞方式打开ServerSocketChannel
为了接收连接,我们需要ServerSockeChanne
。为了保证异步I/O操作,ServerSocketChannel
要以非阻塞方式打开。
SeverSocketChannel ssc = SeverSocketChannel.open();
ssc.configureBlocking( false );
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( port[i] );
ss.bind( address );
Selection Keys
操作3:注册ServerSocketChannel
到Selector
使用ServerSocketChannel.register()
方法。这个方法的第一个参数是Selector
,第二个参数是要监测的事件(如:accept)。当有监测的事件发生时,Selector
将它添加到SelectionKey
中。
SelectionKey key = ssc.register( selector, SelectionKey.PO_ACCEPT);
操作4:监听事件
//阻塞直到至少有一个事件发生
int num = selector.select();
Set selectedKeys = selector.seectedKeys();
for(SelectionKye key: selectedKeys){
//deal with I/O event
}
操作5:检查发生的事件的类型
//确认事件是accept
if( ( key.readOps() ) & SelectionKey.OP_ACCEPT == SelectionKey.OP_ACCEPT){
//Accept the new connection
//...
}
操作6:接受一个新连接
上一步,我们已经确认youyige连接在等待,ServerSocket
接受,所以我们可以安全的接受这个连接而不用担心accept()
方法阻塞。
ServerSocketChannel ssc = key.channel();
SocketChannel sc = ssc.accept();
操作7:注册新接收的ScoketChannel
sc.configureBloking( false );
SelectionKey newkey = sc.register( selector, Selection.OP_READ);
操作8:删除处理过的key
selectedKeys.remove(key);
操作9:接受到来的数据
if( (key.readyOps() & Selectionkey.OP_READ) == SelectionKey.OP_READ){
SocketChannel sc = (SocketChannel)key.channel();
}
Part5 并发
在并发编程中有两个基本的执行单元:进程和线程。在Java中,并发编程主要指多线程并发。
Thread对象
**两种定义方法
- 实现Runnable接口
- 继承Thread类
public class HelloRunnable implements Runnable{
public void run(){
System.out.println("Hello from a thread!");
}
public static void main(String args[]){
(new Thread(new HelloRunnable())).start();
}
}
public class HelloThread exends Thread{
public void run(){
System.out.println("Hello from a thread!");
}
public static void main(String args[]){
(new HelloThread()).start;
}
}
//待续