Selector和Channel
在讲其他几个Channel之前,我们看一个和下面几个channel相关的Selector:
这里要介绍一个新的Channel类型叫做SelectableChannel,之前的FileChannel的连接是一对一的,也就是说一个channel要对应一个处理的线程。而SelectableChannel则是一对多的,也就是说一个处理线程可以通过Selector来对应处理多个channel。
SelectableChannel通过注册不同的SelectionKey,实现对多个Channel的监听。后面我们会具体的讲解Selector的使用,敬请期待。
DatagramChannel
DatagramChannel是用来处理UDP的Channel。它自带了Open方法来创建实例。
来看看DatagramChannel的定义:
public abstract class DatagramChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
ByteChannel表示它同时是ReadableByteChannel也是WritableByteChannel,可以同时写入和读取。
MulticastChannel代表的是一种多播协议。正好和UDP对应。
SocketChannel
SocketChannel是用来处理TCP的channel。它也是通过Open方法来创建的。
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
SocketChannel跟DatagramChannel的唯一不同之处就是实现的是NetworkChannel借口。
NetworkChannel提供了一些network socket的操作,比如绑定地址等。
ServerSocketChannel
ServerSocketChannel也是一个NetworkChannel,它主要用在服务器端的监听。
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel
AsynchronousSocketChannel
最后AsynchronousSocketChannel是一种异步的Channel:
public abstract class AsynchronousSocketChannel implements AsynchronousByteChannel, NetworkChannel
为什么是异步呢?我们看一个方法:
public abstract Future<Integer> read(ByteBuffer dst);
可以看到返回值是一个Future,所以read方法可以立刻返回,只在我们需要的时候从Future中取值即可。
使用Channel
小师妹:F师兄,讲了这么多种类的Channel,看得我眼花缭乱,能不能讲一个Channel的具体例子呢?
好的小师妹,我们现在讲一个使用Channel进行文件拷贝的例子,虽然Channel提供了transferTo的方法可以非常简单的进行拷贝,但是为了能够看清楚Channel的通用使用,我们选择一个更加常规的例子:
public void useChannelCopy() throws IOException { FileInputStream input = new FileInputStream ("src/main/resources/www.flydean.com"); FileOutputStream output = new FileOutputStream ("src/main/resources/www.flydean.com.txt"); try(ReadableByteChannel source = input.getChannel(); WritableByteChannel dest = output.getChannel()){ ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (source.read(buffer) != -1) { // flip buffer,准备写入 buffer.flip(); // 查看是否有更多的内容 while (buffer.hasRemaining()) { dest.write(buffer); } // clear buffer,供下一次使用 buffer.clear(); } } }
上面的例子中我们从InputStream中读取Buffer,然后写入到FileOutputStream。
总结
今天讲解了Channel的具体分类,和一个简单的例子,后面我们会再体验一下Channel的其他例子,敬请期待。
第十二章 MappedByteBuffer多大的文件我都装得下
简介
大大大,我要大!小师妹要读取的文件越来越大,该怎么帮帮她,让程序在性能和速度上面得到平衡呢?快来跟F师兄一起看看吧。
虚拟地址空间
小师妹:F师兄,你有没有发现,最近硬盘的价格真的是好便宜好便宜,1T的硬盘大概要500块,平均1M五毛钱。现在下个电影都1G起步,这是不是意味着我们买入了大数据时代?
没错,小师妹,硬件技术的进步也带来了软件技术的进步,两者相辅相成,缺一不可。
小师妹:F师兄,如果要是去读取G级的文件,有没有什么快捷简单的方法?
还记得上次我们讲的虚拟地址空间吗?
再把上次讲的图搬过来:
通常来说我们的应用程序调用系统的接口从磁盘空间获取Buffer数据,我们把自己的应用程序称之为用户空间,把系统的底层称之为系统空间。
传统的IO操作,是操作系统讲磁盘中的文件读入到系统空间里面,然后再拷贝到用户空间中,供用户使用。
这中间多了一个Buffer拷贝的过程,如果这个量够大的话,其实还是挺浪费时间的。
于是有人在想了,拷贝太麻烦太耗时了,我们单独划出一块内存区域,让系统空间和用户空间同时映射到同一块地址不就省略了拷贝的步骤吗?
这个被划出来的单独的内存区域叫做虚拟地址空间,而不同空间到虚拟地址的映射就叫做Buffer Map。 Java中是有一个专门的MappedByteBuffer来代表这种操作。
小师妹:F师兄,那这个虚拟地址空间和内存有什么区别呢?有了内存还要啥虚拟地址空间?
虚拟地址空间有两个好处。
第一个好处就是虚拟地址空间对于应用程序本身而言是独立的,从而保证了程序的互相隔离和程序中地址的确定性。比如说一个程序如果运行在虚拟地址空间中,那么它的空间地址是固定的,不管他运行多少次。如果直接使用内存地址,那么可能这次运行的时候内存地址可用,下次运行的时候内存地址不可用,就会导致潜在的程序出错。
第二个好处就是虚拟空间地址可以比真实的内存地址大,这个大其实是对内存的使用做了优化,比如说会把很少使用的内存写如磁盘,从而释放出更多的内存来做更有意义的事情,而之前存储到磁盘的数据,当真正需要的时候,再从磁盘中加载到内存中。
这样物理内存实际上可以看做虚拟空间地址的缓存。
详解MappedByteBuffer
小师妹:MappedByteBuffer听起来好神奇,怎么使用它呢?
我们先来看看MappedByteBuffer的定义:
public abstract class MappedByteBuffer extends ByteBuffer
它实际上是一个抽象类,具体的实现有两个:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
class DirectByteBufferR extends DirectByteBuffer implements DirectBuffer
分别是DirectByteBuffer和DirectByteBufferR。
小师妹:F师兄,这两个ByteBuffer有什么区别呢?这个R是什么意思?
R代表的是ReadOnly的意思,可能是因为本身是个类的名字就够长了,所以搞了个缩写。但是也不写个注解,让人看起来十分费解....
我们可以从RandomAccessFile的FilChannel中调用map方法获得它的实例。
我们看下map方法的定义:
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
MapMode代表的是映射的模式,position表示是map开始的地址,size表示是ByteBuffer的大小。
MapMode
小师妹:F师兄,文件有只读,读写两种模式,是不是MapMode也包含这两类?
对的,其实NIO中的MapMode除了这两个之外,还有一些其他很有趣的用法。
- FileChannel.MapMode.READ_ONLY 表示只读模式
- FileChannel.MapMode.READ_WRITE 表示读写模式
- FileChannel.MapMode.PRIVATE 表示copy-on-write模式,这个模式和READ_ONLY
- 有点相似,它的操作是先对原数据进行拷贝,然后可以在拷贝之后的Buffer中进行读写。但是这个写入并不会影响原数据。可以看做是数据的本地拷贝,所以叫做Private。
基本的MapMode就这三种了,其实除了基础的MapMode,还有两种扩展的MapMode:
- ExtendedMapMode.READ_ONLY_SYNC 同步的读
- ExtendedMapMode.READ_WRITE_SYNC 同步的读写
MappedByteBuffer的最大值
小师妹:F师兄,既然可以映射到虚拟内存空间,那么这个MappedByteBuffer是不是可以无限大?
当然不是了,首先虚拟地址空间的大小是有限制的,如果是32位的CPU,那么一个指针占用的地址就是4个字节,那么能够表示的最大值是0xFFFFFFFF,也就是4G。
另外我们看下map方法中size的类型是long,在java中long能够表示的最大值是0x7fffffff,也就是2147483647字节,换算一下大概是2G。也就是说MappedByteBuffer的最大值是2G,一次最多只能map 2G的数据。
MappedByteBuffer的使用
小师妹,F师兄我们来举两个使用MappedByteBuffer读写的例子吧。
善!
先看一下怎么使用MappedByteBuffer来读数据:
public void readWithMap() throws IOException { try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r")) { //get Channel FileChannel fileChannel = file.getChannel(); //get mappedByteBuffer from fileChannel MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); // check buffer log.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一个提醒而不是guarantee log.info("capacity {}",buffer.capacity()); //read the buffer for (int i = 0; i < buffer.limit(); i++) { log.info("get {}", buffer.get()); } } }
然后再看一个使用MappedByteBuffer来写数据的例子:
public void writeWithMap() throws IOException { try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw")) { //get Channel FileChannel fileChannel = file.getChannel(); //get mappedByteBuffer from fileChannel MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 ); // check buffer log.info("is Loaded in physical memory: {}",buffer.isLoaded()); //只是一个提醒而不是guarantee log.info("capacity {}",buffer.capacity()); //write the content buffer.put("www.flydean.com".getBytes()); } }
MappedByteBuffer要注意的事项
小师妹:F师兄,MappedByteBuffer因为使用了内存映射,所以读写的速度都会有所提升。那么我们在使用中应该注意哪些问题呢?
MappedByteBuffer是没有close方法的,即使它的FileChannel被close了,
MappedByteBuffer仍然处于打开状态,只有JVM进行垃圾回收的时候才会被关闭。而这个时间是不确定的。
总结
本文再次介绍了虚拟地址空间和MappedByteBuffer的使用。
第十三章 NIO中那些奇怪的Buffer
简介
妖魔鬼怪快快显形,今天F师兄帮助小师妹来斩妖除魔啦,什么BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU统统给你剖析个清清楚楚明明白白。
Buffer的分类
小师妹:F师兄不都说JDK源码是最好的java老师吗?为程不识源码,就称牛人也枉然。但是我最近在学习NIO的时候竟然发现有些Buffer类居然没有注释,就那么突兀的写在哪里,让人好生心烦。
更多内容请访问www.flydean.com
居然还有这样的事情?快带F师兄去看看。
小师妹:F师兄你看,以ShortBuffer为例,它的子类怎么后面都带一些奇奇怪怪的字符:
什么什么BufferB,BufferL,BufferRB,BufferRL,BufferS,BufferU,BufferRS,BufferRU都来了,点进去看他们的源码也没有说明这些类到底是做什么的。
还真有这种事情,给我一个小时,让我仔细研究研究。
一个小时后,小师妹,经过我一个小时的辛苦勘察,结果发现,确实没有官方文档介绍这几个类到底是什么含义,但是师兄我掐指一算,好像发现了这些类之间的小秘密,且听为兄娓娓道来。
之前的文章,我们讲到Buffer根据类型可以分为ShortBuffer,LongBuffer,DoubleBuffer等等。
但是根据本质和使用习惯,我们又可以分为三类,分别是:ByteBufferAsXXXBuffer,DirectXXXBuffer和HeapXXXBuffer。
ByteBufferAsXXXBuffer主要将ByteBuffer转换成为特定类型的Buffer,比如CharBuffer,IntBuffer等等。
而DirectXXXBuffer则是和虚拟内存映射打交道的Buffer。
最后HeapXXXBuffer是在堆空间上面创建的Buffer。
Big Endian 和 Little Endian
小师妹,F师兄,你刚刚讲的都不重要,我就想知道类后面的B,L,R,S,U是做什么的。
好吧,在给你讲解这些内容之前,师兄我给你讲一个故事。
话说在明末浙江才女吴绛雪写过一首诗:《春 景 诗》
莺啼岸柳弄春晴,
柳弄春晴夜月明。
明月夜晴春弄柳,
晴春弄柳岸啼莺。
小师妹,可有看出什么特异之处?最好是多读几遍,读出声来。
小师妹:哇,F师兄,这首诗从头到尾和从尾到头读起来是一样的呀,又对称又有意境!
不错,这就是中文的魅力啦,根据读的方式不同,得出的结果也不同,其实在计算机世界也存在这样的问题。
我们知道在java中底层的最小存储单元是Byte,一个Byte是8bits,用16进制表示就是Ox00-OxFF。
java中除了byte,boolean是占一个字节以外,好像其他的类型都会占用多个字节。
如果以int来举例,int占用4个字节,其范围是从Ox00000000-OxFFFFFFFF,假如我们有一个int=Ox12345678,存到内存地址里面就有这样两种方式。
第一种Big Endian将高位的字节存储在起始地址
第二种Little Endian将地位的字节存储在起始地址
其实Big Endian更加符合人类的读写习惯,而Little Endian更加符合机器的读写习惯。
目前主流的两大CPU阵营中,PowerPC系列采用big endian方式存储数据,而x86系列则采用little endian方式存储数据。
如果不同的CPU架构直接进行通信,就由可能因为读取顺序的不同而产生问题。
java的设计初衷就是一次编写处处运行,所以自然也做了设计。
所以BufferB表示的是Big Endian的buffer,BufferL表示的是Little endian的Buffer。
而BufferRB,BufferRL表示的是两种只读Buffer。
aligned内存对齐
小师妹:F师兄,那这几个又是做什么用的呢? BufferS,BufferU,BufferRS,BufferRU。
在讲解这几个类之前,我们先要回顾一下JVM中对象的存储方式。
还记得我们是怎么使用JOL来分析JVM的信息的吗?代码非常非常简单:
log.info("{}", VM.current().details());
输出结果:
## Running 64-bit HotSpot VM. ## Using compressed oop with 3-bit shift. ## Using compressed klass with 3-bit shift. ## WARNING | Compressed references base/shifts are guessed by the experiment! ## WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE. ## WARNING | Make sure to attach Serviceability Agent to get the reliable addresses. ## Objects are 8 bytes aligned. ## Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] ## Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
上面的输出中,我们可以看到:Objects are 8 bytes aligned,这意味着所有的对象分配的字节都是8的整数倍。
再注意上面输出的一个关键字aligned,确认过眼神,是对的那个人。
aligned对齐的意思,表示JVM中的对象都是以8字节对齐的,如果对象本身占用的空间不足8字节或者不是8字节的倍数,则补齐。
还是用JOL来分析String对象:
[main] INFO com.flydean.JolUsage - java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 byte[] String.value N/A 16 4 int String.hash N/A 20 1 byte String.coder N/A 21 1 boolean String.hashIsZero N/A 22 2 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
可以看到一个String对象占用24字节,但是真正有意义的是22字节,有两个2字节是补齐用的。
对齐的好处显而易见,就是CPU在读取数据的时候更加方便和快捷,因为CPU设定是一次读取多少字节来的,如果你存储是没有对齐的,则CPU读取起来效率会比较低。
现在可以回答部分问题:BufferU表示是unaligned,BufferRU表示是只读的unaligned。
小师妹:那BufferS和BufferRS呢?
这个问题其实还是很难回答的,但是经过师兄我的不断研究和探索,终于找到了答案:
先看下DirectShortBufferRU和DirectShortBufferRS的区别,两者的区别在两个地方,先看第一个Order:
DirectShortBufferRU: public ByteOrder order() { return ((ByteOrder.nativeOrder() != ByteOrder.BIG_ENDIAN) ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); }
DirectShortBufferRS: public ByteOrder order() { return ((ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); }
可以看到DirectShortBufferRU的Order是跟nativeOrder是一致的。而DirectShortBufferRS的Order跟nativeOrder是相反的。
为什么相反?再看两者get方法的不同:
DirectShortBufferU: public short get() { try { checkSegment(); return ((UNSAFE.getShort(ix(nextGetIndex())))); } finally { Reference.reachabilityFence(this); } }
DirectShortBufferS: public short get() { try { checkSegment(); return (Bits.swap(UNSAFE.getShort(ix(nextGetIndex())))); } finally { Reference.reachabilityFence(this); } }
区别出来了,DirectShortBufferS在返回的时候做了一个bits的swap操作。
所以BufferS表示的是swap过后的Buffer,和BufferRS表示的是只读的swap过后的Buffer。
总结
不写注释实在是害死人啊!尤其是JDK自己也不写注释的情况下!
第十四章 用Selector来说再见
简介
NIO有三宝:Buffer,Channel,Selector少不了。本文将会介绍NIO三件套中的最后一套Selector,并在理解Selector的基础上,协助小师妹发一张好人卡。我们开始吧。
Selector介绍
小师妹:F师兄,最近我的桃花有点旺,好几个师兄莫名其妙的跟我打招呼,可是我一心向着工作,不想谈论这些事情。毕竟先有事业才有家嘛。我又不好直接拒绝,有没有什么比较隐晦的方法来让他们放弃这个想法?
更多内容请访问www.flydean.com
这个问题,我沉思了大约0.001秒,于是给出了答案:给他们发张好人卡吧,应该就不会再来纠缠你了。
小师妹:F师兄,如果给他们发完好人卡还没有用呢?
那就只能切断跟他们的联系了,来个一刀两断。哈哈。
这样吧,小师妹你最近不是在学NIO吗?刚好我们可以用Selector来模拟一下发好人卡的过程。
假如你的志伟师兄和子丹师兄想跟你建立联系,每个人都想跟你建立一个沟通通道,那么你就需要创建两个channel。
两个channel其实还好,如果有多个人都想同时跟你建立联系通道,那么要维持这些通道就需要保持连接,从而浪费了资源。
但是建立的这些连接并不是时时刻刻都有消息在传输,所以其实大多数时间这些建立联系的通道其实是浪费的。
如果使用Selector就可以只启用一个线程来监听通道的消息变动,这就是Selector。
从上面的图可以看出,Selector监听三个不同的channel,然后交给一个processor来处理,从而节约了资源。
创建Selector
先看下selector的定义:
public abstract class Selector implements Closeable
Selector是一个abstract类,并且实现了Closeable,表示Selector是可以被关闭的。
虽然Selector是一个abstract类,但是可以通过open来简单的创建:
Selector selector = Selector.open();
如果细看open的实现可以发现一个很有趣的现象:
public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); }
open方法调用的是SelectorProvider中的openSelector方法。
再看下provider的实现:
public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } });
有三种情况可以加载一个SelectorProvider,如果系统属性指定了java.nio.channels.spi.SelectorProvider,那么从指定的属性加载。
如果没有直接指定属性,则从ServiceLoader来加载。
最后如果都找不到的情况下,使用默认的DefaultSelectorProvider。
关于ServiceLoader的用法,我们后面会有专门的文章来讲述。这里先不做多的解释。
注册Selector到Channel中
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
如果是在服务器端,我们需要先创建一个ServerSocketChannel,绑定Server的地址和端口,然后将Blocking设置为false。因为我们使用了Selector,它实际上是一个非阻塞的IO。
注意FileChannels是不能使用Selector的,因为它是一个阻塞型IO。
小师妹:F师兄,为啥FileChannel是阻塞型的呀?做成非阻塞型的不是更快?
小师妹,我们使用FileChannel的目的是什么?就是为了读文件呀,读取文件肯定是一直读一直读,没有可能读一会这个channel再读另外一个channel吧,因为对于每个channel自己来讲,在文件没读取完之前,都是繁忙状态,没有必要在channel中切换。
最后我们将创建好的Selector注册到channel中去。
SelectionKey
SelectionKey表示的是我们希望监听到的事件。
总的来说,有4种Event:
- SelectionKey.OP_READ 表示服务器准备好,可以从channel中读取数据。
- SelectionKey.OP_WRITE 表示服务器准备好,可以向channel中写入数据。
- SelectionKey.OP_CONNECT 表示客户端尝试去连接服务端
- SelectionKey.OP_ACCEPT 表示服务器accept一个客户端的请求
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
我们可以看到上面的4个Event是用位运算来定义的,如果将这个四个event使用或运算合并起来,就得到了SelectionKey中的interestOps。
和interestOps类似,SelectionKey还有一个readyOps。
一个表示感兴趣的操作,一个表示ready的操作。
最后,SelectionKey在注册的时候,还可以attach一个Object,比如我们可以在这个对象中保存这个channel的id:
SelectionKey key = channel.register( selector, SelectionKey.OP_ACCEPT, object); key.attach(Object); Object object = key.attachment();
object可以在register的时候传入,也可以调用attach方法。
最后,我们可以通过key的attachment方法,获得该对象。
selector 和 SelectionKey
我们通过selector.select()这个一个blocking操作,来获取一个ready的channel。
然后我们通过调用selector.selectedKeys()来获取到SelectionKey对象。
在SelectionKey对象中,我们通过判断ready的event来处理相应的消息。
总的例子
接下来,我们把之前将的串联起来,先建立一个小师妹的ChatServer:
public class ChatServer { private static String BYE_BYE="再见"; public static void main(String[] args) throws IOException, InterruptedException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { selector.select(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey selectionKey = iter.next(); if (selectionKey.isAcceptable()) { register(selector, serverSocketChannel); } if (selectionKey.isReadable()) { serverResonse(byteBuffer, selectionKey); } iter.remove(); } Thread.sleep(1000); } } private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey) throws IOException { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); log.info(new String(bytes).trim()); if(new String(bytes).trim().equals(BYE_BYE)){ log.info("说再见不如不见!"); socketChannel.write(ByteBuffer.wrap("再见".getBytes())); socketChannel.close(); }else { socketChannel.write(ByteBuffer.wrap("你是个好人".getBytes())); } byteBuffer.clear(); } private static void register(Selector selector, ServerSocketChannel serverSocketChannel) throws IOException { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } }
上面例子有两点需要注意,我们在循环遍历中,当selectionKey.isAcceptable时,表示服务器收到了一个新的客户端连接,这个时候我们需要调用register方法,再注册一个OP_READ事件到这个新的SocketChannel中,然后继续遍历。
第二,我们定义了一个stop word,当收到这个stop word的时候,会直接关闭这个client channel。
再看看客户端的代码:
public class ChatClient { private static SocketChannel socketChannel; private static ByteBuffer byteBuffer; public static void main(String[] args) throws IOException { ChatClient chatClient = new ChatClient(); String response = chatClient.sendMessage("hello 小师妹!"); log.info("response is {}", response); response = chatClient.sendMessage("能不能?"); log.info("response is {}", response); chatClient.stop(); } public void stop() throws IOException { socketChannel.close(); byteBuffer = null; } public ChatClient() throws IOException { socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527)); byteBuffer = ByteBuffer.allocate(512); } public String sendMessage(String msg) throws IOException { byteBuffer = ByteBuffer.wrap(msg.getBytes()); String response = null; socketChannel.write(byteBuffer); byteBuffer.clear(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); response =new String(bytes).trim(); byteBuffer.clear(); return response; } }
客户端代码没什么特别的,需要注意的是Buffer的读取。
最后输出结果:
server收到: INFO com.flydean.ChatServer - hello 小师妹! client收到: INFO com.flydean.ChatClient - response is 你是个好人 server收到: INFO com.flydean.ChatServer - 能不能? client收到: INFO com.flydean.ChatClient - response is 再见
解释一下整个流程:志伟跟小师妹建立了一个连接,志伟向小师妹打了一个招呼,小师妹给志伟发了一张好人卡。志伟不死心,想继续纠缠,小师妹回复再见,然后自己关闭了通道。
总结
本文介绍了Selector和channel在发好人卡的过程中的作用。
第十五章 文件编码和字符集Unicode
简介
小师妹一时兴起,使用了一项从来都没用过的新技能,没想却出现了一个无法解决的问题。把大象装进冰箱到底有几步?乱码的问题又是怎么解决的?快来跟F师兄一起看看吧。
使用Properties读取文件
这天,小师妹心情很愉悦,吹着口哨唱着歌,标准的45度俯视让人好不自在。
小师妹呀,什么事情这么高兴,说出来让师兄也沾点喜庆?
小师妹:F师兄,最新我发现了一种新型的读取文件的方法,很好用的,就跟map一样:
public void usePropertiesFile() throws IOException { Properties configProp = new Properties(); InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties"); configProp.load(in); log.info(configProp.getProperty("name")); configProp.setProperty("name", "www.flydean.com"); log.info(configProp.getProperty("name")); }
F师兄你看,我使用了Properties来读取文件,文件里面的内容是key=value形式的,在做配置文件使用的时候非常恰当。我是从Spring项目中的properties配置文件中得到的灵感,才发现原来java还有一个专门读取属性文件的类Properties。
小师妹现在都会抢答了,果然青出于蓝。
乱码初现
小师妹你做得非常好,就这样触类旁通,很快java就要尽归你手了,后面的什么scala,go,JS等估计也统统不在话下。再过几年你就可以升任架构师,公司技术在你的带领之下一定会蒸蒸日上。
做为师兄,最大的责任就是给小师妹以鼓励和信心,给她描绘美好的未来,什么出任CEO,赢取高富帅等全都不在话下。听说有个专业的词汇来描述这个过程叫做:画饼。
小师妹有点心虚:可是F师兄,我还有点小小的问题没有解决,有点中文的小小乱码....
我深有体会的点点头:马赛克是阻碍人类进步的绊脚石...哦,不是马赛克,是文件乱码,要想弄清楚这个问题,还要从那个字符集和文件编码讲起。
字符集和文件编码
在很久很久以前,师兄我都还没有出生的时候,西方世界出现了一种叫做计算机的高科技产品。
初代计算机只能做些简单的算数运算,还要使用人工打孔的程序才能运行,不过随着时间的推移,计算机的体积越来越小,计算能力越来越强,打孔已经不存在了,变成了人工编写的计算机语言。
一切都在变化,唯有一件事情没有变化。这件事件就是计算机和编程语言只流传在西方。而西方日常交流使用26个字母加有限的标点符号就够了。
最初的计算机存储可以是非常昂贵的,我们用一个字节也就是8bit来存储所有能够用到的字符,除了最开始的1bit不用以外,总共有128中选择,装26个小写+26个大写字母和其他的一些标点符号之类的完全够用了。
这就是最初的ASCII编码,也叫做美国信息交换标准代码(American Standard Code for Information Interchange)。
后面计算机传到了全球,人们才发现好像之前的ASCII编码不够用了,比如中文中常用的汉字就有4千多个,怎么办呢?
没关系,将ASCII编码本地化,叫做ANSI编码。1个字节不够用就用2个字节嘛,路是人走出来的,编码也是为人来服务的。于是产生了各种如GB2312, BIG5, JIS等各自的编码标准。这些编码虽然与ASCII编码兼容,但是相互之间却并不兼容。
这严重的影响了国际化的进程,这样还怎么去实现同一个地球,同一片家园的梦想?
于是国际组织出手了,制定了UNICODE字符集,为所有语言的所有字符都定义了一个唯一的编码,unicode的字符集是从U+0000到U+10FFFF这么多个编码。
小师妹:F师兄,那么unicode和我平时听说的UTF-8,UTF-16,UTF-32有什么关系呢?
我笑着问小师妹:小师妹,把大象装进冰箱有几步?
小师妹:F师兄,脑筋急转弯的故事,已经不适合我了,大象装进冰箱有三步,第一打开冰箱,第二把大象装进去,第三关上冰箱,完事了。
小师妹呀,作为一个有文化的中国人,要真正的承担起民族复兴,科技进步的大任,你的想法是很错误的,不能光想口号,要有实际的可操作性的方案才行,要不然我们什么时候才能够打造秦芯,唐芯和明芯呢?
师兄说的对,可是这跟unicode有什么关系呢?
unicode字符集最后是要存储到文件或者内存里面的,那怎么存呢?使用固定的1个字节,2个字节还是用变长的字节呢?根据编码方式的不同,可以分为UTF-8,UTF-16,UTF-32等多种编码方式。
其中UTF-8是一种变长的编码方案,它使用1-4个字节来存储。UTF-16使用2个或者4个字节来存储,JDK9之后的String的底层编码方式变成了两种:LATIN1和UTF16。
而UTF-32是使用4个字节来存储。这三种编码方式中,只有UTF-8是兼容ASCII的,这也是为什么国际上UTF-8编码方式比较通用的原因(毕竟计算机技术都是西方人搞出来的)。
解决Properties中的乱码
小师妹,要解决你Properties中的乱码问题很简单,Reader基本上都有一个Charsets的参数,通过这个参数可以传入要读取的编码方式,我们把UTF-8传进去就行了:
public void usePropertiesWithUTF8() throws IOException{ Properties configProp = new Properties(); InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties"); InputStreamReader inputStreamReader= new InputStreamReader(in, StandardCharsets.UTF_8); configProp.load(inputStreamReader); log.info(configProp.getProperty("name")); configProp.setProperty("name", "www.flydean.com"); log.info(configProp.getProperty("name")); }
上面的代码中,我们使用InputStreamReader封装了InputStream,最终解决了中文乱码的问题。
真.终极解决办法
小师妹又有问题了:F师兄,这样做是因为我们知道文件的编码方式是UTF-8,如果不知道该怎么办呢?是选UTF-8,UTF-16还是UTF-32呢?
小师妹问的问题越来越刁钻了,还好这个问题我也有准备。
接下来介绍我们的终极解决办法,我们将各种编码的字符最后都转换成unicode字符集存到properties文件中,再读取的时候是不是就没有编码的问题了?
转换需要用到JDK自带的工具:
native2ascii -encoding utf-8 file/src/main/resources/www.flydean.com.properties.utf8 file/src/main/resources/www.flydean.com.properties.cn
上面的命令将utf-8的编码转成了unicode。
转换前:
site=www.flydean.com name=程序那些事
转换后:
site=www.flydean.com name=\u7a0b\u5e8f\u90a3\u4e9b\u4e8b
再运行下测试代码:
public void usePropertiesFileWithTransfer() throws IOException { Properties configProp = new Properties(); InputStream in = this.getClass().getClassLoader().getResourceAsStream("www.flydean.com.properties.cn"); configProp.load(in); log.info(configProp.getProperty("name")); configProp.setProperty("name", "www.flydean.com"); log.info(configProp.getProperty("name")); }
输出正确的结果。