Java Socketプログラミング概要_動力ノードJava学院の整理


Java Socketプログラミングには二つの概念があります。一つはServer Socketで、一つはSocketです。サービスとクライアントの間にはソケットを介して接続が確立され、その後通信が可能になる。まず、ServerSocketは、クライアントがSocketを接続しようとしていることを発見したとき、サービス端末で対応するソケットを確立しながら、acceptの接続要求を行います。このように二つのソケットがあります。クライアントとサービスはそれぞれ一つずつです。
       ソケット間の通信は簡単です。サービスはソケットの出力フローにものを書きます。クライアントはソケットの入力フローで対応する内容を読み取ることができます。ソケットとソケットの間は双方向に接続されているので、クライアントは対応するソケットにストリームの中に書き込みを出力し、サービス端末に対応するソケットの入力ストリームに対応する内容を読み出すことができます。いくつかのサービスがクライアントと通信する例を以下に示す。
      1、クライアントが書き終わったら読みます。
       サービスコード 
Javaコード  

 public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 //server      Socket     ,server accept        
 Socket socket = server.accept(); 
 //           ,       socket InputStream,               。 
 Reader reader = new InputStreamReader(socket.getInputStream()); 
 char chars[] = new char[64]; 
 int len; 
 StringBuilder sb = new StringBuilder(); 
 while ((len=reader.read(chars)) != -1) { 
  sb.append(new String(chars, 0, len)); 
 } 
 System.out.println("from client: " + sb); 
 reader.close(); 
 socket.close(); 
 server.close(); 
 } 
 } 
サービスがSocketのInputStreamからデータを読み出す動作も遮断されています。入力ストリームからデータプログラムを読み取らないと、クライアントがSocketの出力ストリームにデータを書き込んだり、Socketの出力ストリームをオフにしたりします。もちろん、クライアントに対するソケットも同じです。操作が完了すると、プログラム全体が終了する前に対応するリソースをオフにすること、すなわち対応するIOストリームとソケットをオフにすることを覚えてください。
       クライアントコード
Javaコード  

public class Client { 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream()); 
 writer.write("Hello Server."); 
 writer.flush();//      flush 
 writer.close(); 
 client.close(); 
 } 
 } 
    クライアントのSocketへの出力フローについて、データを書いてサービス側に渡す場合は注意してください。書き込み操作後のプログラムは出力フローのクローズに対応するものではなく、他のブロック操作(例えば、入力ストリームからデータを読む)を行います。flushを覚えてください。このようなサービスがあれば、クライアントから送信されたデータを受信することができます。そうしないと、無限にお互いを待つことができます。後ほどクライアントとサービスを同時に読んだり書いたりする時にこの問題を言います。 
      2、クライアントとサービス端末は同時に読み、書きます。
       前に述べたように、ソケット間は双方向通信であり、データを受信することもできるし、データを送信することもできる。
       サービスコード 
Javaコード    

public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 //server      Socket     ,server accept        
 Socket socket = server.accept(); 
 //           ,       socket InputStream,               。 
 Reader reader = new InputStreamReader(socket.getInputStream()); 
 char chars[] = new char[64]; 
 int len; 
 StringBuilder sb = new StringBuilder(); 
 while ((len=reader.read(chars)) != -1) { 
  sb.append(new String(chars, 0, len)); 
 } 
 System.out.println("from client: " + sb); 
 //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 
 writer.write("Hello Client."); 
 writer.flush(); 
 writer.close(); 
 reader.close(); 
 socket.close(); 
 server.close(); 
 } 
 } 
上記のコードではまず、入力ストリームからクライアントから送信されたデータを読み取り、次に出力ストリームにデータを書き込み、対応するリソースファイルをクローズします。実際には、上記のコードは、私たちがあらかじめ想定した方法で動作しないかもしれません。入力ストリームからデータを読み取るのはブロック操作です。上記のwhileループでは、データを読むと循環体が実行されます。さもなければ、ブロックされます。これでは、後の書き込みは永遠に実行できなくなります。クライアント対応のソケットが閉塞してから停止しない限り、whileループも飛び出します。このような永遠に実行できないかもしれない状況に対する解決方法は、whileループは中から条件があります。上記のコードを見ても、絶えず変化しているのは取った長さlenと読んだデータだけです。lenはもう使えなくなりました。唯一使えるのは読んだデータです。このような状況に対して、通常は終了マークを約束します。クライアントから送られてきたデータに終了マークが含まれている場合、現在のデータはすでに発送済みとなりました。この時に循環して飛び出すことができます。改善されたコードはこのようになります。
Javaコード  

 public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 //server      Socket     ,server accept        
 Socket socket = server.accept(); 
 //           ,       socket InputStream,               。 
 Reader reader = new InputStreamReader(socket.getInputStream()); 
 char chars[] = new char[64]; 
 int len; 
 StringBuilder sb = new StringBuilder(); 
 String temp; 
 int index; 
 while ((len=reader.read(chars)) != -1) { 
  temp = new String(chars, 0, len); 
  if ((index = temp.indexOf("eof")) != -1) {//  eof       
  sb.append(temp.substring(0, index)); 
  break; 
  } 
  sb.append(temp); 
 } 
 System.out.println("from client: " + sb); 
 //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 
 writer.write("Hello Client."); 
 writer.flush(); 
 writer.close(); 
 reader.close(); 
 socket.close(); 
 server.close(); 
 } 
 } 
