EeBlog(テクニカルブログ)

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

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

前回はサーバ側のプログラムを実装したので、今回はクライアント側のプログラムを実装してみたいと思います。
なお、前回のサーバプログラムはHTTPのようにステートレスなものとして実装しているため、今回のプログラムもそれにあわせた形で実装します。(1回の送受信で接続は切断される)

次のサンプルコードは前回作成したサーバに接続し、通信を行うプログラムです。

import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.ClosedByInterruptException;
 import java.nio.channels.SocketChannel;
 import java.nio.charset.Charset;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;


 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) {
         SwingUtilities.invokeLater(new Runnable() {
             public void run() {
                 try {
                     UIManager.setLookAndFeel(UIManager
                                  .getSystemLookAndFeelClassName());
                 } catch (Exception e) {}
                 new Main();
             }
         });
     }


     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 JPanel jPanel = new JPanel();
     private final JButton submitButton = new JButton("送信");
     private final JButton cancelButton = new JButton("キャンセル");
     private final ExecutorService executorService 
                                       = Executors.newSingleThreadExecutor();
     private Future future;


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


     public void actionPerformed(ActionEvent actionEvent) {
         if (submitButton.equals(actionEvent.getSource())) {
             future = executorService.submit(new Request(port.getText()));
             buttonChangeEnabled();
         } else if (cancelButton.equals(actionEvent.getSource())) {
             future.cancel(true);
         }
     }


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


     private void buttonChangeEnabled() {
         if (SwingUtilities.isEventDispatchThread()) {
             submitButton.setEnabled(!submitButton.isEnabled());
             cancelButton.setEnabled(!cancelButton.isEnabled());
         } else {
             SwingUtilities.invokeLater(new Runnable() {
                 public void run() {
                     submitButton.setEnabled(!submitButton.isEnabled());
                     cancelButton.setEnabled(!cancelButton.isEnabled());
                 }
             });
         }
     }
     
     private class Request implements Runnable {


         private String host = "localhost";
         private String port;


         private Request(String port) {
             this.port = port;
         }


         public void run() {
             SocketChannel socketChannel = null;
             try {
                 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                 Charset charset = Charset.forName("Shift-JIS");
                 socketChannel = SocketChannel.open(new InetSocketAddress
                                                   (host, Integer.parseInt(port)));
                 socketChannel.write(charset.encode("request from client"));
                 socketChannel.read(byteBuffer);
                 byteBuffer.flip();
                 messageOut(charset.decode(byteBuffer));
             } catch (ClosedByInterruptException e) {
                 messageOut("通信を中断しました。");
             } catch (Exception e) {
                 e.printStackTrace();
             } finally {
                 try {
                     socketChannel.close();
                 } catch (Exception e) {}
             }
             buttonChangeEnabled();
         }
     }
 }

「送信」ボタンを押すと、サーバに対してメッセージを送信し、サーバから応答があった場合は、そのメッセージを画面に表示します。
送受信はSocketChannelを使った入出力によりおこないます。
また、なんらかの理由でサーバから応答がないような場合を想定して「キャンセル」ボタンを用意しました。

SocketChannelはInterruptibleChannelインターフェースを実装しています。
このチャンネルの入出力操作を待機しているスレッドに対して割り込みを行うとClosedByInterruptExceptionがスローされます。
サーバからの応答を待機しているスレッドを終了させることができるように、「キャンセル」ボタンを押したときにそのスレッドに対して割り込みを行うようにしています。