博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java Socket实战之六 使用NIO包实现Socket通信
阅读量:2247 次
发布时间:2019-05-09

本文共 18909 字,大约阅读时间需要 63 分钟。

原文

本文地址:

前面几篇文章介绍了使用java.io和java.net类库实现的Socket通信,下面介绍一下使用java.nio类库实现的Socket。

java.nio包是Java在1.4之后增加的,用来提高I/O操作的效率。在nio包中主要包括以下几个类或接口:

* Buffer:缓冲区,用来临时存放输入或输出数据。

* Charset:用来把Unicode字符编码和其它字符编码互转。

* Channel:数据传输通道,用来把Buffer中的数据写入到数据源,或者把数据源中的数据读入到Buffer。

* Selector:用来支持异步I/O操作,也叫非阻塞I/O操作。

nio包中主要通过下面两个方面来提高I/O操作效率:

* 通过Buffer和Channel来提高I/O操作的速度。

* 通过Selector来支持非阻塞I/O操作。

下面来看一下程序中是怎么通过这些类库实现Socket功能。

首先介绍一下几个辅助类

辅助类SerializableUtil,这个类用来把java对象序列化成字节数组,或者把字节数组反序列化成java对象。

[java]
  1. package com.googlecode.garbagecan.test.socket; 
  2.  
  3. import java.io.ByteArrayInputStream; 
  4. import java.io.ByteArrayOutputStream; 
  5. import java.io.IOException; 
  6. import java.io.ObjectInputStream; 
  7. import java.io.ObjectOutputStream; 
  8.  
  9. public class SerializableUtil { 
  10.      
  11.     public static byte[] toBytes(Object object) { 
  12.         ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
  13.         ObjectOutputStream oos = null
  14.         try
  15.             oos = new ObjectOutputStream(baos); 
  16.             oos.writeObject(object); 
  17.             byte[] bytes = baos.toByteArray(); 
  18.             return bytes; 
  19.         } catch(IOException ex) { 
  20.             throw new RuntimeException(ex.getMessage(), ex); 
  21.         } finally
  22.             try
  23.                 oos.close(); 
  24.             } catch (Exception e) {} 
  25.         } 
  26.     } 
  27.      
  28.     public static Object toObject(byte[] bytes) { 
  29.         ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 
  30.         ObjectInputStream ois = null
  31.         try
  32.             ois = new ObjectInputStream(bais); 
  33.             Object object = ois.readObject(); 
  34.             return object; 
  35.         } catch(IOException ex) { 
  36.             throw new RuntimeException(ex.getMessage(), ex); 
  37.         } catch(ClassNotFoundException ex) { 
  38.             throw new RuntimeException(ex.getMessage(), ex); 
  39.         } finally
  40.             try
  41.                 ois.close(); 
  42.             } catch (Exception e) {} 
  43.         } 
  44.     } 
package com.googlecode.garbagecan.test.socket;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class SerializableUtil {		public static byte[] toBytes(Object object) {		ByteArrayOutputStream baos = new ByteArrayOutputStream();		ObjectOutputStream oos = null;		try {			oos = new ObjectOutputStream(baos);			oos.writeObject(object);			byte[] bytes = baos.toByteArray();			return bytes;		} catch(IOException ex) {			throw new RuntimeException(ex.getMessage(), ex);		} finally {			try {				oos.close();			} catch (Exception e) {}		}	}		public static Object toObject(byte[] bytes) {		ByteArrayInputStream bais = new ByteArrayInputStream(bytes);		ObjectInputStream ois = null;		try {			ois = new ObjectInputStream(bais);			Object object = ois.readObject();			return object;		} catch(IOException ex) {			throw new RuntimeException(ex.getMessage(), ex);		} catch(ClassNotFoundException ex) {			throw new RuntimeException(ex.getMessage(), ex);		} finally {			try {				ois.close();			} catch (Exception e) {}		}	}}

辅助类MyRequestObject和MyResponseObject,这两个类是普通的java对象,实现了Serializable接口。MyRequestObject类是Client发出的请求,MyResponseObject是Server端作出的响应。

[java]
  1. package com.googlecode.garbagecan.test.socket.nio; 
  2.  
  3. import java.io.Serializable; 
  4.  
  5. public class MyRequestObject implements Serializable { 
  6.  
  7.     private static final long serialVersionUID = 1L; 
  8.  
  9.     private String name; 
  10.      
  11.     private String value; 
  12.  
  13.     private byte[] bytes; 
  14.      
  15.     public MyRequestObject(String name, String value) { 
  16.         this.name = name; 
  17.         this.value = value; 
  18.         this.bytes = new byte[1024]; 
  19.     } 
  20.      
  21.     public String getName() { 
  22.         return name; 
  23.     } 
  24.  
  25.     public void setName(String name) { 
  26.         this.name = name; 
  27.     } 
  28.  
  29.     public String getValue() { 
  30.         return value; 
  31.     } 
  32.  
  33.     public void setValue(String value) { 
  34.         this.value = value; 
  35.     } 
  36.      
  37.     @Override 
  38.     public String toString() { 
  39.         StringBuffer sb = new StringBuffer(); 
  40.         sb.append("Request [name: " + name  + ", value: " + value + ", bytes: " + bytes.length+ "]"); 
  41.         return sb.toString(); 
  42.     } 
  43.  
  44. package com.googlecode.garbagecan.test.socket.nio; 
  45.  
  46. import java.io.Serializable; 
  47.  
  48. public class MyResponseObject implements Serializable { 
  49.  
  50.     private static final long serialVersionUID = 1L; 
  51.  
  52.     private String name; 
  53.      
  54.     private String value; 
  55.  
  56.     private byte[] bytes; 
  57.      
  58.     public MyResponseObject(String name, String value) { 
  59.         this.name = name; 
  60.         this.value = value; 
  61.         this.bytes = new byte[1024]; 
  62.     } 
  63.      
  64.     public String getName() { 
  65.         return name; 
  66.     } 
  67.  
  68.     public void setName(String name) { 
  69.         this.name = name; 
  70.     } 
  71.  
  72.     public String getValue() { 
  73.         return value; 
  74.     } 
  75.  
  76.     public void setValue(String value) { 
  77.         this.value = value; 
  78.     } 
  79.      
  80.     @Override 
  81.     public String toString() { 
  82.         StringBuffer sb = new StringBuffer(); 
  83.         sb.append("Response [name: " + name  + ", value: " + value + ", bytes: " + bytes.length+ "]"); 
  84.         return sb.toString(); 
  85.     } 
package com.googlecode.garbagecan.test.socket.nio;import java.io.Serializable;public class MyRequestObject implements Serializable {	private static final long serialVersionUID = 1L;	private String name;		private String value;	private byte[] bytes;		public MyRequestObject(String name, String value) {		this.name = name;		this.value = value;		this.bytes = new byte[1024];	}		public String getName() {		return name;	}	public void setName(String name) {		this.name = name;	}	public String getValue() {		return value;	}	public void setValue(String value) {		this.value = value;	}		@Override	public String toString() {		StringBuffer sb = new StringBuffer();		sb.append("Request [name: " + name  + ", value: " + value + ", bytes: " + bytes.length+ "]");		return sb.toString();	}}package com.googlecode.garbagecan.test.socket.nio;import java.io.Serializable;public class MyResponseObject implements Serializable {	private static final long serialVersionUID = 1L;	private String name;		private String value;	private byte[] bytes;		public MyResponseObject(String name, String value) {		this.name = name;		this.value = value;		this.bytes = new byte[1024];	}		public String getName() {		return name;	}	public void setName(String name) {		this.name = name;	}	public String getValue() {		return value;	}	public void setValue(String value) {		this.value = value;	}		@Override	public String toString() {		StringBuffer sb = new StringBuffer();		sb.append("Response [name: " + name  + ", value: " + value + ", bytes: " + bytes.length+ "]");		return sb.toString();	}}

下面主要看一下Server端的代码,其中有一些英文注释对理解代码很有帮助,注释主要是来源jdk的文档和例子,这里就没有再翻译

[java]
  1. package com.googlecode.garbagecan.test.socket.nio; 
  2.  
  3. import java.io.ByteArrayOutputStream; 
  4. import java.io.IOException; 
  5. import java.net.InetSocketAddress; 
  6. import java.nio.ByteBuffer; 
  7. import java.nio.channels.ClosedChannelException; 
  8. import java.nio.channels.SelectionKey; 
  9. import java.nio.channels.Selector; 
  10. import java.nio.channels.ServerSocketChannel; 
  11. import java.nio.channels.SocketChannel; 
  12. import java.util.Iterator; 
  13. import java.util.logging.Level; 
  14. import java.util.logging.Logger; 
  15.  
  16. import com.googlecode.garbagecan.test.socket.SerializableUtil; 
  17.  
  18. public class MyServer3 { 
  19.  
  20.     private final static Logger logger = Logger.getLogger(MyServer3.class.getName()); 
  21.      
  22.     public static void main(String[] args) { 
  23.         Selector selector = null
  24.         ServerSocketChannel serverSocketChannel = null
  25.          
  26.         try
  27.             // Selector for incoming time requests 
  28.             selector = Selector.open(); 
  29.  
  30.             // Create a new server socket and set to non blocking mode 
  31.             serverSocketChannel = ServerSocketChannel.open(); 
  32.             serverSocketChannel.configureBlocking(false); 
  33.              
  34.             // Bind the server socket to the local host and port 
  35.             serverSocketChannel.socket().setReuseAddress(true); 
  36.             serverSocketChannel.socket().bind(new InetSocketAddress(10000)); 
  37.              
  38.             // Register accepts on the server socket with the selector. This 
  39.             // step tells the selector that the socket wants to be put on the 
  40.             // ready list when accept operations occur, so allowing multiplexed 
  41.             // non-blocking I/O to take place. 
  42.             serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 
  43.      
  44.             // Here's where everything happens. The select method will 
  45.             // return when any operations registered above have occurred, the 
  46.             // thread has been interrupted, etc. 
  47.             while (selector.select() > 0) { 
  48.                 // Someone is ready for I/O, get the ready keys 
  49.                 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); 
  50.      
  51.                 // Walk through the ready keys collection and process date requests. 
  52.                 while (it.hasNext()) { 
  53.                     SelectionKey readyKey = it.next(); 
  54.                     it.remove(); 
  55.                      
  56.                     // The key indexes into the selector so you 
  57.                     // can retrieve the socket that's ready for I/O 
  58.                     execute((ServerSocketChannel) readyKey.channel()); 
  59.                 } 
  60.             } 
  61.         } catch (ClosedChannelException ex) { 
  62.             logger.log(Level.SEVERE, null, ex); 
  63.         } catch (IOException ex) { 
  64.             logger.log(Level.SEVERE, null, ex); 
  65.         } finally
  66.             try
  67.                 selector.close(); 
  68.             } catch(Exception ex) {} 
  69.             try
  70.                 serverSocketChannel.close(); 
  71.             } catch(Exception ex) {} 
  72.         } 
  73.     } 
  74.  
  75.     private static void execute(ServerSocketChannel serverSocketChannel) throws IOException { 
  76.         SocketChannel socketChannel = null
  77.         try
  78.             socketChannel = serverSocketChannel.accept(); 
  79.             MyRequestObject myRequestObject = receiveData(socketChannel); 
  80.             logger.log(Level.INFO, myRequestObject.toString()); 
  81.              
  82.             MyResponseObject myResponseObject = new MyResponseObject( 
  83.                     "response for " + myRequestObject.getName(),  
  84.                     "response for " + myRequestObject.getValue()); 
  85.             sendData(socketChannel, myResponseObject); 
  86.             logger.log(Level.INFO, myResponseObject.toString()); 
  87.         } finally
  88.             try
  89.                 socketChannel.close(); 
  90.             } catch(Exception ex) {} 
  91.         } 
  92.     } 
  93.      
  94.     private static MyRequestObject receiveData(SocketChannel socketChannel) throws IOException { 
  95.         MyRequestObject myRequestObject = null
  96.         ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
  97.         ByteBuffer buffer = ByteBuffer.allocate(1024); 
  98.          
  99.         try
  100.             byte[] bytes; 
  101.             int size = 0
  102.             while ((size = socketChannel.read(buffer)) >= 0) { 
  103.                 buffer.flip(); 
  104.                 bytes = new byte[size]; 
  105.                 buffer.get(bytes); 
  106.                 baos.write(bytes); 
  107.                 buffer.clear(); 
  108.             } 
  109.             bytes = baos.toByteArray(); 
  110.             Object obj = SerializableUtil.toObject(bytes); 
  111.             myRequestObject = (MyRequestObject)obj; 
  112.         } finally
  113.             try
  114.                 baos.close(); 
  115.             } catch(Exception ex) {} 
  116.         } 
  117.         return myRequestObject; 
  118.     } 
  119.  
  120.     private static void sendData(SocketChannel socketChannel, MyResponseObject myResponseObject) throws IOException { 
  121.         byte[] bytes = SerializableUtil.toBytes(myResponseObject); 
  122.         ByteBuffer buffer = ByteBuffer.wrap(bytes); 
  123.         socketChannel.write(buffer); 
  124.     } 