上記コードでは、クライアントから送信された終了フラグ、すなわち「eof」をサービス端末が読み込むと、データの受信を終了し、ループを終了し、その後のコードを継続することができる。 
       クライアントコード
Javaコード       

public class Client { 
 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream()); 
 writer.write("Hello Server."); 
 writer.flush(); 
 //          
 Reader reader = new InputStreamReader(client.getInputStream()); 
 char chars[] = new char[64]; 
 int len; 
 StringBuffer sb = new StringBuffer(); 
 while ((len=reader.read(chars)) != -1) { 
  sb.append(new String(chars, 0, len)); 
 } 
 System.out.println("from server: " + sb); 
 writer.close(); 
 reader.close(); 
 client.close(); 
 } 
 } 
上記のコードの中で、まずサービス側にデータを送りました。その後、サービス側から戻ってきたデータを読みました。前のサービス側と同じように読んでいるうちに、プログラムがずっとそこに掛かっていて、いつまでもwhileサイクルを跳び出せないかもしれません。このコードはサービス端末の第一段コードに合わせて、サービス端末が永遠にそこでデータを受信することを分析させてくれます。いつまでもwhileループから飛び出せません。つまり、その後のサービスがないので、データをクライアントに返します。クライアントもサービスから帰るデータを受信することができません。解決方法は、サービス端末の第二段コードに示されているように、クライアントがデータを送信し終わった後、出力ストリームに完了フラグを書き込み、サービス端末のデータがすでに送信されたことを伝えます。修正後のクライアントコードはこのようにするべきです。
Javaコード    

public class Client { 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream()); 
 writer.write("Hello Server."); 
 writer.write("eof"); 
 writer.flush(); 
 //          
 Reader reader = new InputStreamReader(client.getInputStream()); 
 char chars[] = new char[64]; 
 int len; 
 StringBuffer sb = new StringBuffer(); 
 String temp; 
 int index; 
 while ((len=reader.read(chars)) != -1) { 
  temp = new String(chars, 0, len); 
  if ((index = temp.indexOf("eof")) != -1) { 
  sb.append(temp.substring(0, index)); 
  break; 
  } 
  sb.append(new String(chars, 0, len)); 
 } 
 System.out.println("from server: " + sb); 
 writer.close(); 
 reader.close(); 
 client.close(); 
 } 
 } 
      
私たちが日常的に使っているのは、このようなクライアントがサービスにデータを送信し、サービス端末がデータを受信してから、対応する結果をクライアントに返すという形です。ただ、クライアントとサービス端末の間はこのような一対一の関係ではなく、以下に述べる複数のクライアントが同じサービスを提供する状況に対応しています。      
      3、複数のクライアントと同じサービスを接続する
       前に述べた二つの例はいずれもサービス端末が一つのクライアントの要求を受信した後で終わります。他のクライアントの要求をもう受信できません。これはよく私たちの要求を満たすことができません。私たちは通常こうします。
Javaコード       

public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 while (true) { 
  //server      Socket     ,server accept        
 Socket socket = server.accept(); 
  //           ,       socket InputStream,               。 
 Reader reader = new InputStreamReader(socket.getInputStream()); 
  char chars[] = new char[64]; 
  int len; 
  StringBuilder sb = new StringBuilder(); 
  String temp; 
  int index; 
  while ((len=reader.read(chars)) != -1) { 
  temp = new String(chars, 0, len); 
  if ((index = temp.indexOf("eof")) != -1) {//  eof       
   sb.append(temp.substring(0, index)); 
   break; 
  } 
  sb.append(temp); 
  } 
  System.out.println("from client: " + sb); 
  //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 
  writer.write("Hello Client."); 
  writer.flush(); 
  writer.close(); 
  reader.close(); 
  socket.close(); 
 } 
 } 
 } 
