NIO(二)--CS架构的非阻塞通信

来源:互联网 发布:怎么看天猫店铺数据 编辑:程序博客网 时间:2024/06/01 10:22
                                                                           Socket服务器编程范例
1. 阻塞服务器 

 (1)方法 : 用主线程处理接受客户端连接, 在主线程中开启线程池, 用池子中的每个线程处理客户端请求
 (2)弊端 : 
   1)JVM为每个线程分配自己独立的堆栈空间 (每个线程中变量的值是主线程变量值的拷贝), 导致线程间同步过程变得复杂(eg:valitile), 也会增加
      死锁的可能性, 增加JVM进行线程调度的开销
   2)服务器在处理客户端请求时, 会惊醒大量的I/O操作. 而I/O操作不需CPU进行控制, 且会使处理线程阻塞, JVM会释放阻塞线程的CPU时间片给
      其他非阻塞的线程, 这些CPU时间片的轮转会降低响应效率 (所以处理线程并非越多越好)
 (3)步骤 : 
   1) ServerSocketChannel.open()打开服务器的nio
   2) 设置服务器段的socket地址重用
   3) 把serverSocketChannel的socket绑定到本地的一个端口上
   4) while(true)循环内, 用ServerSocketChannel用阻塞的accept()方法, 接收客户端连接, 每接收一个客户端连接, 就用线程池中的线程处理请求
