放进NIO体系进行网络编程的工作流程:
Selector的创建
通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();
向Selector注册通道
通过Channel.register()方法来实现,
注意:Channel和Selector一起使用时,Channel必须处于非阻塞模式下。
channel.configureBlocking(false); //设置通道为非阻塞模式
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
register()方法的第二个参数:是一个“兴趣(interest)集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。
可以监听四种不同类型的事件:
- Connect 链接就绪,某个channel成功连接到另一个服务器称为“连接就绪”。
- Accept 接收就绪,一个server socket channel准备好接收新进入的连接称为“接收就绪”。
- Read 读就绪,一个有数据可读的通道可以说是“读就绪”。
- Write 写就绪,等待写数据的通道可以说是“写就绪”。
这四种事件用SelectionKey的四个常量来表示:
1.SelectionKey.OP_CONNECT
2.SelectionKey.OP_ACCEPT
3.SelectionKey.OP_READ
4.SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey说明
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。
这个对象包含了一些有用的属性:
1.interest集合
2.ready集合
3.Channel
4.Selector
5.附加的对象(可选)
interest集合
interest集合是你所选择的感兴趣的事件集合。
可以通过SelectionKey读写interest集合,像这样:
SelectionKey selectionKey=channel.register(selector, SelectionKey.OP_xxxx);
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以看到,用“和”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
ready集合
ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。
可以这样访问ready集合:
int readySet = selectionKey.readyOps();
可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
另外
从SelectionKey访问Channel和Selector很简单。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加的对象
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。
例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
选择器的select()
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。
这些方法返回你所感兴趣的事件(如连接、接受、读或写)同时这些事件已经准备就绪的那些通道。
换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
下面是select()方法:
1.select()阻塞到至少有一个通道在你注册的事件上就绪了。
2.select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数),一般用这个,不能无限阻塞。
3.selectNow()不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。
如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。
select()方法有返回值,返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。
如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,
如果另一个通道就绪了,它会再次返回1。
如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,
但在每次select()方法调用之间,只有一个通道就绪了。
选择器的selectedKeys()
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法获取到:
Set selectedKeys = selector.selectedKeys();
当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey对象。
这个对象代表了注册到该Selector的通道。
可以通过SelectionKey的selectedKeySet()方法访问这些对象。
可以遍历这个已选择的键集合来访问就绪的通道。如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// 一个连接被ServerSocketChannel接受
} else if (key.isConnectable()) {
// 与远程服务器建立了连接
} else if (key.isReadable()) {
// 一个channel做好了读准备
} else if (key.isWritable()) {
// 一个channel做好了写准备
}
keyIterator.remove();
}
这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。
注意每次迭代末尾的keyIterator.remove()一定要调用
,Selector不会自己从已选择键集中移除SelectionKey实例。
必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。若不移除,下次还是读取原来的数据,这是要命的。
SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。
选择器的wakeUp()
某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。
只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。
阻塞在select()方法上的线程会立马返回。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,上一个调用select()方法的阻塞线程会立即醒来(wake up)。
选择器的close()
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。
通道本身并不会关闭。
客户端与服务端简单交互实例
下面的程序涉及到一些网络编程的知识:
- 服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接
- 客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接
- 服务器接受到客户端的连接受,再生成一个Socket或者SocketChannel与此客户端通信
服务器:
/*
* nio网络编程实例,这是服务端
* 1.建立ServerSocketChannel通道,设置非阻塞 ,并绑定一个地址,客户链接到这个地址,等待客户端的链接
* 2.获取选择器的实例,注册通道到里面去,设置感兴趣的事件,ServerSocketChannel这种通道只有一个事件--链接事件
* 3.准备缓冲区:两个,一个读,一个写
* 4.实现与客户端交互,首先一个死循环,不停的获取各种客户端链接请求通道,轮询作用似的
* a.选择器的select(1000)方法,会获取各通道对象个数(通道感兴趣事件的)1000是表示阻塞一秒,如果一秒之内没结果,重新轮询
b.有通道就绪,则拿到通道对象键值集合Set,遍历集合,找到接入链接的通道,让其接入,返回一个能读写的通道,不是上面的接入通道
c.读写通道也要注册到选择器里面,先读事件,同一个通道的多次注册,如是不同的事件改变,只是注删了一个通道
*/
public class ServerDemo {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1",8000));
serverSocketChannel.configureBlocking(false);
Selector selector=Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //接入事件就绪
ByteBuffer readBuffer= ByteBuffer.allocate(1024);
ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
writeBuffer.put("this is server".getBytes()); //写缓冲区中写入这个字符串
writeBuffer.flip(); //翻转非直接缓冲区,非直接缓冲区,postion指针不能自动指向开缓冲区开头处
while(true) {
int nReady=selector.select(1000); //捕获所有通道
if(nReady==0) continue; //一个通道都没有,一秒后继续查询
Set<SelectionKey> keys =selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key=iterator.next();
iterator.remove(); //手动移除,让下次轮询通道是全新的,而不是这次的
if(key.isAcceptable()) { //如果是准备链接的通道,就让客户端链接, 当然了,这个程序简单只有一个通道
SocketChannel channel=serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ); //不用担心,下次读客户发送的信息,因为有死循在那不的轮询
}else if(key.isReadable()) { //如果通道对象key是有可读事件,就读到缓冲区中
SocketChannel channel = (SocketChannel) key.channel();
readBuffer.clear();
channel.read(readBuffer);
readBuffer.flip();
/*while(readBuffer.hasRemaining()) { //判断缓冲区还没有数据,有的话,从缓冲区读数据,打印到控制台
System.out.print("读到的数据是:"+(char)readBuffer.get()); //读得是字节码,中文会乱码,一次读一个字节
}*/
System.out.println("服务器端收到的数据,直接打印:"+ new String(readBuffer.array()));
key.interestOps(SelectionKey.OP_WRITE);
}else if(key.isWritable()){
SocketChannel channel =(SocketChannel)key.channel();
writeBuffer.rewind();
channel.write(writeBuffer);
key.interestOps(SelectionKey.OP_READ); //以便下次再读
}
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
客户端
/*
* nio网络编程实例,这是客户端
* 不用实例化,链接通道(类似洒店的迎宾)
* 1.通道
* 2.缓冲区,两个,一个读,一个写
* 3.先向服务器写,再读数据
*
*/
public class ClientDemo {
public static void main(String[] args) {
try {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1",8000)); //链接到服务器
ByteBuffer readBuffer= ByteBuffer.allocate(1024);
ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
writeBuffer.put("this is client".getBytes()); //写缓冲区中写入这个字符串
writeBuffer.flip();
while(true) {
writeBuffer.rewind(); //重置缓冲区,写入
channel.write(writeBuffer);
readBuffer.clear();
channel.read(readBuffer);
readBuffer.flip();
System.out.println("客户端收到的数据:"+new String(readBuffer.array()));
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
字符集和Charset
基础认识
计算机中储存的信息都是用二进制数表示的;
而我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果。
通俗的说,按照何种规则将字符存储在计算机中,如’a’用什么表示,称为"编码";
反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密。
在解码过程中,如果使用了错误的解码规则,则导致’a’解析成’b’或者乱码。
字符集(Charset)
:多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,字符和二进制数字的对应规则不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GBK字符集、UTF-8字符集等。
字符编码(Character Encoding)
:是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),
与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。
而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。
元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。
常用字符集和字符编码
- ASCII字符集&编码
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语。 - GBXXXX字符集&编码
计算机发明之处及后面很长一段时间,只用应用于美国及西方一些发达国家,ASCII能够很好满足用户的需求。
但是当天朝也有了计算机之后,为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。
规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,
前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,
这样我们就可以组合出大约7000多个简体汉字了。
上面的编码规则形成的字符集就是GB2312。
GB2312或GB2312-80是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》
由于GB 2312-80只收录6763个汉字,有不少汉字并未有收录在内,如部分在GB 2312-80推出以后才简化的汉字(如"啰"),部分人名用字(如中国前总理朱镕基的"镕"字),台湾及香港使用的繁体字,日语及朝鲜语汉字等。因此1995年发布了GBK编码,是在GB2312-80标准基础上的进行了扩展,共收录了21003个汉字 - BIG5字符集&编码
Big5,又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准。 - Unicode
像天朝一样,当计算机传到世界各个国家时,为了适合当地语言和字符,设计和实现类似GB2312/GBK/BIG5的编码方案。这样各搞一套,在本地使用没有问题,一旦出现在网络中,由于不兼容,互相访问就出现了乱码现象。
为了解决这个问题,产生了——Unicode。Unicode编码系统为表达任意语言的任意字符而设计。
它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。
在计算机科学领域中,Unicode(统一码、万国码、单一码、标准万国码)是业界的一种标准,它可以使电脑得以体现世界上数十种文字的系统。
Unicode是标准,是规则,
UTF-32/ UTF-16/ UTF-8是三种字符编码方案就是Unicode的具体实现。
UTF-8:电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码方案,也是用的最多的.
java的字符集
Java默认使用Unicode字符集,但很多系统并不使用Unicode字符集,那么当从系统中读取数据到Java程序时,就可能出现乱码的问题。
JDK1.4提供了Charset来处理字节序列和字符序列(字符串)之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset类是不可变的。
Charset类提供了一个静态方法availableCharset()来获取当前JDK所支持的所有字符集。返回的是一个SortedMap<String,Charset>
例:查看JDK支持的所有字符集
public class CharsetTest{
public static void main(String[] args) {
SortedMap<String,Charset> map = Charset.availableCharsets();
for(String s:map.keySet()) {
System.out.println(s);
}
}
}
-------------------------------------------------------------------------
Big5
Big5-HKSCS
CESU-8
EUC-JP
EUC-KR
GB18030
GB2312
GBK
....
UTF-8
...
创建字符集对象与获取相应的编码解码对象
一旦知道字符集的别名后,就可以用Charset的forName()方法来创建对应的Charset对象,
forName方法的参数就是字符集的别名。
拿到Charset对象,就可以调用它的newEncoder和newDecoder方法,创建对应的编码器和解码器对象。
解码器对象CharsetDecoder里的decode()方法就可以将ByteBuffer转为CharBuffer,CharsetEncoder里的encode()方法作用相反,将CharBuffer转为ByteBuffer。
编码与解码缓冲区
接下来使用编码器将CharBuffer中的字符序列转换为字节序列ByteBuffer。
CharBuffer和ByteBuffer是java NIO中的IO操作类。
编码器,编码字符缓冲区后,转换为字节序列缓冲区
解码器,解码字节缓冲区后,转换为字符序列缓冲区。
编码
public class CharsetTest{
public static void main(String[] args) throws CharacterCodingException {
Charset charset = Charset.forName("GBK");
CharsetEncoder encoder = charset.newEncoder();
CharBuffer charBuffer = CharBuffer.allocate(20);
charBuffer.put("熊少文");
charBuffer.flip();
ByteBuffer byteBuffer=encoder.encode(charBuffer);
for(int i=0;i<byteBuffer.limit();i++) {
System.out.println(byteBuffer.get(i)+"");
}
}
}
-------------------------------------------------------
-48
-36
-55
-39
-50
-60
解码
public class CharsetTest{
public static void main(String[] args) throws Exception {
Charset charset = Charset.forName("GBK");
CharsetEncoder encoder = charset.newEncoder();
CharBuffer charBuffer = CharBuffer.allocate(20);
charBuffer.put("熊少文");
charBuffer.flip();
ByteBuffer byteBuffer=encoder.encode(charBuffer);
for(int i=0;i<byteBuffer.limit();i++) {
System.out.println(byteBuffer.get(i)+"");
}
CharsetDecoder decoder=charset.newDecoder();
//byteBuffer.flip(); //解码器不用翻转,
CharBuffer charBuffer2 = decoder.decode(byteBuffer);
//charBuffer2.flip(); //也不用翻转
for(int j=0;j<charBuffer2.limit();j++) {
System.out.print(charBuffer2.get(j));
}
}
}
-----------------------------------------------------------------
-48
-36
-55
-39
-50
-60
熊少文
NIO2.0即AIO
JDK7在java.nio这个工具包里加入了很多新的元素
我们把这些改变叫NIO2.0(即AIO,Asynchronous IO:异步非阻塞IO)和NIO相比,同样有Channel和Buffer, 但没有Selector。
Path雷同于File路径
Java Path接口是Java NIO 2.0更新的一部分,同Java NIO一起已经包括在Java6和Java7中。Java Path接口是在Java7中添加到java.nio的。
Path接口位于java.nio.file包中,所以Path接口的完全限定名称为java.nio.file.Path。
Java Path实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。
在许多方面,java.nio.file.Path接口类似于java.io.File类,但是有一些细微的差别。不过,在许多情况下,您可以使用Path接口来替换File类的使用。
创建一个Path寮例
为了使用java.nio.file.Path实例必须创建一个Path实例。
您可以使用Paths类(java.nio.file.Paths)中的静态方法来创建路径实例,名为Paths.get()。
public class PathTest {
public static void main(String[] args) {
//您可以使用Paths类(java.nio.file.Paths)中的静态方法来创建路径实例,名为Paths.get()。
Path path=Paths.get("c:\\xiong\\aaa.txt");//相对路径写法与File类一样
Path path2=Paths.get("c:\\xiong","aaa.txt");
System.out.println(path+" "+path2);
Path path3 = Paths.get(".","aaa.txt"); //当前目录下
Path path4= Paths.get("..","aaa.txt"); //上一级目录下
System.out.println(path3+" "+path4);
}
}
------------------------------------------------------------
c:\xiong\aaa.txt c:\xiong\aaa.txt
.\aaa.txt ..\aaa.txt
在Unix系统(Linux、MacOS、FreeBSD等)上,上面的绝对路径可能如下:
Path path = Paths.get("/home/jakobjenkov/myfile.txt");
Java NIO Files
Java NIO Files类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法。
java.nio.file.Files类与java.nio.file.Path实例一起工作。
Files.exists()
Files.exists()方法检查给定的Path在文件系统中是否存在。
可以创建在文件系统中不存在的Path实例。
public class FilesTest {
public static void main(String[] args) {
Path path=Paths.get("c:\\xiong\\aaa.txt");
boolean exist = Files.exists(path, new LinkOption[] {LinkOption.NOFOLLOW_LINKS});
System.out.println(exist);
}
}
------------------------------------------------------------------
true
Files.createDirectory()
Files.createDirectory()方法,用于根据Path实例创建一个新目录,下面是一个Files.createDirectory()例子:
public class FilesTest {
public static void main(String[] args) {
Path path=Paths.get("c:\\bbb\\");
if(!Files.exists(path, new LinkOption[] {LinkOption.NOFOLLOW_LINKS})) {
try {
Files.createDirectory(path);
}catch(IOException e) {
e.printStackTrace();
}
}
}
}
第一行创建表示要创建的目录的Path实例。
在try-catch块中,用路径作为参数调用Files.createDirectory()方法。
如果创建目录成功,将返回一个Path实例,该实例指向新创建的路径。
如果该目录已经存在,则是抛出一个java.nio.file.FileAlreadyExistsException。
如果出现其他错误,可能会抛出IOException。
例如,如果想要的新目录的父目录不存在,则可能会抛出IOException。
Files.copy()
Files.copy()方法从一个路径拷贝一个文件到另外一个目录:这个方法在File中没有。当需要复制文件时,用这个是十分方便的。
Path spath = Paths.get("c:\\xiong\\aaa.txt");
Path dpath = Paths.get("c:\\xiong\\aaa.txt");
try{
Files.copy(spath,dpath);
//Files.copy(spath,dpath,StandardCopyOption.REPLACE_EXISTING); //覆盖复制
}catch(IOException e){
e.printStackTrace();
}
Files.move()
Java NIO Files还包含一个函数,用于将文件从一个路径移动到另一个路径。
移动文件与重命名相同,但是移动文件既可以移动到不同的目录,也可以在相同的操作中更改它的名称。
java.io.File类也可以使用它的renameTo()方法来完成这个操作,但是现在已经在java.nio.file.Files中有了文件移动功能。
Path sourcePath = Paths.get("data/logging-copy.properties");
Path destinationPath = Paths.get("data/subdir/logging-moved.properties");
try {
Files.move(sourcePath, destinationPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
//移动文件失败
e.printStackTrace();
}
Files.delete()
Files.delete()方法可以删除一个文件或者目录。
Path path = Paths.get("data/subdir/logging-moved.properties");
try {
Files.delete(path);
} catch (IOException e) {
// 删除文件失败
e.printStackTrace();
}
首先,创建指向要删除的文件的Path。然后调用Files.delete()方法。
如果Files.delete()由于某种原因不能删除文件(例如,文件或目录不存在),会抛出一个IOException。