上記のコードでは、デッドサイクルを使用して、クライアントからの接続要求を受信しようとする、そのaccept方法を循環体の中でServerSocketに呼び出します。要求を受信していない場合、プログラムはここでブロックされ、クライアントからの接続要求を受信すると、その後、現在接続されているクライアントと通信し、終了したら、循環体を実行して再度新しい接続要求を受信することを試みる。このように、私たちのServerSocketはすべてのクライアントからの接続要求を受信し、それらと通信することができます。これにより、一つの簡単なサービス端末が複数のクライアントと通信するモードが実現される。
       上記の例では、一つのサービス端末が複数のクライアントと通信することが実現されたが、もう一つの問題がある。上記の例では、クライアントからの接続要求を受信するたびに、現在のクライアントと通信が終わってから次の接続要求を処理することができます。これは、同時多発の場合、プログラムの性能に重大な影響を与えるので、クライアントとの通信方式に変えられます。
Javaコード       

public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 while (true) { 
  //server      Socket     ,server accept        
  Socket socket = server.accept(); 
  //      Socket              
  new Thread(new Task(socket)).start(); 
 } 
 } 
 /** 
 *     Socket    
 */ 
 static class Task implements Runnable { 
 private Socket socket; 
 public Task(Socket socket) { 
  this.socket = socket; 
 } 
 public void run() { 
  try { 
  handleSocket(); 
  } catch (Exception e) { 
  e.printStackTrace(); 
  } 
 } 
 /** 
 *     Socket     
 * @throws Exception 
 */ 
 private void handleSocket() throws Exception { 
  Reader reader = new InputStreamReader(socket.getInputStream()); 
  char chars[] = new char[64]; 
  int len; 
  StringBuilder sb = new StringBuilder(); 
  String temp; 
  int index; 
  while ((len=reader.read(chars)) != -1) { 
  temp = new String(chars, 0, len); 
  if ((index = temp.indexOf("eof")) != -1) {//  eof       
  sb.append(temp.substring(0, index)); 
   break; 
  } 
  sb.append(temp); 
  } 
  System.out.println("from client: " + sb); 
  //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 
  writer.write("Hello Client."); 
  writer.flush(); 
  writer.close(); 
  reader.close(); 
  socket.close(); 
 } 
 } 
 } 
上記のコードでは、ServerSocketが新しいソケット接続要求を受信するたびに、現在のソケットと通信するスレッドが新たに作成され、これにより、クライアントソケットとの通信が非同期処理される場合がある。
       SocketのInputStreamからデータを受信する時、上のように少しずつ読むのは複雑すぎます。私たちはBufferedReaderを使って一度読んでみます。