public class EchoServerBlock {private ServerSocketChannel serverSocketChannel = null;//nio获取服务器端口通道private ExecutorService executorService = null;//线程池处理客户端请求//构造函数实例化服务器端口 + 线程池public EchoServerBlock() throws IOException {serverSocketChannel = ServerSocketChannel.open();/* serverSocketChannel.socket()获取服务器打开的socket, Socket的setReuseAddress(true)让服务器可以快速重连*/serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.socket().bind(new InetSocketAddress(12121)); //绑定serverSocketChannel到本机的端口int num = Runtime.getRuntime().availableProcessors()*4;//让每个核心运行4个线程executorService = Executors.newFixedThreadPool(num);System.out.println("阻塞版服务器启动 . . .");}//客户端请求加入线程池处理public void service(){while(true){try {//SocketChannel获取客户端的socket流, accept()方法是阻塞的, 没有请求的时候会阻塞SocketChannel socketChannel = this.serverSocketChannel.accept();executorService.execute(new Handler(socketChannel));} catch (IOException e) {e.printStackTrace();}}}class Handler implements Runnable{private SocketChannel socketChannel;private Handler(SocketChannel socketChannel){this.socketChannel = socketChannel;}public void run() {handle(socketChannel);}public void handle(SocketChannel socketChannel){Socket socket = socketChannel.socket();//阻塞模式从nio中获取socket与serversocketSystem.out.println("接收客户端"+socket.getInetAddress()+":"+socket.getPort());try {InputStream in = socket.getInputStream();OutputStream out = socket.getOutputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(in,"utf-8"));PrintWriter writer = new PrintWriter(new OutputStreamWriter(out,"utf-8"), true);//自动flushString msg = null;while((msg=reader.readLine())!=null){System.out.println("客户端发来:"+msg);writer.println("echo:"+msg);//把数据发回客户端if(msg.equals("bye")){break;}}} catch (IOException e) {e.printStackTrace();}finally{try {if(socketChannel!=null)  socketChannel.close();System.exit(0);} catch (IOException e) {e.printStackTrace();}}}}public static void main(String[] args) throws IOException {new EchoServerBlock().service();}}
2. 非阻塞服务器
 (1)方法 : 
    1) 先在Selector中注册SelectionKey.OP_ACCEPT监听事件
    2) 当accept事件发生 ,再注册读写事件
    3) 读事件 : 非阻塞模式遵循能读多少读多少, 所以一次读并非用户的一行数据, 先用一个readbuffer接受传来的数据, 在复制到整个大Buffer
         写事件 : 写之前先确定大Buffer中已经有了一行数据(截取"\r\n")
                        非阻塞模式下的写遵循能写多少写多少, 所以先把"echo"+这行数据复制到一个outputBuffer , 用while(hasRemaining)循环写
                        接着删除大Buffer中已经发送的数据:用tempBuffer存储这行数据(不带echo), 在把大Buffer的position设为tempBuffer的limit, 再把大Buffer, compact()删                                             除了已发送的数据
 (2)Selector的select()方法是阻塞方法,知道阻塞的事件到来, 返回新发生的注册事件个数 (循环打印一次,可减小到0)
     Selector的select(long timeout)方法是阻塞方法, 可被Selector的wakeip()方法唤醒
     Selectionkey的channel()方法, 返回SelectableChannel
 (3)SocketChannel的read(Buffer b),write(Buffer b)都会使Buffer的positon变为原先位置+读/写的数据, 但是Buffer的limit不变
     int  read(Buffer b) -- 返回从channel中读到的字节数, 返回-1表示读到channel的末尾
                                         阻塞版的read方法会去读满Buffer中的剩余容量(buffer. remaining()的数量), 若未读满且未达到channel的末尾, 就会阻塞
                                         非阻塞版的read方法奉行有多少读多少, 即使未读满buffer, 也立即返回
     int write(Buffer b) -- 返回写出的字节数
                                         阻塞版的write()方法一次输出buffer的全部数据, 当底层soket输出缓冲区不能容纳buffer从position到limit的数据时, 就会阻塞
                                        非阻塞版的write()能帮读多少数据就读多少数据, 会改变positon的值, 使得可以继续从该位置读
 (4)key.isAcceptable()为true, key.channel()得到的是服务器端的ServerSocket 
     key.isReadable()与key.isWritable()为true, 得到的是客户端socket
    //主线程同时处理客户端tcp连接与数据处理public class EchoServerNoBlock {private ServerSocketChannel serverSocketChannel = null;private Selector selector;private Charset charSet = Charset.forName("utf-8");public EchoServerNoBlock() throws IOException {selector = Selector.open();this.serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().setReuseAddress(true);//服务器可立即重启serverSocketChannel.configureBlocking(false);//服务器配置成非阻塞模式serverSocketChannel.socket().bind(new InetSocketAddress(12121)); //地址一旦绑定,服务器开启System.out.println("服务器开启. . . ");}public void service() throws IOException {//服务器端注册监听的事件this.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//System.out.println(selector.select()); //打印1  //System.out.println(selector.select()); // 打印0 所以不能直接打印发生事件的个数while(selector.select()>0){  // selector.select():返回发生的事件个数Set<SelectionKey> readyKeys = selector.selectedKeys(); //该方法返回的集合是"单例的,公用的",下次循环得到的还是上次循环的集合for(SelectionKey key:readyKeys){try{    //因为下面取客户端时serverSocketChannel.accept()在非阻塞模式下会立刻返回,如果没有客户端连接就返回null     //并且serverSocketChannel.accept()总是返回新连接的客户端,已经接收的客户端就不去返回readyKeys.remove(key);  //集合中必须删除处理过的事件,否则在下次循环中还需处理一遍if(key.isAcceptable()){ //连接事件触发ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);Socket socket = socketChannel.socket();System.out.println("请求来自" + socket.getInetAddress() + ":" + socket.getPort());ByteBuffer buffer = ByteBuffer.allocate(1024);//存放读来的数据socketChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE , buffer);}if(key.isReadable()){ //服务器读事件触发recieve(key);}if(key.isWritable()){  //服务器写事件触发  (客户端不发来信息,会在此处无限循环)send(key);}}catch(IOException e){e.printStackTrace();try{if(key != null){key.cancel();key.channel().close();}}catch(IOException e1){e1.printStackTrace();}}}}}private void send(SelectionKey key) throws IOException {//从大Buffer中取得一行string,编码后放到发送BufferByteBuffer buffer = (ByteBuffer)key.attachment();SocketChannel socketChannel = (SocketChannel) key.channel();buffer.flip();CharBuffer charBuffer = charSet.decode(buffer);String data = charBuffer.toString();if(data.indexOf("\r\n")==-1) // 若此时的Buffer中不是一整行数据,就返回,先不去写return;data = data.substring(0,data.indexOf("\n")+1);System.out.print(""+data);ByteBuffer outputBuffer = charSet.encode("echo:"+data);while(outputBuffer.hasRemaining()){socketChannel.write(outputBuffer);}//发送后在大Buffer中删除已发送完毕的数据 : 大Buffer中前面的data已被发送,先致positionByteBuffer compareBuffer = charSet.encode(data);buffer.position(compareBuffer.limit());buffer.compact();//compact会把position前面的数据清除,并把后面的数据从Buffer的0位置放置//最后发现bye结束,则释放key,关闭channelif(data.equals("bye\r\n")){key.cancel();socketChannel.close();System.out.println("客户端退出:"+socketChannel.socket().getInetAddress());}}private void recieve(SelectionKey key) throws IOException {//从key里面拿到连接的socketChannel和bufferByteBuffer buffer = (ByteBuffer)key.attachment();SocketChannel socketChannel = (SocketChannel)key.channel();//非阻塞模式下,read(Buffer)方法读到多少数据是不确定的,不一定是一行, 所以在写事件中,应该截取读buffer中的一行数据ByteBuffer readBuffer = ByteBuffer.allocate(64); //假设一次read不会导致buffer溢出socketChannel.read(readBuffer);readBuffer.flip();  //一次read完毕后,把readBuffer中的数据复制到大Buffer中buffer.limit(buffer.capacity());buffer.put(readBuffer);}public static void main(String[] args) throws IOException {new EchoServerNoBlock().service();}}
