手写JAVA NIO实现Socket通信及其过程中注意的问题

           当然现在不需要自己手写NIO实现socket,都是在需要建立TCP/IP连接的程序中直接使用mina框架,或者netty框架, 后者使用的更多。趁着在用mina框架解析网关协议之际,本文手写NIO实现socket的服务端,找一找学习NIO中遇到的问题,以及在调试的过程中学习对某些API的理解,文中只写了服务端,客户端用SocketTools这个工具充当,测试。

服务端代码实现:(运行起来只需要导入log4j和slf4j的jar包,看启动日志需要加上log4j.properties)

本来是同一个文件的东西,我也不知道系统为什么给自动分到两个部分去了

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;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 
 * @author yanzh
 *
 */
public class NIOServer {

	private Logger LOG = LoggerFactory.getLogger(NIOServer.class);
	private ServerSocketChannel server;
	private ByteBuffer sendBuffer;
	private ByteBuffer recvBuffer;
	private Selector selector;
	private int port = 9012;
	//初始化服务器
	NIOServer(int port){
		this.port = port;
	  try{
		recvBuffer = ByteBuffer.allocate(1024);	
		sendBuffer = ByteBuffer.allocate(1024);
		server = ServerSocketChannel.open();
		server.socket().bind(new InetSocketAddress(port));
		server.configureBlocking(false);
		selector = Selector.open();
		server.register(selector, SelectionKey.OP_ACCEPT);
		LOG.info("服务器已启动,监控端口号:{}", this.port);
		
		 }catch(IOException e){
			LOG.error("连接时出现IO异常");
		 }
		 	
	 }
	
