最近在动手写一个DNS服务器,遇到了超多的问题。之前没有任何NIO的储备知识,啃起来太慢了。做一些整理,记录看过的知识点,日后再详细整理吧。
一、用到的类、接口和方法
1.Selector
Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。使用Selector的好处就是可以使用更少的线程来就可以来处理通道了,相比于使用多个线程,避免了线程上下文切换带来的开销。了解更多
你可能会发现flip()方法多次出现。调用flip()方法之后,读/写指针position指到缓冲区头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
2.ServerSocketChannel
是整个服务端的的leader,相当于一个网络管理中心,通过open、bind方法可以绑定端口号监听请求。有一个生动的讲解可以学习一下。
3.SocketChannel
SocketChannel是一种面向流连接的可选择通道,可以被多路复用。具体内容参考这里
参数里有个Nagle算法可以了解一下。
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快。
你可能会顺路发现一些关于TCP粘包的问题。tcp虽然是个强大的协议,能保证数据的稳定性,一致性,但在实际开发中,需要根据实际的数据协议,来控制每次获取的包是客户端发过来的一个完整的可以解析的包,当客户端接收过于慢或缓冲区大小小于包大小等情况发生时可能会发生拆包、粘包的事情。
4.InetSocketAddress
- java.net.InetSocketAddress类:此类实现 IP 套接字地址(IP 地址 + 端口号)。
可以由如下构建方法构建
InetSocketAddress(InetAddress addr, int port)根据 IP 地址和端口号创建套接字地址。
InetSocketAddress(String hostname, int port) 根据主机名和端口号创建套接字地址。 - 常用方法:
- getAddress():获取 InetAddress。
- getPort() 获取端口号。
- toString() 构造此 InetSocketAddress 的字符串表示形式。(主机名/Ip:端口号)
- getHostName()获取 主机名。
5.InerAddress
- java.net.InetAddress类:表示互联网协议 (IP) 地址。
- 常用方法:
- getByName(String host) 在给定主机名的情况下确定主机的 IP 地址。
- getHostName() 获取此 IP地址的主机名。
- getHostAddress()返回 IP 地址字符串(以文本表现形式)。
- getLocalHost() 返回本地主机。
- getAllByName(String host) 在给定主机名的情况下,根据系统上配置的名称服务返回其 IP 地址所组成的数组。
6.SelectionKey
SelectionKey是一个抽象类,表示selectableChannel在Selector中注册的标识.每个Channel向Selector注册时,都将会创建一个selectionKey.选择键将Channel与Selector建立了关系,并维护了channel事件.了解更多
7.Thread
线程
二、Reactor模式示例
基本上所有的网络处理程序都有Read request、Decode request、Process service、Encode reply和Send reply的过程
对于每一个请求都分发给一个线程,每个线程中都独自处理上面的流程。这种模型由于IO在阻塞时会一直等待,因此在用户负载增加时,性能下降的非常快。几个主要原因,一是accept方法本身是个阻塞方法,阻塞等待client连接,直到client连接成功;二是读入数据时阻塞至数据读完,同样写入数据时也会阻塞到数据全写入完毕。
那么我们很自然地可以做出改进,采用基于事件驱动的设计,当有事件触发时,才调用处理器进行数据处理。
服务端Reactor和Handler代码
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
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;
class Reactor implements Runnable
{
protected Logger logger = Logger.getLogger(getClass());
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException
{ //Reactor初始化
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
logger.info("init Reactor class!");
serverSocket.socket().bind(new InetSocketAddress(port));
//非阻塞
serverSocket.configureBlocking(false);
logger.info("bind port "+port);
//分步处理,第一步,接收accept事件
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
//attach callback object, Acceptor
sk.attach(new Acceptor());
}
public void run()
{
logger.info("run() is running...");
try
{
while (!Thread.interrupted())
{
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext())
{
//Reactor负责dispatch收到的事件
dispatch((SelectionKey) (it.next()));
}
selected.clear();
}
} catch (IOException ex)
{ /* ... */ }
}
void dispatch(SelectionKey k)
{
logger.info("dispatch event for "+ Thread.currentThread().getId());
Runnable r = (Runnable) (k.attachment());
//调用之前注册的callback对象
if (r != null)
{
r.run();
}
}
// inner class
class Acceptor implements Runnable
{
public void run()
{
try
{
SocketChannel channel = serverSocket.accept();
if (channel != null)
new Handler(selector, channel);
logger.info("run Acceptor for "+Thread.currentThread().getId());
} catch (IOException ex)
{ /* ... */ }
}
}
public static void main(String[] args)throws IOException {
Reactor reactor = new Reactor(666);
reactor.run();
}
}
import org.apache.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
class Handler implements Runnable
{
protected Logger logger = Logger.getLogger(getClass());
final SocketChannel channel;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(100);
ByteBuffer output = ByteBuffer.allocate(100);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector selector, SocketChannel c) throws IOException
{
channel = c;
c.configureBlocking(false);
// Optionally try first read now
sk = channel.register(selector, 0);
logger.info("Optionally try first read now");
//将Handler作为callback对象
sk.attach(this);
logger.info("regard Handler as CallBack Object");
//第二步,注册Read就绪事件
sk.interestOps(SelectionKey.OP_READ);
selector.wakeup();
logger.info("register ready event");
}
boolean inputIsComplete()
{
logger.info("check if input is completed");
/* ... */
return true;
}
boolean outputIsComplete()
{
logger.info("check if output is completed");
/* ... */
return true;
}
void process()
{
logger.info("process it");
/* ... */
return;
}
public void run()
{
logger.info("run() of Handler is running...");
try
{
if (state == READING)
{
read();
}
else if (state == SENDING)
{
send();
}
} catch (IOException ex)
{ /* ... */ }
}
void read() throws IOException
{
channel.read(input);
if (inputIsComplete())
{
process();
state = SENDING;
// Normally also do first write now
//第三步,接收write就绪事件
sk.interestOps(SelectionKey.OP_WRITE);
}
}
void send() throws IOException
{
channel.write(output);
//write完就结束了, 关闭select key
if (outputIsComplete())
{
sk.cancel();
}
}
}
那我们写个客户端程序,尝试向服务器发送请求
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",666);
//输出数据到服务器
String message = "hello world!";
OutputStream outputStream = socket.getOutputStream();
outputStream.write(message.getBytes("UTF-8"));
}
}
输出日志结果