目录
二、在socket网络编程中,Stream和Channel的对比
一、stream和channel的区别
通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。(Channel本身不存储数据,因此需要配合缓冲区进行传输)
NIO是jdk1.4开始提供的一种新的IO方式。原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。原来的 I/O 以流的方式处理数据,而 NIO 以块(buffer)的方式处理数据。面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。这里需要说明为什么面向块的IO要比面向流的IO要快,举例来说应用程序从磁盘读取数据的时候其实并不是直接从磁盘取数据的而是先由操作系统把磁盘上的数据读到系统内存中,这一步是操作系统的IO,不由应用程序控制;然后应用程序再把数据从系统内存读取到应用内存中去,后一步也就是我们程序中的IO操作。操作系统一般一次将一块的数据由磁盘移动到系统内存上去,基于块的IO和基于流IO的不同之处在于基于流的IO需要一个个字符的把系统内存上的数据移动到应用内存上去,而基于块的IO会一次一块的将数据移动到应用内存,效率自然是基于块的IO更好。但在jDK1.4之后,BIO的底层也使用NIO方式进行了部分重写,所以就文件读写效率方面来说,两者差别已经不大,最重要的差别还是NIO提供了异步非阻塞的网络编程模型,这是BIO所不能实现的。
Channel 如何做到异步?
例如下图,我们应NIO的 Selector来监听 多个Channel,由于Channel是既可以读也可以写的,因此Selector监听Channel时,并不需要像BIO一样,阻塞到Channel有数据才返回,则是可以达到 当Channel有数据时,主动去通知Selector,让Selector去处理,这样的话,Selector就不需要阻塞了,这就是Channel的异步。
最明显的区别:一个是阻塞读取,一个是到达读取
stream 是BIO,也就是阻塞模式,分IN OUT两个,无论是IN还是OUT,在进行消息处理是是阻塞当前线程的,也就是需要等待对方的数据到达。
channel 是NIO,可以看作其包含了IN OUT是一个通道的概念,channel的实现有很多,针对本地文件的,针对网络的,针对内存的,不同的其表现并不完全相同,但可以理解为:channel的数据消费是将数据写入缓冲区buffer,或从buffer读取数据;这个过程是可以通过selector来辅助监听channel的状态
二、在socket网络编程中,Stream和Channel的对比
2.1 BIO
例如在聊天室系统中,如果用BIO的stream,那么就意味着,系统必须要为每一个连接进服务器的客户端开一条线程用来接收客户端传来的数据。那就存在2个问题:
1. 会开辟比较多的线程
2. 当客户端没有信息到达时,线程会处于阻塞状态,如果是百万级别的连接量的话,就会造成非常多线程状态的切换,线程状态切换是要消耗系统资源的。
如下图,如果有n个客户端连入服务器,那么在BIO(用stream)的情况下,就得额外开辟n条线程去负责接收n个客户端传来的信息。
具体代码如下,大家可以运行看看:
服务端:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
ServerSocket ss = null;
boolean started =false;
public void start(){
try{
ss = new ServerSocket(8888);
started = true;
}catch (BindException e){
System.out.println("端口已被占用");
System.exit(0);
}catch (Exception e){
e.printStackTrace();
}
try{
while(started){
Socket s = ss.accept();
Client c = new Client(s);
new Thread(c).start();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Client implements Runnable{
Socket s = null;
DataInputStream dis = null;
DataOutputStream dos = null;
boolean bconnected = false;
public Client(Socket s){
this.s = s;
try {
dis = new DataInputStream(s.getInputStream());
dos = new DataOutputStream(s.getOutputStream());
bconnected = true;
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
String str = null;
try{
while(bconnected){
str =dis.readUTF();
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new TcpServer().start();
}
}
客户端:
import javax.xml.crypto.Data;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class TcpClient {
Socket s = null;
DataInputStream dis = null;
DataOutputStream dos = null;
boolean bconnected = false;
Scanner sc = new Scanner(System.in);
public void start(){
try {
s = new Socket("127.0.0.1",8888);
dis = new DataInputStream(s.getInputStream());
dos = new DataOutputStream(s.getOutputStream());
bconnected = true;
System.out.println("connect...");
while (bconnected){
while(sc.hasNext()){
String str = sc.next()+'\n';
dos.writeUTF(str);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TcpClient().start();
}
}
2.2 NIO
服务端代码:
这里只展示服务端代码,想要完整代码,请参照:https://www.cnblogs.com/yixiu868/p/8034899.html
下面的服务端代码很明显就是用一条线程来监听:客户端连接时间、读事件、写事件。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
public class TCPServerSelector{
//缓冲区的长度
private static final int BUFSIZE = 256;
//select方法等待信道准备好的最长时间
private static final int TIMEOUT = 3000;
public static void main(String[] args) throws IOException {
if (args.length < 1){
throw new IllegalArgumentException("Parameter(s): <Port> ...");
}
//创建一个选择器
Selector selector = Selector.open();
for (String arg : args){
//实例化一个信道
ServerSocketChannel listnChannel = ServerSocketChannel.open();
//将该信道绑定到指定端口
listnChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));
//配置信道为非阻塞模式
listnChannel.configureBlocking(false);
//将选择器注册到各个信道
listnChannel.register(selector, SelectionKey.OP_ACCEPT);
}
//创建一个实现了协议接口的对象
TCPProtocol protocol = new EchoSelectorProtocol(BUFSIZE);
//不断轮询select方法,获取准备好的信道所关联的Key集
while (true){
//一直等待,直至有信道准备好了I/O操作
if (selector.select(TIMEOUT) == 0){
//在等待信道准备的同时,也可以异步地执行其他任务,
//这里只是简单地打印"."
System.out.print(".");
continue;
}
//获取准备好的信道所关联的Key集合的iterator实例
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
//循环取得集合中的每个键值
while (keyIter.hasNext()){
SelectionKey key = keyIter.next();
//如果服务端信道感兴趣的I/O操作为accept
if (key.isAcceptable()){
protocol.handleAccept(key);
}
//如果客户端信道感兴趣的I/O操作为read
if (key.isReadable()){
protocol.handleRead(key);
}
//如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write
if (key.isValid() && key.isWritable()) {
protocol.handleWrite(key);
}
//这里需要手动从键集中移除当前的key
keyIter.remove();
}
}
}
}
Selector的select方法主要是通过轮询式地监听各个channel ,看是否有对应的事件就绪(例如,新客户端连接进来,有客户端发送消息过来 等等),若有事件就绪,就处理事件,若无,则阻塞到有事件到达为止,但是只要一处理事件,就肯定不会阻塞住的。异步就体现在,select不会只看死一个事件,不同种类的事件,只要就绪了,就会通知监听的select,让其进行处理。若有N个客户端,那么就创建N个客户端对应的channel,再把channel注册给select监听即可。
(当然,select在等待的时候也可以设置阻塞时间,让它去执行别的任务)