package com.googlecode.garbagecan.test.socket.nio;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.ClosedChannelException;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.logging.Level;import java.util.logging.Logger;import com.googlecode.garbagecan.test.socket.SerializableUtil;public class MyServer3 {	private final static Logger logger = Logger.getLogger(MyServer3.class.getName());		public static void main(String[] args) {		Selector selector = null;		ServerSocketChannel serverSocketChannel = null;				try {			// Selector for incoming time requests			selector = Selector.open();			// Create a new server socket and set to non blocking mode			serverSocketChannel = ServerSocketChannel.open();			serverSocketChannel.configureBlocking(false);						// Bind the server socket to the local host and port			serverSocketChannel.socket().setReuseAddress(true);			serverSocketChannel.socket().bind(new InetSocketAddress(10000));						// Register accepts on the server socket with the selector. This			// step tells the selector that the socket wants to be put on the			// ready list when accept operations occur, so allowing multiplexed			// non-blocking I/O to take place.			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);				// Here's where everything happens. The select method will			// return when any operations registered above have occurred, the			// thread has been interrupted, etc.			while (selector.select() > 0) {				// Someone is ready for I/O, get the ready keys				Iterator
it = selector.selectedKeys().iterator(); // Walk through the ready keys collection and process date requests. while (it.hasNext()) { SelectionKey readyKey = it.next(); it.remove(); // The key indexes into the selector so you // can retrieve the socket that's ready for I/O execute((ServerSocketChannel) readyKey.channel()); } } } catch (ClosedChannelException ex) { logger.log(Level.SEVERE, null, ex); } catch (IOException ex) { logger.log(Level.SEVERE, null, ex); } finally { try { selector.close(); } catch(Exception ex) {} try { serverSocketChannel.close(); } catch(Exception ex) {} } } private static void execute(ServerSocketChannel serverSocketChannel) throws IOException { SocketChannel socketChannel = null; try { socketChannel = serverSocketChannel.accept(); MyRequestObject myRequestObject = receiveData(socketChannel); logger.log(Level.INFO, myRequestObject.toString()); MyResponseObject myResponseObject = new MyResponseObject( "response for " + myRequestObject.getName(), "response for " + myRequestObject.getValue()); sendData(socketChannel, myResponseObject); logger.log(Level.INFO, myResponseObject.toString()); } finally { try { socketChannel.close(); } catch(Exception ex) {} } } private static MyRequestObject receiveData(SocketChannel socketChannel) throws IOException { MyRequestObject myRequestObject = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteBuffer buffer = ByteBuffer.allocate(1024); try { byte[] bytes; int size = 0; while ((size = socketChannel.read(buffer)) >= 0) { buffer.flip(); bytes = new byte[size]; buffer.get(bytes); baos.write(bytes); buffer.clear(); } bytes = baos.toByteArray(); Object obj = SerializableUtil.toObject(bytes); myRequestObject = (MyRequestObject)obj; } finally { try { baos.close(); } catch(Exception ex) {} } return myRequestObject; } private static void sendData(SocketChannel socketChannel, MyResponseObject myResponseObject) throws IOException { byte[] bytes = SerializableUtil.toBytes(myResponseObject); ByteBuffer buffer = ByteBuffer.wrap(bytes); socketChannel.write(buffer); }}

下面是Client的代码,代码比较简单就是启动了100个线程来访问Server

[java]
  1. package com.googlecode.garbagecan.test.socket.nio; 
  2.  
  3. import java.io.ByteArrayOutputStream; 
  4. import java.io.IOException; 
  5. import java.net.InetSocketAddress; 
  6. import java.net.SocketAddress; 
  7. import java.nio.ByteBuffer; 
  8. import java.nio.channels.SocketChannel; 
  9. import java.util.logging.Level; 
  10. import java.util.logging.Logger; 
  11.  
  12. import com.googlecode.garbagecan.test.socket.SerializableUtil; 
  13.  
  14. public class MyClient3 { 
  15.  
  16.     private final static Logger logger = Logger.getLogger(MyClient3.class.getName()); 
  17.      
  18.     public static void main(String[] args) throws Exception { 
  19.         for (int i = 0; i < 100; i++) { 
  20.             final int idx = i; 
  21.             new Thread(new MyRunnable(idx)).start(); 
  22.         } 
  23.     } 
  24.      
  25.     private static final class MyRunnable implements Runnable { 
  26.          
  27.         private final int idx; 
  28.  
  29.         private MyRunnable(int idx) { 
  30.             this.idx = idx; 
  31.         } 
  32.  
  33.         public void run() { 
  34.             SocketChannel socketChannel = null
  35.             try
  36.                 socketChannel = SocketChannel.open(); 
  37.                 SocketAddress socketAddress = new InetSocketAddress("localhost", 10000); 
  38.                 socketChannel.connect(socketAddress); 
  39.  
  40.                 MyRequestObject myRequestObject = new MyRequestObject("request_" + idx, "request_" + idx); 
  41.                 logger.log(Level.INFO, myRequestObject.toString()); 
  42.                 sendData(socketChannel, myRequestObject); 
  43.                  
  44.                 MyResponseObject myResponseObject = receiveData(socketChannel); 
  45.                 logger.log(Level.INFO, myResponseObject.toString()); 
  46.             } catch (Exception ex) { 
  47.                 logger.log(Level.SEVERE, null, ex); 
  48.             } finally
  49.                 try
  50.                     socketChannel.close(); 
  51.                 } catch(Exception ex) {} 
  52.             } 
  53.         } 
  54.  
  55.         private void sendData(SocketChannel socketChannel, MyRequestObject myRequestObject) throws IOException { 
  56.             byte[] bytes = SerializableUtil.toBytes(myRequestObject); 
  57.             ByteBuffer buffer = ByteBuffer.wrap(bytes); 
  58.             socketChannel.write(buffer); 
  59.             socketChannel.socket().shutdownOutput(); 
  60.         } 
  61.  
  62.         private MyResponseObject receiveData(SocketChannel socketChannel) throws IOException { 
  63.             MyResponseObject myResponseObject = null
  64.             ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
  65.              
  66.             try
  67.                 ByteBuffer buffer = ByteBuffer.allocateDirect(1024); 
  68.                 byte[] bytes; 
  69.                 int count = 0
  70.                 while ((count = socketChannel.read(buffer)) >= 0) { 
  71.                     buffer.flip(); 
  72.                     bytes = new byte[count]; 
  73.                     buffer.get(bytes); 
  74.                     baos.write(bytes); 
  75.                     buffer.clear(); 
  76.                 } 
  77.                 bytes = baos.toByteArray(); 
  78.                 Object obj = SerializableUtil.toObject(bytes); 
  79.                 myResponseObject = (MyResponseObject) obj; 
  80.                 socketChannel.socket().shutdownInput(); 
  81.             } finally
  82.                 try
  83.                     baos.close(); 
  84.                 } catch(Exception ex) {} 
  85.             } 
  86.             return myResponseObject; 
  87.         } 
  88.     } 
