基于NIO的socket通信

(不建议初次接触nio的socket的通信的读者观看,建议对nio的socket的通信有一定了解之后,再阅读)
普通的socket通信是:ServerSocket 和Socket
NIO的Socket是:ServerSocketChannel和SocketChannel
我们知道ServerSocket 和 Socket是阻塞的
而ServerSocketChannel 和 SocketChannel 是否阻塞可设置的。
------ 通过configureBlocking(boolean) 方法指定 ----
如果不借助于Selector

ServerSocketChannel 和 SocketChannel 为阻塞时的通信体系

  • 当ServerSocketChannel 和 SocketChannel 为阻塞的时候,其实就相当于普通的socket通信
public class ServerSocketChannelDemo {
   public static Charset charset = Charset.forName("utf-8");
   /**
    * serverSocketChannel 和 socketChannel 都是阻塞式的。
    * 所以 serverSocketChannel 的accept和socketChannel 的read 都会造成阻塞
    * @param args
    */
   public static void main(String[] args) {
   	try {
   		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
   		serverSocketChannel.bind(new InetSocketAddress(30000));
//			serverSocketChannel.configureBlocking(false);
   		
   		SocketChannel socketChannel = serverSocketChannel.accept();
   		ByteBuffer buffer = ByteBuffer.allocate(1024);
   		socketChannel.read(buffer);
   		System.out.println(charset.decode(buffer));
   	} catch (Exception e) {
   		// TODO Auto-generated catch block
   		e.printStackTrace();
   	}
   }
}

public class ClientSocketChannelDemo {
   public static void main(String[] args) throws IOException, Exception {
   	SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",30000));
   	Thread.currentThread().sleep(100000);//不要让客户端关闭了socket连接
   }
}

调试代码,会发现 serverSocketChannel.accept() 和 socketChannel.read(buffer) 都会造成阻塞,与ServerSocket 和Socket的组合构建的通信系统差不多

ServerSocketChannel 和 SocketChannel 为非阻塞通信体系

  • 当ServerSocketChannel 和 SocketChannel 设置为非阻塞的时候,
    这种模式的设计,因为采用的是非阻塞式的,所以不管有没有接收到连接或者读取到数据,都会返回,因此要保证读取数据的完整性和准确性,就需要借助循环和线程休眠的方式进行设计。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentLinkedQueue;

// 由于是非阻塞的,所以可以引入线程池,阻塞式的不能采用线程池
public class NonBlockingSocketChannelServer {

	public static volatile Map keys = Collections.synchronizedMap(new HashMap());//考虑并发 移除也可以用ConcurrentHashMap
	static ConcurrentLinkedQueue msgQueue = new ConcurrentLinkedQueue();
	static Charset charset = Charset.forName("utf-8");
	
	
	public static void main(String[] args) {
		AcceptSocketThread acceptSocketThread = new AcceptSocketThread();
		acceptSocketThread.start();
		
		ReadMsgThread readMsgThread = new ReadMsgThread();
		readMsgThread.start();
		
		ConsumerMsgThread consumerMsgThread = new ConsumerMsgThread();
		consumerMsgThread.start();
	}
	
	static class AcceptSocketThread extends Thread {
		volatile boolean runningFlag = true;
		
