之前用nio写了一个聊天室的简单demo,发现思路很乱,然后就开始看zookeeper是怎么做的,发现思路很妙,于是就仿照着写了一个很简单的客户端版本,就是输入一个加法的字符串,然后让服务器计算,再返回。
客户端:
package client;
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;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class Client {
private ClientIO cIO;
public Client(){
init();
}
private void init(){
cIO = new ClientIO();
}
class ClientIO{
private Queue<Packet> outgoingQueue;
private Queue<Packet> pendingQueue;
private Selector selector;
private SocketChannel channel;
public ClientIO(){
outgoingQueue = new LinkedList<>();
pendingQueue = new LinkedList<>();
try {
selector = Selector.open();
channel = SocketChannel.open();
channel.connect(new InetSocketAddress("localhost", 20002));
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_CONNECT);
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new SendThread()).start();
}
public void submit(Packet packet){
outgoingQueue.add(packet);
}
class SendThread implements Runnable{
private void connect(){
try {
channel.register(selector, SelectionKey.OP_WRITE);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
private void read(){
if(pendingQueue.isEmpty())
return;
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
channel.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
buffer.flip();
String r = new String(buffer.array());
Packet packet = pendingQueue.poll();
packet.setResult(r);
synchronized (packet) {
packet.notifyAll();
}
}
private void updateInterest(){
try {
if(!outgoingQueue.isEmpty() && !pendingQueue.isEmpty()){
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}else if (!outgoingQueue.isEmpty() && pendingQueue.isEmpty()) {
channel.register(selector, SelectionKey.OP_WRITE);
}else if (outgoingQueue.isEmpty() && !pendingQueue.isEmpty()) {
channel.register(selector, SelectionKey.OP_READ);
}else {
channel.register(selector, SelectionKey.OP_WRITE);
}
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
private void write(){
if(outgoingQueue.isEmpty())
return;
Packet packet = outgoingQueue.poll();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(packet.getOp().getBytes());
buffer.flip();
try {
channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
pendingQueue.add(packet);
}
@Override
public void run() {
try {
while(true){
selector.select();
Iterator<SelectionKey> itr = selector.selectedKeys().iterator();
while(itr.hasNext()){
SelectionKey key = itr.next();
if(key.isConnectable()){
connect();
}else if(key.isReadable()){
read();
}else if(key.isWritable()){
write();
}
itr.remove();
}
updateInterest();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Packet{
private String op;
private String result;
public void setOp(String op){
this.op = op;
}
public void setResult(String result){
this.result = result;
}
public String getOp(){
return this.op;
}
public String getResult(){
return this.result;
}
}
private void loop(){
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
while(true){
System.out.println("Plz input: ");
String op = scanner.next();
System.out.println(getResult(op));
}
} finally {
scanner.close();
}
}
private String getResult(String op){
Packet packet = new Packet();
packet.setOp(op);
cIO.submit(packet);
synchronized (packet) {
try {
packet.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return packet.getResult();
}
public static void main(String[] args) {
new Client().loop();
}
}
服务端:
没有什么,还是自己写的,完了研究了zookeeper的再更一个
package server;
import java.io.IOException;
import java.math.BigDecimal;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server {
private Selector selector;
private static final int PORT = 20002;
public void connect(SelectionKey key){
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
System.out.println("log: client connects...");
try {
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (ClosedChannelException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public String compute(String m){
String[] op = m.split("\\+");
BigDecimal a1 = new BigDecimal(op[0]);
BigDecimal a2 = new BigDecimal(op[1]);
return a1.add(a2).toString();
}
public void read(SelectionKey key){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuffer sb = new StringBuffer();
int c = 0;
try {
while((c = socketChannel.read(buffer)) > 0){
buffer.flip();
int size = buffer.remaining();
byte[] bytes = new byte[size];
buffer.get(bytes, 0, size);
sb.append(new String(bytes));
}
if(c == -1){
System.out.println("log: client closes");
socketChannel.close();
return;
}else{
System.out.println("log: msg-> " + sb.toString());
String r = compute(sb.toString());
SocketChannel sc = (SocketChannel) key.channel();
sc.register(selector, SelectionKey.OP_WRITE, r);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void write(SelectionKey key){
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(((String)(key.attachment())).getBytes());
buffer.flip();
try {
sc.write(buffer);
sc.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
this.selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.register(selector, SelectionKey.OP_ACCEPT, "server");
System.out.println("log: sever starts...");
while(selector.select() > 0){
Iterator<SelectionKey> itr = selector.selectedKeys().iterator();
while(itr.hasNext()){
SelectionKey key = itr.next();
if(key.isAcceptable()){
connect(key);
}else if(key.isReadable()){
read(key);
}else if(key.isWritable()){
write(key);
}
itr.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Server().start();
}
}
最常使用的场景是请求响应模式,服务器和客户端处于不同的角色。服务端是被动方,客户端是主动方,所以客户端的特征是先写后读,而服务端是先读后写。再结合nio中可以监听的四种事件,accept是服务端专有的,也是服务端最开始监听的事件,而connect是客户端专有的,也是客户端最开始监听的事件。然后服务端一旦监听到了连接,就需要为这个新的socket设置read事件来等待客户端的请求,而客户端connect以后一般是设置write事件,等待用户输入然后向服务端发送请求。
nio的另一个特征是服务端的请求响应或者客户端的请求响应都是处于两个监听周期的。
对于服务端,肯定是先从read周期获取用户的请求,然后再在write周期返回,就涉及到了一个缓存结果,然后在下一个周期write的过程,其实这个有一个比较简单的做法就是每一次read周期内,解析了请求,然后得到了相应的资源结果,然后就需要注册一个写事件写入结果,那么可以在注册写事件的同时,把结果使用register的attachment来附加到key上,那么下一个写周期的时候,服务器就可以直接从key里面取到这个attachment,直接写入。这个办法应对简单的场景个人感觉还是很好的,因为nio的思路是使用一个select来管理所有的channel或者连接,所以,每一个read周期都有多个channel的请求,我们缓存结果的时候需要考虑或者维护一个映射关系,以便于在下一个write周期的时候知道每一个write事件就绪的channel要写入的是哪一份结果,这个维护过程我们可以使用attachment机制来应对,attachment本身也就是用来标识或者绑定与这个channel相关的一些东西,所以感觉这个办法是对请求响应服务器模型的一个很好的帮助。
所以个人感觉一个比较简单的nio服务器模式是:首先在serversocketchannel上注册一个accept事件,然后有了连接,就先注册一个读事件等待请求,然后一旦有了请求,就处理,计算结果,然后就注册一个写事件,并且把结果attach上,注意这里的事件都是单独的事件,尤其是写入结果的时候写事件不能和读事件一起,因为如果我们的while循环里如果先处理的是读事件,那么就会覆盖之前的结果。这个模型可以应对请求响应模式。如果是聊天那种,可能需要维护一个channel和session的关系。这也是我最近写了两个ni的简单服务器额感悟。
下面重点说下nio客户端怎么写。我感觉客户端还是最原始的bio比较好写。因为客户端通常吧只有一个socket,而且客户端通常会有一个客户端线程来梳理ui,每一个按钮都会对应一次发送请求并等待结果的过程。所以很自然地,就是使用bio会比较简单,就是用outstream发,然后用instream收。nio还是用在服务器端比较好,因为服务器要处理多个socket,所以使用bio会对应很多线程,才引入了nio的select机制。个人感觉客户端nio写起来并不是很舒服。
所以我研究了一下zookeeper的nio客户端的实现,大致写出了上面的那个客户端,感觉还是比较舒服的,当然还有很多需要完善的地方。
思路:
这里主要针对阻塞式的客户端模式,比如说客户端界面线程要获取服务端的数据来显示,这种场景一般都是阻塞式的,界面画一个圈然后转,知道服务器返回了资源,再显示。
(1)首先,必须将io的东西和客户端线程后者业务解耦,所以我们可以建立一个clientIO的内部类来处理网络io。然后封装一个发送的最小单位packet。所以客户端的每一次请求都会生成一个packet。
(2)客户端和io唯一通讯的是两个队列,一个outgoing和一个pending。
outgoing:是将要发送的packet;
pending:是发送完的packet,等待服务器的结果。
所以客户端每一次的packet会先进入outgoing队列,然后发送完以后再进入pending队列,当收到服务器相应以后再从pending移除,最后再通知客户端处理完毕。
(3)clientIO内部会建立这两个队列,还会有一个nio的线程来处理队列信息。首先在channel上注册一个connect事件,一旦连接就注册一个write事件,一旦有write事件,就调用write方法把outgoing队列中取一个packet发出去,把packet移到pending中,然后再注册一个读事件,等待结果。这样在下一个读事件里,就可以从pending中拿一个事件处理,最后把结果写入packet,通知客户端线程。这个过程每一次都是从队列的开始拿,从队列的尾部放,因为这个是tcp协议,所以最终会按顺序到达服务器,那么从服务器返回的结果,也就一定是pending队列中的第一个packet,所以这个对应关系正好解决了客户端的一个难题,那就是客户端通常是先写后读的,那么下一次读的时候怎么知道结果对应的是哪一个packet呢?这个双队列正好解决了这个对应问题,而且把客户端业务和io完完全无案解耦了,感觉非常好的一个设计。
所以clietIO的主要职责是:
维护两个队列,向客户端提供一个接口来向队列扔packet;
启动一个线程来处理列队中的packet,每一个写入周期把outgoing发送出去,每一个读周期把pending的packet附上结果。然后通知唤醒客户端线程。
那么客户端要做的就是:
每当业务方法需要io时,就构造一个packet,扔到io的队列里,然后阻塞在这个packet上,知道被clientIO唤醒,返回结果。
过程大致如下:
private String getResult(String op){
Packet packet = new Packet();
packet.setOp(op);
cIO.submit(packet);
synchronized (packet) {
try {
packet.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return packet.getResult();
}