package com.googlecode.garbagecan.test.socket.nio;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.InetSocketAddress;import java.net.SocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SocketChannel;import java.util.logging.Level;import java.util.logging.Logger;import com.googlecode.garbagecan.test.socket.SerializableUtil;public class MyClient3 {	private final static Logger logger = Logger.getLogger(MyClient3.class.getName());		public static void main(String[] args) throws Exception {		for (int i = 0; i < 100; i++) {			final int idx = i;			new Thread(new MyRunnable(idx)).start();		}	}		private static final class MyRunnable implements Runnable {				private final int idx;		private MyRunnable(int idx) {			this.idx = idx;		}		public void run() {			SocketChannel socketChannel = null;			try {				socketChannel = SocketChannel.open();				SocketAddress socketAddress = new InetSocketAddress("localhost", 10000);				socketChannel.connect(socketAddress);				MyRequestObject myRequestObject = new MyRequestObject("request_" + idx, "request_" + idx);				logger.log(Level.INFO, myRequestObject.toString());				sendData(socketChannel, myRequestObject);								MyResponseObject myResponseObject = receiveData(socketChannel);				logger.log(Level.INFO, myResponseObject.toString());			} catch (Exception ex) {				logger.log(Level.SEVERE, null, ex);			} finally {				try {					socketChannel.close();				} catch(Exception ex) {}			}		}		private void sendData(SocketChannel socketChannel, MyRequestObject myRequestObject) throws IOException {			byte[] bytes = SerializableUtil.toBytes(myRequestObject);			ByteBuffer buffer = ByteBuffer.wrap(bytes);			socketChannel.write(buffer);			socketChannel.socket().shutdownOutput();		}		private MyResponseObject receiveData(SocketChannel socketChannel) throws IOException {			MyResponseObject myResponseObject = null;			ByteArrayOutputStream baos = new ByteArrayOutputStream();						try {				ByteBuffer buffer = ByteBuffer.allocateDirect(1024);				byte[] bytes;				int count = 0;				while ((count = socketChannel.read(buffer)) >= 0) {					buffer.flip();					bytes = new byte[count];					buffer.get(bytes);					baos.write(bytes);					buffer.clear();				}				bytes = baos.toByteArray();				Object obj = SerializableUtil.toObject(bytes);				myResponseObject = (MyResponseObject) obj;				socketChannel.socket().shutdownInput();			} finally {				try {					baos.close();				} catch(Exception ex) {}			}			return myResponseObject;		}	}}

