Socket之bio和nio

在此之前先谈论一下网络io.当一个客户端和服务端之间相互通信,交互我们称之为网络io(网络通讯).网络通讯基本都是通过socket来通讯的。客户端和服务端这样建立连接:第一步客户端发起建立连接的请求,第二部服务端收到请求建立连接的请求,并同意和该客户端建立连接,并响应给客户端,第三步客户端收到服务端响应的建立连接的消息,并确认和服务端建立连接,通过这样三部客户端和服务端就真正的建立了连接,服务端和客户端就可以开始通讯,交互了.通过这样三次的握手交互服务端和客户端就成功的建立了连接..

如图所示:


辣么在我们传统的bio里面服务端是怎么处理客户端的请求的呢,首先我们的服务端需要实例化一个socket给我们的该实例socket 绑定ip(本机)和端口,然后进行监听,然后就会一直堵塞等待和客户端stoket建立连接,此时客户端也需要实例化一个socket来和我们服务端建立请求,具体操作和服务端一样,绑定ip和端口(服务端监控的ip和port),然后通过三次握手确认和服务端建立连接通讯.成功建立连接之后服务端需要新建一个线程去处理该客户端的通讯请求(io操作),然后主线程会一直堵塞等到下一个客户端socket发起连接请求,在处理下一个,这就是最原始的堵塞式的bio,这种方式的最明显的缺点就是,服务端与每个客户端socket建立连接都需要实时的去创建一个线程去处理该客户端操作,加入同时又1000个客户端socket同时求情建立连接,那我们的服务端就需要同时创建1000个线程去处理,完全没有一点点的缓冲也不能拒绝,显然我们的服务器会吃不消,如果客户端增加socket增加到2000个达到了我们的系统所承受的上线,那我们的服务端就会直接蹦掉.但是jdk1.5以后出现了线程池和队列,能稍稍减轻一点点服务器的负担,吧客户端请求直接扔给我们的线程池去处理,再对我们的线程池进行配置最大线程处理数量和队列处理请求排队,等方式去解决服务器的并发过大的情况。

如图所示:



java代码实现 

首先我们需要一个处理请求的类,就是通过输入输出流去读取和反馈客户端的这样的一个类,这个类实现Runnable接口