	   NIOServer(){
		   this(9012);
	   }	
	 //初始化,监听端口  
	public void start(){
		
	  try {
		  
	     	listener();
	     	
	      } catch (IOException e) {
	    	  
		    LOG.info("监听端口IO异常");
	      }
	  
    }
	//一个死循环一直在监听,处理端口事件
	public void listener() throws IOException{
		while(true){
			LOG.info("-----------------------------------------------------");
			LOG.info("1.selectedKeys的值:{}", selector.selectedKeys().size());
			LOG.info("1.registe的值:{}", selector.keys().size());
			int n = selector.select();
			LOG.info("----------------------fengexian1---------------------");
			LOG.info("2.select返回值:{}", n);
			LOG.info("2.selectedKeys的值:{}", selector.selectedKeys().size());
			LOG.info("2.registe的值:{}", selector.keys().size());
			LOG.info("-------------------------------------------------------");
			//没有准备好的通道,其实我觉得根本不会到这里,因为如果没有通道准备好,
			//应该select函数一直阻塞着。
			if(n == 0){
				continue;
			}
			Set eventKeys = selector.selectedKeys();
			Iterator it = eventKeys.iterator();
			while(it.hasNext()){
				SelectionKey eventKey = it.next();
				it.remove();
				//准备好的通道中取得了通道和选择器的对应关系,利用此关系可以得到通道或者选择器。
				//开始具体处理通道相关内容,连接,读,写等;
				handleKey(eventKey);
			}
	   }
	}
	//处理IO口连接,读写等函数
	public void handleKey(SelectionKey eventKey) throws IOException{
		if(eventKey.isAcceptable()){
			SocketChannel sc = server.accept();
	        LOG.info("新的客户端已经连接成功");
	        sc.configureBlocking(false);
	        sc.register(selector, SelectionKey.OP_READ);
		}
		
		if(eventKey.isReadable()){
			SocketChannel sc = (SocketChannel)eventKey.channel();
			String content = "";
			int n;
			recvBuffer.clear();
			try{
			while((n = sc.read(recvBuffer)) > 0){
				    
			      	content = content + new String(recvBuffer.array(), 0, n);
			     }
			}catch(IOException e){
				eventKey.cancel();
				sc.close();
				return;
			}
			if(n == -1){
				SocketChannel scc = (SocketChannel)eventKey.channel();
				eventKey.channel().close();
				eventKey.cancel();
				LOG.info("客户端{}已经关闭。", scc.socket().getRemoteSocketAddress());
				return;
			}
			LOG.info("receive client input Stirng : {}", content);
			//content = "yanzh";
			if(content.length() > 0){
				sendBuffer.clear();
				sendBuffer.put(content.getBytes());
				sendBuffer.flip();
				sc.write(sendBuffer);								
			}
			//sc.configureBlocking(false);
			//sc.register(selector, SelectionKey.OP_WRITE);
			//eventKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
		}
		if(eventKey.isWritable()){
			LOG.info("sendBuffer可写");
			SocketChannel sc = (SocketChannel)eventKey.channel();
			if(sendBuffer.remaining()>0){
			    sc.write(sendBuffer);
			    LOG.info("sendBuffer剩余大小:{}", sendBuffer.remaining());
			}
		}
		
		
	}
        //主函数启动服务
	public static void main(String[] args) {
		new NIOServer().start();
	}
	
}


       学习的过程中对一些概念的理解、常见的问题以及网上一些相关NIO程序中的问题总结:

       1. Selector的select方法在没有准备好的IO操作时,一直处于阻塞状态,直到有可操作的IO准备好。这里的准备好不是指有通道注册在Selector上,而是指在所注册的通道里已经有了相关请求(例如有客户端的连接,客户端输入了数据等)。

       2. 一旦通道注册在Selector上,就是一直注册在其上,除非关闭此通道(调用channe的close方法)。每次select方法返回大于0后,会调用selectionKeys()方法找出所有准备好的选择键,对SelectionKeys的遍历过程中,必须删除SelectionKeys集合中单个SelectionKey,此处与通道的注册毫无关系,注册在通道上的channel仍旧在通道上,删除的只是当前select大于0的单次的准备好的SelectionKey.(显然对SelectionKey的处理是以select返回为周期的,返回一次处理一次,而且每次处理必须把返回的SelectionKey清理干净(放在一个Set中的,由selectionKeys()方法返回))。

       3. SelectionKey的interestOps()只是改变已经注册在Selector上那个的通道感兴趣的事件,覆盖原来调用register时注册的感兴趣事件,这里并不是新增注册或者其他,仅仅是对原有状态的改变(疯狂java讲义中,NIO实现非阻塞socket通信中对此方法的使用显然是多余的,不用使用此方法设置成准备下次连接/读 ,照样是可以被连接或者读的,因为本来注册的时候就是对读/连接的事件感兴趣)。

       4.网上有些程序例子在调用完isReadable()方法后,将SelectionKey取出的channel注册为可写register(selector,OP_WRITE),显然这样处理以后,对所注册通道感兴趣事件修改为 了可写,不可以与当前客户端进行下次读了。此通道先前是注册的可读,现在被修改为了可写,是同一个通道,如果正常操作,此处注册为可读|可写,当然调用interestOps修改也是可以的,和注册是同一个意思(因为是一个已经注册的通道)

       5. 一旦注册了可写通道,select方法返回值永远大于0,因为至少有一个可写的通道注册在上面,所以一直陷入了IsWritable的死循环中,其实也不能叫死循环,其他请求来照样可以处理,只是每次必然会走这个判断,下同

       6.不对socket连接断开的情况做处理的话,一旦连接服务器的客户端TCP/IP连接断开,会使得服务器select方法一直返回值大于0,因为里面一直有一个可读的请求,这样每次都会进入isReadable的判断里,陷入一个可读的死循环。具体需要处理断开的方法是对这个读请求捕获,捕获到以后将此通道关闭。(判断Socketchannel的read方法返回值,如果是-1,表示已经断开,取消当前SelectionKey  .cancel,关闭当前通道  .close())



你可能感兴趣的:(计算机网络(TCP/IP),Java基础)