您的位置:首页 > 编程语言 > Java开发

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

2017-07-26 13:33 645 查看
           当然现在不需要自己手写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())


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: