当然现在不需要自己手写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<SelectionKey> eventKeys = selector.selectedKeys();
Iterator<SelectionKey> 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())