3. 阻塞与非阻塞混合服务器
  (1) 方法 : 当客户端请求数量变大时, 可以用2个线程完成服务
     1)用阻塞模式接受连接请求
     2)用费阻塞模式完成业务处理
  (2)产生的同步问题 : 
      Selector.select()一开始处于阻塞状态,当有连接时唤醒,同时防止socketChannel.register()和selector.select()一起阻塞
public class EchoServerMix {private ServerSocketChannel serversoketChannel = null;private Selector selector = null;private Charset charSet = Charset.forName("utf-8");private Object obj = new Object();public EchoServerMix() throws IOException{this.selector = Selector.open();this.serversoketChannel = ServerSocketChannel.open();this.serversoketChannel.configureBlocking(true);this.serversoketChannel.socket().setReuseAddress(true);this.serversoketChannel.socket().bind(new InetSocketAddress(12121));System.out.println("服务器启动. . .");}public void accept(){while(true){try {SocketChannel socketChannel = this.serversoketChannel.accept(); //阻塞socketChannel.configureBlocking(false); //非阻塞Socket socket = socketChannel.socket();System.out.println("请求来自" + socket.getInetAddress() + ":" + socket.getPort());ByteBuffer buffer = ByteBuffer.allocate(1024);synchronized (obj) {selector.wakeup(); // 业务处理的Selector.select()一开始处于阻塞状态,有连接时唤醒,同时防止socketChannel.register()                                                            // 和selector.select()一起阻塞socketChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE,buffer);;}} catch (IOException e) { e.printStackTrace(); }}}private void service() throws IOException{while(true){synchronized (obj) {}; //里面可以不放任何代码int i = selector.select();if(i==0) continue;   //没有事件发生就继续循环Set<SelectionKey> readyKeys = selector.selectedKeys();for(SelectionKey key:readyKeys){readyKeys.remove(key);if(key.isReadable()){ recieve(key);}if(key.isWritable()){send(key);}}}}private void send(SelectionKey key) throws IOException {//从大Buffer中取得一行string,编码后放到发送BufferByteBuffer buffer = (ByteBuffer)key.attachment();SocketChannel socketChannel = (SocketChannel) key.channel();buffer.flip();CharBuffer charBuffer = charSet.decode(buffer);String data = charBuffer.toString();if(data.indexOf("\r\n")==-1) // 若此时的Buffer中不是一整行数据,就返回,先不去写return;data = data.substring(0,data.indexOf("\n")+1);System.out.print(""+data);ByteBuffer outputBuffer = charSet.encode("echo:"+data);while(outputBuffer.hasRemaining()){socketChannel.write(outputBuffer);}//发送后在大Buffer中删除已发送完毕的数据 : 大Buffer中前面的data已被发送,先致positionByteBuffer compareBuffer = charSet.encode(data);buffer.position(compareBuffer.limit());buffer.compact();//compact会把position前面的数据清除,并把后面的数据从Buffer的0位置放置//最后发现bye结束,则释放key,关闭channelif(data.equals("bye\r\n")){key.cancel();socketChannel.close();System.out.println("客户端退出:"+socketChannel.socket().getInetAddress());}}private void recieve(SelectionKey key) throws IOException {//从key里面拿到连接的socketChannel和bufferByteBuffer buffer = (ByteBuffer)key.attachment();SocketChannel socketChannel = (SocketChannel)key.channel();//非阻塞模式下,read(Buffer)方法读到多少数据是不确定的,不一定是一行, 所以在写事件中,应该截取读buffer中的一行数据ByteBuffer readBuffer = ByteBuffer.allocate(64); //假设一次read不会导致buffer溢出socketChannel.read(readBuffer);readBuffer.flip();  //一次read完毕后,把readBuffer中的数据复制到大Buffer中buffer.limit(buffer.capacity());buffer.put(readBuffer);}public static void main(String[] args) throws IOException {final EchoServerMix server  = new EchoServerMix();new Thread(){@Overridepublic void run() {server.accept();}}.start();server.service();}}
客户端 : 
 (1)阻塞版客户端 : 用户输入一句话, 接收到返回的echo字符串后才能输入下一句话
public class EchoClientBlock {private SocketChannel socketChannel = null;public EchoClientBlock() throws IOException{this.socketChannel = SocketChannel.open();InetSocketAddress serverAddress = new InetSocketAddress(12121);socketChannel.connect(serverAddress);//客户端链接服务器System.out.println("与服务器建立连接");}public void talk(){Socket server = this.socketChannel.socket();try{BufferedReader reader = new BufferedReader(new InputStreamReader(System.in,"utf-8"));OutputStream out = server.getOutputStream();InputStream in = server.getInputStream();BufferedReader sreader = new BufferedReader(new InputStreamReader(in,"utf-8"));PrintWriter swriter = new PrintWriter(new OutputStreamWriter(out,"utf-8"), true);//自动flushString msg = null;while((msg=reader.readLine())!=null){swriter.println(msg);System.out.println(sreader.readLine());if(msg.equals("bye")){break;}}}catch(IOException e){e.printStackTrace();}finally{try {socketChannel.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) throws IOException {new EchoClientBlock().talk();}}