package liuzw.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable{
	private Socket socket ;
	public ServerHandler(Socket socket){
		this.socket = socket;
	}
	@Override
	public void run() {
		BufferedReader in = null;
		PrintWriter out = null;
		try {
			in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
			out = new PrintWriter(this.socket.getOutputStream(), true);
			String body = null;
			while(true){
				body = in.readLine();
				if(body == null) break;
				System.out.println("Server :" + body);
				out.println("服务器端回送响的应数据.");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(in != null){
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(out != null){
				try {
					out.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			if(socket != null){
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			socket = null;
		}
	}
}
然后就是用于接受客户端请求的入口

package liuzw.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
	final static int PROT = 8765;
	public static void main(String[] args) {
		ServerSocket server = null;
		try {
			server = new ServerSocket(PROT);
			System.out.println(" server start .. ");
			//进行阻塞
			Socket socket = server.accept();
			//新建一个线程执行客户端的任务
			new Thread(new ServerHandler(socket)).start();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(server != null){
				try {
					server.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			server = null;
		}
	}
}

接下来就是我那的客户端了

package liuzw.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {
	final static String ADDRESS = "127.0.0.1";
	final static int PORT = 8765;
	public static void main(String[] args) {
		Socket socket = null;
		BufferedReader in = null;
		PrintWriter out = null;
		try {
			socket = new Socket(ADDRESS, PORT);
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			out = new PrintWriter(socket.getOutputStream(), true);
			//向服务器端发送数据
			out.println("接收到客户端的请求数据...");
			out.println("接收到客户端的请求数据1111...");
			String response = in.readLine();
			System.out.println("Client: " + response);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(in != null){
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(out != null){
				try {
					out.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			if(socket != null){
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			socket = null;
		}
	}
}

上面说了bio是一种堵塞式的同步的网络io,接下来的讲到了的nio是一种非堵塞式的网络io但是最开始的时候也是同步的.我们前面介绍了bio是客户端和服务端各自创建一个socket实例建立连接相互通信,并且通信是通过单向io客户端发送数据的时候客户端就必须同步去接收收据,同样的服务端向客户端相应数据的时候也必须同保持连接,如果说客户端和服务端的网速非常慢,这样就会导致客户端和服务端的连接长时间不能关闭,从而浪费了很多资源。因为是直连和单向io所以每请次求和响应客户端和服务端都需要创建一个输出流和输入流,收网速的影响流长时间不能关闭那我们nio的改进方式是建立一个缓冲区buffer,这个是nio里面特有的,有了这个缓冲区我们客户端和服务端的输出流和输入流就不用直连了,之前是输出流和输入流都是单向的流(单向io),但是nio里面的buffer可以当做双向流的既可以写数据有可以读数据.同时nio和引入了管道的概念channel,和选择器也叫多路复用器Selector.相比传统的bio的建立连接的方式,nio的练级方式是在原有socket的基础上进行了封装和加强,通过管道注册的方式去建立通讯,首先我们的服务端的socketChanenl(我们称之为通讯管道)注册到我们的多路复用器上,然后客户端的通讯管道也注册到我们的多路复用器上,区别是服务端的管道状态是堵塞状态,而客户端的管道是可读状态,在这里补充一下管道的状态有四种,分别是连接状态(Connect),堵塞状态(Accecp),可读状态(Read),可写状态(Write),连接状态就是管道刚刚连接,堵塞就是一直堵塞(多半用于服务端管道),可读状态就是该管道可以读取数据,可写状态是该管道可以写入数据.那刚刚说到服务端管道一直堵塞在这里,然后服务端会起一个线程去轮询注册器也就是多路复用器上已经注册并且处于连接状态的客户端socketChannel,并和他简历管道通讯(注意这里不是socket直连)而是通过管道和缓冲区做通讯交互.在根据通道的状态变化去执行相应的操作,顺带说明一下管道注册其实并不是吧管道本身注册在多路复用器上,而是通过selectedKey去注册的,可以理解为selectedKeys是唯一识别指定管道的标识列,同样Selector咋轮询获取的也不是管道本身,而是获取的一组管道的key,然后建立通讯的时候通过key获取该管道,再在次深挖一下每个socketChinnel底层必然对应一个socket实例,获取该管道本身以后然后就开始根据管道的状态执行对应的操作,这样就达到了通讯。讲完了nio这里就简单的对应一下之前的bio有哪些优势和好处,最明显的就是传统的bio, 是一个服务端socket和一个客户端socket建立直连,并且服务端需要为每个客户端新起一个线程去处理客户端的通讯交互,这样必然会无故的开销服务端的很多资源.而我们的nio只需要一个选择器和一个轮询线程就能接入成千上万甚至更多的客户端连接,这点是nio相比bio最大的进步和改变.其次就是建立连接的方式,传统的bio是通过客户端服务端三次握手的方式建立tcp连接,而nio是客户端直接把通道注册到服务端的多路复用器上,然后服务端去轮询,这就减少了三次握手请求响应的开销。再次之就是缓冲区代码直连流,传统的bio请求和响应数据读是通过一端创建输出流直接向另一端输出,而另一点穿件输入流写入数据,这样就很依赖网络,如果网络不好就会导致流长时间不能关闭,从而导致资源无故浪费,增加开销.而nio引入了缓冲区都数据写数据都是直接向缓冲区读写,这样就不依赖网络,一端吧数据写完到缓冲区就可以关闭写入流,这时候只需要通知另一端去读。另一端开启读取流快速的读取缓冲区的数据,然后就可以快速的关闭.如果网络不好情况向就不会开销另一端的资源。

如图所示:


nio代码实现

首先我们的nio主要就通过多路复用器来对客户的管道请求进行注册然后服务端进行轮询

package liuzw.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class Server implements Runnable{
	//1 多路复用器(管理所有的通道)
	private Selector seletor;
	//2 建立缓冲区
	private ByteBuffer readBuf = ByteBuffer.allocate(1024);
	//3 
	private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
	public Server(int port){
		try {
			//1 打开路复用器
			this.seletor = Selector.open();
			//2 打开服务器通道
			ServerSocketChannel ssc = ServerSocketChannel.open();
			//3 设置服务器通道为非阻塞模式
			ssc.configureBlocking(false);
			//4 绑定地址
			ssc.bind(new InetSocketAddress(port));
			//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
			ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
			System.out.println("Server start, port :" + port);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void run() {
		while(true){
			try {
				//1 必须要让多路复用器开始监听
				this.seletor.select();
				//2 返回多路复用器已经选择的结果集
				Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
				//3 进行遍历
				while(keys.hasNext()){
					//4 获取一个选择的元素
					SelectionKey key = keys.next();
					//5 直接从容器中移除就可以了
					keys.remove();
					//6 如果是有效的
					if(key.isValid()){
						//7 如果为阻塞状态
						if(key.isAcceptable()){
							this.accept(key);
						}
						//8 如果为可读状态
						if(key.isReadable()){
							this.read(key);
						}
						//9 写数据
						if(key.isWritable()){
							//this.write(key); //ssc
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	private void write(SelectionKey key){
		//ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
		//ssc.register(this.seletor, SelectionKey.OP_WRITE);
	}
	private void read(SelectionKey key) {
		try {
			//1 清空缓冲区旧的数据
			this.readBuf.clear();
			//2 获取之前注册的socket通道对象
			SocketChannel sc = (SocketChannel) key.channel();
			//3 读取数据
			int count = sc.read(this.readBuf);
			//4 如果没有数据
			if(count == -1){
				key.channel().close();
				key.cancel();
				return;
			}
			//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
			this.readBuf.flip();
			//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
			byte[] bytes = new byte[this.readBuf.remaining()];
			//7 接收缓冲区数据
			this.readBuf.get(bytes);
			//8 打印结果
			String body = new String(bytes).trim();
			System.out.println("Server : " + body);
			// 9..可以写回给客户端数据 
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void accept(SelectionKey key) {
		try {
			//1 获取服务通道
			ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
			//2 执行阻塞方法
			SocketChannel sc = ssc.accept();
			//3 设置阻塞模式
			sc.configureBlocking(false);
			//4 注册到多路复用器上,并设置读取标识
			sc.register(this.seletor, SelectionKey.OP_READ);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	public static void main(String[] args) {
		new Thread(new Server(8765)).start();;
	}
}
客户端代码:

package bhz.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
	//需要一个Selector 
	public static void main(String[] args) {
		//创建连接的地址
		InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
		//声明连接通道
		SocketChannel sc = null;
		//建立缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		try {
			//打开通道
			sc = SocketChannel.open();
			//进行连接
			sc.connect(address);
			while(true){
				//定义一个字节数组,然后使用系统录入功能:
				byte[] bytes = new byte[1024];
				System.in.read(bytes);
				//把数据放到缓冲区中
				buf.put(bytes);
				//对缓冲区进行复位
				buf.flip();
				//写出数据
				sc.write(buf);
				//清空缓冲区数据
				buf.clear();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(sc != null){
				try {
					sc.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}



  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值