最后测试上面的代码,首先运行Server类,然后运行Client类,就可以分别在Server端和Client端控制台看到发送或接收到的MyRequestObject或MyResponseObject对象了。
关于NIO和IO的比较,下面的两篇文章对理解很有帮助,可以参考一下。

http://tutorials.jenkov.com/java-nio/nio-vs-io.html

https://blogs.oracle.com/slc/entry/javanio_vs_javaio

 

转载地址:http://nnjdb.baihongyu.com/

你可能感兴趣的文章
TensorFlow2 学习——RNN生成古诗词
查看>>
Spark源码剖析——SparkContext实例化
查看>>
基于阿里云的数据仓库架构设计
查看>>
OpenCV学习——图像基础与几何变换
查看>>
OpenCV学习——图像特效
查看>>
分布式——缓存一致性(Redis、MySQL)
查看>>
Gminer 配置参数
查看>>
Linux学习笔记——20170802
查看>>
Linux学习笔记——20170803
查看>>
Linux学习笔记——20170804
查看>>
Linux学习笔记——20170805
查看>>
Linux学习笔记——20170807
查看>>
MySQL学习笔记——20170808
查看>>
MySQL学习笔记——20170809
查看>>
MySQL学习笔记——20170810
查看>>
MySQL学习笔记——20170811
查看>>
MySQL学习笔记——20170812
查看>>
Android内存溢出与优化(五)——防止static引用
查看>>
Scala学习笔记——20170817
查看>>
Scala学习笔记——20170818
查看>>