EeBlog(テクニカルブログ)

第111 回 ソケットチャンネルの入出力

引き続き「ソケットチャンネルの入出力」です。

ServerSocketChannel、SocketChannelはSelectableChannelを継承しています。
このチャンネルにはconfigureBlockingメソッドによりブロックモードを設定できます。
ブロックモードが非ブロックに設定された場合、入出力による待機が発生しません。
したがって、非ブロックモードの場合、入出力を行うタイミングが解らなければなりません。
そのためにはセレクタ(Selector)を使用します。
セレクタにはあらかじめチャンネルを登録しておきます。
そしてSelectorのselectメソッドは、登録されたチャンネルのうち入出力が可能なチャンネルを選択してくれます。

次のサンプルコードはサーバを起動し、クライアントの接続要求に応答するプログラムです。
入出力は非ブロックモードで行います。

import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 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.nio.charset.Charset;
 import java.util.Iterator;


 import javax.swing.DefaultListModel;
 import javax.swing.JButton;
 import javax.swing.JFrame;
 import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTextField;
 import javax.swing.SwingUtilities;
 import javax.swing.UIManager;


 public class Main extends JFrame implements ActionListener {


     private static final long serialVersionUID = 1L;


     public static void main(String[] args) throws Exception {
         SwingUtilities.invokeLater(new Runnable() {
             public void run() {
                 try {
                     UIManager.setLookAndFeel(UIManager
                                     .getSystemLookAndFeelClassName());
                 } catch (Exception e) {}
                 new Main();
             }
         });
     }


     private final Service service = new Service();
     private final JPanel jPanel = new JPanel();
     private final JTextField port = new JTextField("9999");
     private final DefaultListModel defaultListModel = new DefaultListModel();
     private final JList jList = new JList(defaultListModel);
     private final JScrollPane jScrollPane = new JScrollPane(jList);
     private final JButton startupButton = new JButton("起動");
     private final JButton shutdownButton = new JButton("停止");


     public Main() {
         startupButton.addActionListener(this);
         shutdownButton.addActionListener(this);
         jPanel.add(port);
         jPanel.add(jScrollPane);
         jPanel.add(startupButton);
         jPanel.add(shutdownButton);
         setContentPane(jPanel);
         setTitle("SERVER");
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         setSize(300, 225);
         setResizable(false);
         setLocationRelativeTo(null);
         setVisible(true);
     }


     public void actionPerformed(ActionEvent actionEvent) {
         if (startupButton.equals(actionEvent.getSource())) {
             service.startup(port.getText());
         }
         if (shutdownButton.equals(actionEvent.getSource())) {
             service.shutdown();
         }
     }


     private void messageOut(final Object object) {
         if (SwingUtilities.isEventDispatchThread()) {
             defaultListModel.addElement(object);
         } else {
             SwingUtilities.invokeLater(new Runnable() {
                 public void run() {
                     defaultListModel.addElement(object);
                 }
             });
         }
     }


     private class Service implements Runnable {


         private ServerSocketChannel serverSocketChannel;
         private Selector selector;


         public void run() {
             try {
                 while (selector.select() > 0) {
                     for (Iterator iterator 
                                  = selector.selectedKeys().iterator();
                                                 iterator.hasNext();) {
                         SelectionKey selectionKey = iterator.next();
                         iterator.remove();
                         if (selectionKey.isAcceptable()) {
                             doAccept(selectionKey);
                         } else if (selectionKey.isReadable()) {
                             doResponse(selectionKey);
                         }
                     }
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }


         private void startup(String port) {
             if (serverSocketChannel == null || !serverSocketChannel.isOpen()) {
                 try {
                     serverSocketChannel = ServerSocketChannel.open();
                     serverSocketChannel.configureBlocking(false);
                     serverSocketChannel.socket().bind(new InetSocketAddress
                                                        (Integer.parseInt(port)));
                     selector = Selector.open();
                     serverSocketChannel.register(selector, 
                                                      SelectionKey.OP_ACCEPT);
                     new Thread(this).start();
                     messageOut("起動しました。");
                 } catch (Exception e) {
                     try {
                         serverSocketChannel.close();
                     } catch (Exception e2) {}
                     messageOut("起動できませんでした。");
                 }
             } else {
                 messageOut("起動しています。");
             }
         }


         private void shutdown() {
             if (serverSocketChannel != null && serverSocketChannel.isOpen()) {
                 try {
                     selector.wakeup();
                     serverSocketChannel.close();
                     messageOut("停止しました。");
                 } catch (Exception e) {}
             } else {
                 messageOut("起動していません。");
             }
         }


         private void doAccept(SelectionKey selectionKey) {
             ServerSocketChannel serverSocketChannel 
                              = (ServerSocketChannel) selectionKey.channel();
             try {
                 SocketChannel socketChannel = serverSocketChannel.accept();
                 socketChannel.configureBlocking(false);
                 socketChannel.register(selector, SelectionKey.OP_READ);
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }


         private void doResponse(SelectionKey selectionKey) {
             SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
             try {
                 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                 Charset charset = Charset.forName("Shift-JIS");
                 socketChannel.read(byteBuffer);
                 byteBuffer.flip();
                 messageOut(charset.decode(byteBuffer));
                 socketChannel.write(charset.encode("response from server"));
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 try {
                     socketChannel.close();
                 } catch (Exception e) {}
             }
         }
     }
 }

セレクタへのチャンネルの登録はregisterメソッドにより行います。
最初はServerSocketChannelに非ブロックの設定を行い、セレクタに登録しています。
このとき、このチャンネルが接続要求を受け取ることができる場合に、選択されるように設定しています。(registerメソッドの第二引数で設定)
このチャンネルが接続要求を受け取ることができる状態であれば、selectメソッドにより選択されます。
selectedKeysメソッドは選択されたチャンネルのキー(SelectionKey)の集合を返します。
SelectionKeyのchannelメソッドによりチャンネルを取得し、非ブロックのacceptメソッドでSocketChannelを取得しています。

次に取得したSocketChannelに非ブロックの設定を行い、セレクタに登録しています。
このとき、このチャンネルが読み込み可能である場合に、選択されるように設定しています。
このチャンネルが選択されたあとに、非ブロックのreadメソッドでクライアントからのメッセージを読み込んでいます。

このサンプルの利点はacceptメソッドやreadメソッドによる待機が発生しないため、前々回のようにクライアントの接続毎にスレッドを起動する必要がないことです。
チャンネルが選択されたときには、待機なしで入出力処理が行える状態なので、1つのスレッドで選択されたチャンネルから順に処理を行っていけばよいわけです。