NIO客户端和服务器通信代码详细实例(含注释)

NIO核心为Selector、ByteBuffer、Channel,网上资料很多。大家可先查询。

本实例为基于NIO的管理端和客户端一次对话:服务器启服务,客户端发起连接,连接建立后,客户端向服务发送数据,服务器读取数据,服务器读取完成后,服务器向客户端发送回报数据。

服务器:

package com.NIO.niodeeplearning;

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;


/**
 * 服务器
 * @author evan
 *
 */
public class MyNIOServer extends Thread {
	//服务套接字通道
	private ServerSocketChannel serverSocketChannel;
	//服务器所管理的选择器(selector)
	private Selector selector;
	private boolean isRun = true;
	public MyNIOServer() {
		try {
			serverSocketChannel =ServerSocketChannel.open();
			//通道为非阻塞的
			serverSocketChannel.configureBlocking(false);
			//将通道与本地的8888端口绑定,相当于服务端启的是本地的8888端口,客户端来连接。
			//也可用serverSocketChaserverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8888));
			serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8888));
			selector = Selector.open();
			//将通道注册在选择其上,并此通道对什么操作感兴趣(所谓感兴趣就是这个通道用来干嘛),并返回的是此通道的SelectionKey对象
			//SelectionKey.OP_ACCEPT  此通道用来等待客户端的连接
			//SelectionKey.OP_CONNECT;  此通道用来连接服务器
			//SelectionKey.OP_READ      此通道用来读取数据
			//SelectionKey.OP_WRITE     此通道用来写数据
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
			
		} catch (IOException e) {
			System.out.println("服务端初始化失败");
		}
	}
	@Override
	public void run() {
		//死循环来对selector进行轮询操作
		while(isRun)
		{
			
			try {
				//select方法是阻塞的,他会返回I/O已准备好的通道的数量
				
				selector.select();
				//遍历所有通道的selectionKey对象,
				Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
				while(ite.hasNext())
				{
					SelectionKey sk = ite.next();
					handle(sk);
					ite.remove();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	//SelectionKey能获取到相应的通道和通道对读写连接、被连接的兴趣
	private void handle(SelectionKey sk) throws IOException {
		//与客户端建立连接
		if(sk.isValid()&&sk.isAcceptable())
		{
			try {
				//这个channel就是我上面注册的channel,他们的hashcode是一样的
				ServerSocketChannel  serverSocketChannel = (ServerSocketChannel) sk.channel();
				//获取到客户端的通道
				SocketChannel channel = serverSocketChannel.accept();
				//配置通道为非阻塞的
				channel.configureBlocking(false);
				//将通道注册在selector上,并此通道用来读数据
				channel.register(selector, SelectionKey.OP_READ);
			} catch (IOException e) {
				e.printStackTrace();
				System.out.println("与客户端建链失败");
			}
		}
		
		//从通道里读取数据
		if(sk.isValid()&&sk.isReadable())
		{
			try {
				SocketChannel channel = (SocketChannel) sk.channel();
				//分配1024字节大小的字节缓冲区
				ByteBuffer readBuffer = ByteBuffer.allocate(1024);
				//把数据从通道里读入到字节缓冲区,这里假定传输的数据小于1024个字节
				int length = channel.read(readBuffer);
				String messageFromClient = new String(readBuffer.array(),0,length);
				System.out.println("来自客户端的数据:"+messageFromClient);
				//服务器接收完客户端的数据后发送客户端数据进行响应
				channel.register(selector, SelectionKey.OP_WRITE);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		//写数据
		if(sk.isValid()&&sk.isWritable())
		{
			SocketChannel socketChannel = (SocketChannel)sk.channel();
			//wrap把字节数组转为字节缓冲区
			ByteBuffer byteBuffer = ByteBuffer.wrap("您好,客户端,我是服务器".getBytes());
			/*
			 * write()方法的非阻塞调用哦只会写出其能够发送的数据,而不会阻塞等待所有数据,而后一起发送,
			   *    因此在调用write()方法将数据写入信道时,一般要用到while循环
			 */
			while(byteBuffer.hasRemaining())
			{
				try {
					//写数据
					socketChannel.write(byteBuffer);
					System.out.println("服务器写数据完毕");
				} catch (IOException e) {
					e.printStackTrace();
					System.out.println("写数据失败");
				}
			}
			
			/*
			 * 这里进行一次循环即可,所以这里就不再注册到选择器上了,而是取消此通道在此选择器上的注册
			 *  try {
			 * //客户端给服务器发送数据后接收来自服务器的回报,上面迭代里删除了SelectionKey, //所以这里要把通道再次注册再选择器里
			 * socketChannel.register(selector, SelectionKey.OP_READ); } catch
			 * (ClosedChannelException e) { e.printStackTrace(); }
			 */
			socketChannel.close();
		}
	}
	
	public static void main(String[] args) {
		MyNIOServer myNIOServer = new MyNIOServer();
		myNIOServer.start();
	}
	
}

客户端:

package com.NIO.niodeeplearning;

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

/**
 * 客户端
 * @author evan
 *
 */
public class MyNIOClient  extends Thread{
	//客户端的套接字通道
	private SocketChannel channel;
	//客户端管理的选择器
	private Selector selector;
	private boolean isRun = true;
	public MyNIOClient() {
		try {
			//打开通道
			channel = SocketChannel.open();
			//设置通道为非阻塞
			channel.configureBlocking(false);
			//让通道和服务器连接,此方法连接不一定能建立
			channel.connect(new InetSocketAddress("127.0.0.1", 8888));
			
			selector = Selector.open();
			channel.register(selector, SelectionKey.OP_CONNECT);
			
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("客户端初始话失败");
		}
		
	}
	public void run() {
		while(isRun)
		{
			try {
				//一直进行轮询,找出I/O已准备好的通道
				selector.select();
				Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
				while(ite.hasNext())
				{
					SelectionKey stk = ite.next();
					handle(stk);
					ite.remove();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	//根据stk的属性执行相应的操作
	private void handle(SelectionKey stk) 
	{
		/*
		 * 这里为什么还要进行这个操作呢?
		 * 非阻塞SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,
		 * 而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect() 方法来检查是否完成连接,
		 */
		if(stk.isValid() &&stk.isConnectable())
		{
			//
			try {
				SocketChannel socketChannel = (SocketChannel) stk.channel();
				//若通道为非阻塞的,1:连接成功,返回true;2:正在连接返回false 3:连接失败,抛出异常
				//若通道为阻塞的,此方法会阻塞到连接成功或失败,然后返回true或者抛出异常
				if(socketChannel.finishConnect())
				{
					System.out.println("与服务器建链完成");
					socketChannel.register(selector, SelectionKey.OP_WRITE);
				}else {
					System.out.println("正在尝试与服务器连接。。。。。");
				}
			} catch (IOException e) {
				e.printStackTrace();
				System.out.println("连接失败");
			}
		}
		//写数据
		if(stk.isValid()&&stk.isWritable())
		{
			SocketChannel socketChannel = (SocketChannel)stk.channel();
			//wrap把字节数组转为字节缓冲区
			ByteBuffer byteBuffer = ByteBuffer.wrap("您好,服务器,我是客户端".getBytes());
			/*
			 * write()方法的非阻塞调用哦只会写出其能够发送的数据,而不会阻塞等待所有数据,而后一起发送,
			   *    因此在调用write()方法将数据写入信道时,一般要用到while循环
			 */
			while(byteBuffer.hasRemaining())
			{
				try {
					//写数据
					socketChannel.write(byteBuffer);
					System.out.println("客户端写数据完毕");
				} catch (IOException e) {
					e.printStackTrace();
					System.out.println("写数据失败");
				}
			}
			try {
				//客户端给服务器发送数据后接收来自服务器的回报,上面迭代里删除了SelectionKey,
				//所以这里要把通道再次注册再选择器里
				socketChannel.register(selector, SelectionKey.OP_READ);
			} catch (ClosedChannelException e) {
				e.printStackTrace();
			}
		}
		
		
		//从通道里读取数据
		if(stk.isValid()&&stk.isReadable())
		{
			try {
				SocketChannel channel = (SocketChannel) stk.channel();
				//分配1024字节大小的字节缓冲区
				ByteBuffer readBuffer = ByteBuffer.allocate(1024);
				//把数据从通道里读入到字节缓冲区,这里假定传输的数据小于1024个字节
				int length = channel.read(readBuffer);
				String messageFromServer= new String(readBuffer.array(),0,length);
				System.out.println("来自服务器的数据:"+messageFromServer);
				//这里客户端接收到来自服务器的信息后,把通道再次注册再选择器再给服务器发送数据,
				//这样服务器和客户端就会进入你一句、我一句的死循环中
				//channel.register(selector, SelectionKey.OP_WRITE);
				//我们的例子之进行一次循环即可,关闭通道和选择器即可
				channel.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	} 
	
	public static void main(String[] args) {
		MyNIOClient myNIOClient  = new MyNIOClient();
		myNIOClient.start();
	}
	
	
	
}

代码可运行完成一次通讯,但是有缺陷,在通讯完成后,客户端和服务端都会在Selecotor.select处阻塞,因为无I/O准备好的通道,我尝试selector.close();会出现异常,这样肯定是不行的,后续在进行补充 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值