BIO
传统socket编程,服务端代码如下
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8888);
System.out.println("启动服务器....");
while(true){
Socket s = ss.accept(); // 第一步阻塞
System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//读取客户端发送来的消息
String mess = br.readLine(); // 第二步阻塞
System.out.println("客户端:"+mess);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在BIO中,如果不考虑多线程,会有两个地方阻塞:一个接受连接,一个接受数据。如果考虑多线程,资源消耗很大。
改进BIO
如何改进BIO,我们假设在BIO中,在阻塞的两个地方,都让它不阻塞(假设),同时将建立连接的socket存入集合,这样可以通过遍历来判断每个socket的消息发送情况,假设服务端代码如下
public static void main(String[] args) {
List<Socket> list = new ArrayList<>();
try {
ServerSocket ss = new ServerSocket(8888);
System.out.println("启动服务器....");
while(true){
for(Socket s : list){
s.setBlock(false); // 第二步非阻塞
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//读取客户端发送来的消息
String mess = br.readLine();
System.out.println("客户端:"+mess);
}
ss.setBlock(false); // 第一步非阻塞
Socket s = ss.accept();
if(s != null){
list.add(s);
System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
不断轮询Socket,看是否有数据,但是这种模式,是我们自己来控制遍历。如果连接特别多,而大多数仅仅开启了连接,并没有发送数据,会非常浪费cpu资源,但是这也是NIO的基础思想:解决了阻塞问题,统一管理socket,来获取是否有数据交互。
NIO
NIO采用的是单线程Reactor模型,核心就是Selector多路复用器,作用类似于改进BIO中的List,Selector机制有select,poll,epoll,在操作系统内核进行数据判别socket是否有数据,而不需要通过我们自己来控制遍历的socket结果,大大提高了效率。同时Selector可以注册事件,比如接收连接、读取数据等事件,针对不同事件进行单独处理。不管是什么事件,都会触发事件监听函数,通过函数结果来对建立的通道注册不同的事件。代码如下
public class NioServerWork implements Runnable {
//多路复用器 Selector会对注册在其上面的channel进行;轮询,当某个channel发生读写操作时,
//就会处于相应的就绪状态,通过SelectionKey的值急性IO 操作
private Selector selector;//多路复用器
private ServerSocketChannel channel;
private volatile boolean stop;
/**
* @param port
* 构造函数
*/
public NioServerWork(int port) {
try {
selector = Selector.open();//打开多路复用器
channel = ServerSocketChannel.open();//打开socketchannel
channel.configureBlocking(false);//配置通道为非阻塞的状态
channel.socket().bind(new InetSocketAddress(port), 1024);//通道socket绑定地址和端口
channel.register(selector, SelectionKey.OP_ACCEPT);//将通道channel在多路复用器selector上注册为接收操作
System.out.println("NIO 服务启动 端口: "+ port);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void stop(){
this.stop=true;
}
@Override
public void run() {
//线程的Runnable程序
System.out.println("NIO 服务 run()");
while(!stop){
try {
selector.select(1000);//最大阻塞时间1s
//获取多路复用器的事件值SelectionKey,并存放在迭代器中
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
SelectionKey key =null;
//System.out.println("NIO 服务 try");
while(iterator.hasNext()){
System.out.println("NIO 服务 iterator.hasNext()");
key = iterator.next();
iterator.remove();//获取后冲迭代器中删除此值
try {
handleinput(key);//根据SelectionKey的值进行相应的读写操作
} catch (Exception e) {
if(key!=null){
key.cancel();
if(key.channel()!=null)
key.channel().close();
}
}
}
} catch (IOException e) {
System.out.println("NIO 服务 run catch IOException");
e.printStackTrace();
System.exit(1);
}
}
}
/**
* @param key
* @throws IOException
* 根据SelectionKey的值进行相应的读写操作
*/
private void handleinput(SelectionKey key) throws IOException {
System.out.println("NIO 服务 handleinput");
if(key.isValid()){
//判断所传的SelectionKey值是否可用
if(key.isAcceptable()){
//在构造函数中注册的key值为OP_ACCEPT,,在判断是否为接收操作
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();//获取key值所对应的channel
SocketChannel sc = ssc.accept();//设置为接收非阻塞通道
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);//并把这个通道注册为OP_READ
}
if(key.isReadable()){
//判断所传的SelectionKey值是否为OP_READ,通过上面的注册后,经过轮询后就会是此操作
SocketChannel sc = (SocketChannel)key.channel();//获取key对应的channel
ByteBuffer readbuf = ByteBuffer.allocate(1024);
int readbytes = sc.read(readbuf);//从channel中读取byte数据并存放readbuf
if(readbytes > 0){
readbuf.flip();//检测时候为完整的内容,若不是则返回完整的
byte[] bytes = new byte[readbuf.remaining()];
readbuf.get(bytes);
String string = new String(bytes, "UTF-8");//把读取的数据转换成string
System.out.println("服务器接受到命令 :"+ string);
//"查询时间"就是读取的命令,此字符串要与客户端发送的一致,才能获取当前时间,否则就是bad order
String currenttime = "查询时间".equalsIgnoreCase(string) ? new java.util.Date(System.currentTimeMillis()).toString() : "bad order";
dowrite(sc,currenttime);//获取到当前时间后,就需要把当前时间的字符串发送出去
}else if (readbytes < 0){
key.cancel();
sc.close();
}else{
}
}
}
}
/**
* @param sc
* @param currenttime
* @throws IOException
* 服务器的业务操作,将当前时间写到通道内
*/
private void dowrite(SocketChannel sc, String currenttime) throws IOException {
System.out.println("服务器 dowrite currenttime"+ currenttime);
if(currenttime !=null && currenttime.trim().length()>0){
byte[] bytes = currenttime.getBytes();//将当前时间序列化
ByteBuffer writebuf = ByteBuffer.allocate(bytes.length);
writebuf.put(bytes);//将序列化的内容写入分配的内存
writebuf.flip();
sc.write(writebuf); //将此内容写入通道
}
}
NIO的优点上面已经说了,缺点也很明显,作为单线程Reactor模型,虽然不阻塞,但是在处理事件上,也只有一条线程来处理,如果A事件的处理事件很长,会影响B事件的处理
Netty
基于NIO,采用的是主从Reactor多线程模型。对于NIO来说,主从就表示针对建立连接和其他事件分别用两个Selector来处理,同时在从Selector使用多线程来处理事件