		public void run(){
			try {
				ServerSocketChannel serverChannel = ServerSocketChannel.open();
				serverChannel.bind(new InetSocketAddress(30000));
				serverChannel.configureBlocking(false);
				
				while(runningFlag){
					SocketChannel channel = serverChannel.accept();
					
					if(null == channel){
						System.out.println("服务端监听中.....");
						Thread.currentThread().sleep(1000);
					}else{
						channel.configureBlocking(false);
						System.out.println("一个客户端上线,占用端口 :" + channel.socket().getPort());
						keys.put(channel.socket().getPort(), channel);
					}
				}
			} catch (IOException | InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * 由于读是非阻塞式的,所以没必要一个socketChannel一个线程
	 * 也可以通过线程池来执行,此处只做实例,学习,不扩展
	 * @author liangpro
	 */
	static class ReadMsgThread extends Thread{
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		public void run(){
			try {
				int num = 0;
				for (;;) {
					Iterator ite = keys.keySet().iterator();
					while (ite.hasNext()) {
						int key = ite.next();
						StringBuffer stb = new StringBuffer();
						try{
							while((num = keys.get(key).read(buffer)) > 0 ){
								buffer.flip();
								stb.append(charset.decode(buffer).toString());
								buffer.clear();
							}
							if(stb.length() > 0){
								MsgWrapper msg = new MsgWrapper();
								msg.key = key;
								msg.msg = stb.toString();
								System.out.println("端口:" + msg.key + "的通道,读取到的数据" + msg.msg);
								msgQueue.add(msg);
							}
						}catch(Exception e){
							System.out.println("error: 端口占用为:" + keys.get(key).socket().getPort() + ",的连接的客户端下线了");
							ite.remove();
							e.printStackTrace();//这只是个输出语句
						}
					}
					sleep(1000);
					System.out.println("读取线程监听中......");
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		
		
	}
	
	static class ConsumerMsgThread extends Thread{
		public volatile boolean isRunningFlag = false;
		
		public void run (){
			isRunningFlag = true;
			try {
//				sleep(10000);
				while (isRunningFlag) {
					MsgWrapper msg = msgQueue.poll();
					for (; null != msg; msg = msgQueue.poll()) {
						SocketChannel channel = keys.get(msg.key);
						channel.write(charset.encode("response:" + msg.msg));
					}
					sleep(1000);
					System.out.println("响应线程响应中......");
				}
			} catch (IOException | InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	static class MsgWrapper {
		public int key;
		public String msg;
	}
}

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

public class NonBlockingSocketChannelClient {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			
			
			new Thread(){
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				
				public void run(){
					
					try {
						SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",30000));
						socketChannel.configureBlocking(false);
						socketChannel.write(NonBlockingSocketChannelServer.charset.encode(socketChannel.socket().getPort() + ": send message"));
						int num = 0;
						StringBuffer stb = new StringBuffer();
						for (;;) {
							while(socketChannel.read(buffer) > 0){
								buffer.flip();
								stb.append(NonBlockingSocketChannelServer.charset.decode(buffer));
								buffer.clear();
							}
							if(stb.length()>0){
								break;
							}
							sleep(1000);
						}
						System.out.println(stb);
					} catch (IOException | InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println("over!");
				}
			}.start();
		}
	}
}

**** 程序中还有一个问题
就是服务端怎么知道客户端是否还存在,如果客户端直接把进程杀了,服务端怎样保证正常运行。

  1. 通过socket类的方法isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()等,这些方法都是本地端的状态,无法判断远端是否已经断开连接。
  2. 通过OutputStream发送心跳消息,如果发送失败就表示远端已经断开连接,类似ping,远端需把正常数据和心跳信息分开。
  3. 通过socket的InputStream.read返回-1/0表示 对方关闭连接,抛出异常表示对方异常终止连接。
  4. 方法sendUrgentData,往输出流发送一个字节的数据,只要对方Socket的SO_OOBINLINE属性没有打开,就会自动舍弃这个字节,SO_OOBINLINE属性默认情况下就是关闭的!

下面一段代码就可以判断远端是否断开了连接:

try{
socket.sendUrgentData(0xFF);
}catch(Exception ex){
reconnect();//客户端要是断了,服务端抛异常,
}

  1. 其实在通过socket.getoutstream和socket.getinputstream流对客户端发送、接受信息时如果socket没连接上是会抛出异常的,这也就是为什么Java会要求网络编程都要写在try里面,所以只要在catch里面写入客户端退出的处理就行了
  • 对于非阻塞式的ServerSocketChannel 和 SocketChannel 使用起来有一个难题不好处理,那就是什么时候接收连接,什么时候接收数据,什么时候读取数据,不知道就只能一直循环遍历。
    因此引入了Selector 负责监听通道的接收、读、写事件。

基于Selector的nio通信体系

基于NIO的socket通信_第1张图片

示例代码github超链接:

设计思路

  1. 打开一个Selector,专门用于监听ServerSocketChannel,(之所以启用专门的Selector负责监听ServerSocketChannel,是因为ServerSocketChannel只有一个,在Selector中只注册了一个勾子,所以一次select()方法,只能接收一个客户端的连接,如果跟SocketChannel的读写监听注册同一个Selector,当读写比较耗时的时候,由于每次select()只能新增一个客户端,所以并发比较大的时候,会导致连接不及时。)
  2. 监听到的连接存储到队列中(而不是直接注册到Selector2中,因为注册连接,与Seletor的监听不能并发进行,注意Selector.select()方法是会对 selector对象加锁。)
  3. ServerReadWrite线程负责监听Selecor2中注册的 SocketChannel,将需要读的channel交由ReadMsgThread线程池处理,将可以写的SocketChannel交由WriteMsgThread处理(注意,通道非可读状态的时候,就是可写状态,所以注意控制循环,不要让循环太快)
  4. 交由Selector2监听负责控制通道的写操作,而不是交由ConsumerMsgThread随机并发的写,第一:对于单个通道来说,写操作是非并发的,第二:可以有效防止拥堵,阻塞,避免多个线程写同一个通道,而其它的通道空闲,避免同一通道既在读取数据,又在写数据,导致写数据阻塞

你可能感兴趣的:(JAVA)