Javaコード       

 public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 while (true) { 
  //server      Socket     ,server accept        
  Socket socket = server.accept(); 
  //      Socket              
  new Thread(new Task(socket)).start(); 
 } 
 } 
 /** 
 *     Socket    
 */ 
 static class Task implements Runnable { 
 private Socket socket; 
 public Task(Socket socket) { 
  this.socket = socket; 
 } 
 public void run() { 
  try { 
  handleSocket(); 
  } catch (Exception e) { 
  e.printStackTrace(); 
  } 
 } 
 /** 
 *     Socket     
 * @throws Exception 
 */ 
 private void handleSocket() throws Exception { 
  BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); 
  StringBuilder sb = new StringBuilder(); 
  String temp; 
  int index; 
  while ((temp=br.readLine()) != null) { 
  System.out.println(temp); 
  if ((index = temp.indexOf("eof")) != -1) {//  eof       
  sb.append(temp.substring(0, index)); 
   break; 
  } 
  sb.append(temp); 
  } 
  System.out.println("from client: " + sb); 
  //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 
  writer.write("Hello Client."); 
  writer.write("eof
"); writer.flush(); writer.close(); br.close(); socket.close(); } } }
この時注意しなければならないのは、BufferedReaderのreadline方法は一回一回の一行を読むので、この方法はブロックして、それが1行のデータを読むまでプログラムは続けて実行して、それではreadLineはいつやっと1行まで読むことができますか?プログラムが改行符に出会うまでは、あるいは流れに応じたエンドマークのreadline方法が一行まで読まれたと思えば、そのブロックは終了し、プログラムを継続して実行させます。ですから、私たちはBufferedReaderのreadlineを使ってデータを読み取る時、対応する出力ストリームの中に必ず改行符を書きます。(流れが終わったら自動的に終了と表示されます。readlineは識別できます。)、改行符を書いてから、出力ストリームがすぐにクローズされない場合はflushを覚えてください。このようにデータはバッファから本当に書き込みます。上のコードに対応して、クライアントプログラムはこう書きます。
Javaコード

 public class Client { 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream()); 
 writer.write("Hello Server."); 
 writer.write("eof
"); writer.flush(); // BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); StringBuffer sb = new StringBuffer(); String temp; int index; while ((temp=br.readLine()) != null) { if ((index = temp.indexOf("eof")) != -1) { sb.append(temp.substring(0, index)); break; } sb.append(temp); } System.out.println("from server: " + sb); writer.close(); br.close(); client.close(); } }
      4、タイムアウト時間を設定する
       このような需要があると仮定して、私たちのクライアントは、サービスからソケットを介してXX情報を取得し、ユーザーにページに表示する必要があります。私たちは、データを読んでいるときに、ソケットがブロックされていることを知っています。データを読んでいないと、プログラムがブロックされてしまいます。同期要求の時、私達は必ずこのような状況が発生することを許可できません。これは一定の時間に達したら、ブロックの中断を制御して、プログラムを実行させます。Socketは、受信データのタイムアウト時間を設定するためのsetSoTimeout()方法を提供してくれます。単位はミリ秒です。設定されたタイムアウト時間が0より大きく、この時間を過ぎても返したデータがまだ受信されていない場合、SocketはSocketTimeout Exceptionを投げます。
       データの読み取りを開始してから10秒後にデータが読み込まれないとブロックを中断するように制御する必要があるとします。 
Javaコード  

public class Client { 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream()); 
 writer.write("Hello Server."); 
 writer.write("eof
"); writer.flush(); // BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); // 10 client.setSoTimeout(10*1000); StringBuffer sb = new StringBuffer(); String temp; int index; try { while ((temp=br.readLine()) != null) { if ((index = temp.indexOf("eof")) != -1) { sb.append(temp.substring(0, index)); break; } sb.append(temp); } } catch (SocketTimeoutException e) { System.out.println(" 。"); } System.out.println("from server: " + sb); writer.close(); br.close(); client.close(); } }
       5、受信データの文字化け
       このようなサービス端末やクライアントが中国語の文字化けを受信する場合、通常はデータ送信時に使用する符号化と受信時に使用する符号化が一致しないからです。例えば次のようなサービスコードがあります。
Javaコード 

 public class Server { 
 public static void main(String args[]) throws IOException { 
 //      ,            
 int port = 8899; 
 //    ServerSocket     8899  
 ServerSocket server = new ServerSocket(port); 
 while (true) { 
  //server      Socket     ,server accept        
  Socket socket = server.accept(); 
  //      Socket              
  new Thread(new Task(socket)).start(); 
 } 
 } 
 /** 
 *     Socket    
 */ 
 static class Task implements Runnable { 
 private Socket socket; 
 public Task(Socket socket) { 
  this.socket = socket; 
 } 
 public void run() { 
  try { 
  handleSocket(); 
  } catch (Exception e) { 
  e.printStackTrace(); 
  } 
 } 
 /** 
 *     Socket     
 * @throws Exception 
 */ 
 private void handleSocket() throws Exception { 
  BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK")); 
  StringBuilder sb = new StringBuilder(); 
  String temp; 
  int index; 
  while ((temp=br.readLine()) != null) { 
  System.out.println(temp); 
  if ((index = temp.indexOf("eof")) != -1) {//  eof       
  sb.append(temp.substring(0, index)); 
   break; 
  } 
  sb.append(temp); 
  } 
  System.out.println("   : " + sb); 
  //       
 Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8"); 
  writer.write("  ,   。"); 
  writer.write("eof
"); writer.flush(); writer.close(); br.close(); socket.close(); } } }
ここでテストをすると混乱します。上記のサービスエンドコードでは、入力ストリームを定義する際に、GBK符号化を使用してデータを読み取ることを明確に定義していますが、出力ストリームを定義する際にUTF-8符号化を使用してデータを送信することを明確に指定しています。クライアント上でデータを送る時にGBKコードで送信しないと、サーバー側で受信したデータが文字化けしてしまう可能性が高いです。同様に、クライアントがデータを受信する際に、サービス先でデータの符号化、すなわちUTF-8符号化でデータを受信しないと、データの文字化けが発生する可能性が高い。したがって、上記のサービス端末コードに対して、私達のプログラムが相手から送られてきたデータを読み取ることができるようにするために、文字化けが発生しないように、私達のクライアントはこのようにすべきです。
Javaコード

public class Client { 
 public static void main(String args[]) throws Exception { 
 //      ,            
 String host = "127.0.0.1"; //       IP   
 int port = 8899; //               
 //         
 Socket client = new Socket(host, port); 
 //                 
 Writer writer = new OutputStreamWriter(client.getOutputStream(), "GBK"); 
 writer.write("  ,   。"); 
 writer.write("eof
"); writer.flush(); // BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); // 10 client.setSoTimeout(10*1000); StringBuffer sb = new StringBuffer(); String temp; int index; try { while ((temp=br.readLine()) != null) { if ((index = temp.indexOf("eof")) != -1) { sb.append(temp.substring(0, index)); break; } sb.append(temp); } } catch (SocketTimeoutException e) { System.out.println(" 。"); } System.out.println(" : " + sb); writer.close(); br.close(); client.close(); } }