 (2)非阻塞版客户端 :  
     1)准备sendBuffer和recieveBuffer, sendBuffer负责把用户输入的数据存储并发往服务器,recieveBuffer负责接收服务器发来的echo字符串
     2) 用阻塞的SocketChannel连接服务器, 之后设置为非阻塞形式来发送接受字符串
     3) 接受键盘输入和发送字符串为2个线程
public class EchoClientNoBlock {private SocketChannel socketaChannel = null;private Charset charset = Charset.forName("utf-8");private Selector selector = null;private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);//发往服务器private ByteBuffer recieveBuffer = ByteBuffer.allocate(1024);//接受服务器发来的数据public EchoClientNoBlock() throws IOException{this.socketaChannel = SocketChannel.open();this.socketaChannel.connect(new InetSocketAddress(12121));  //阻塞连接this.socketaChannel.configureBlocking(false);this.selector = Selector.open();System.out.println("服务器连接成功");}public void recieveFromUser() throws IOException{BufferedReader reader = new BufferedReader(new InputStreamReader(System.in,"utf-8"));String msg = "";while(true){msg = reader.readLine();synchronized (sendBuffer) {this.sendBuffer.put(this.charset.encode(msg+"\r\n")); //encode("str")返回ByteBuffer}if(msg.equals("bye"))break;}}public void sendAndRecieve() throws IOException {this.socketaChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);while(selector.select()>0){Set<SelectionKey> readyKeys = this.selector.selectedKeys();for(SelectionKey key:readyKeys){readyKeys.remove(key);if(key.isReadable())recieve(key);if(key.isWritable())send(key);}}}private void send(SelectionKey key) throws IOException {//从sendBuffer里面拿出数据,删除已经发送的数据SocketChannel socketaChannel = (SocketChannel) key.channel();synchronized (sendBuffer) {sendBuffer.flip(); //准备读socketaChannel.write(sendBuffer);sendBuffer.compact(); //删除发送完毕的数据}}private void recieve(SelectionKey key) throws IOException {SocketChannel socketaChannel = (SocketChannel) key.channel();socketaChannel.read(recieveBuffer);recieveBuffer.flip();String recieveStr = this.charset.decode(recieveBuffer).toString();if(recieveStr.indexOf("\n")==-1) return;String outputStr = recieveStr.substring(0,recieveStr.indexOf("\n")+1);System.out.print(outputStr);ByteBuffer temp = this.charset.encode(outputStr); recieveBuffer.position(temp.limit());recieveBuffer.compact();}public static void main(String[] args) throws IOException {final EchoClientNoBlock client = new EchoClientNoBlock();new Thread(){ //先开启接受输入数据的线程,否则阻塞@Overridepublic void run() {try {client.recieveFromUser();} catch (IOException e) {e.printStackTrace();}}}.start();client.sendAndRecieve();}}

0 0