Javaソケットのガイド
1. 概要
socketprogrammingという用語は、デバイスがすべてネットワークを使用して相互に接続されている複数のコンピューター間で実行されるプログラムを作成することを指します。
ソケットプログラミングに使用できる通信プロトコルは2つあります:User Datagram Protocol (UDP) and Transfer Control Protocol (TCP)。
2つの主な違いは、UDPはコネクションレスであり、クライアントとサーバーの間にセッションがないことを意味しますが、TCPはコネクション指向です。つまり、通信を行うにはクライアントとサーバーの間に排他的な接続を確立する必要があります。
このチュートリアルでは、an introduction to sockets programming over TCP/IPネットワークを紹介し、Javaでクライアント/サーバーアプリケーションを作成する方法を示します。 UDPは主流のプロトコルではないため、頻繁に遭遇することはありません。
2. プロジェクトのセットアップ
Javaは、クライアントとサーバー間の低レベルの通信の詳細を処理するクラスとインターフェイスのコレクションを提供します。
これらは主にjava.netパッケージに含まれているため、次のインポートを行う必要があります。
import java.net.*;
また、通信中に書き込みおよび読み取りを行うための入力ストリームと出力ストリームを提供するjava.ioパッケージも必要です。
import java.io.*;
簡単にするために、クライアントプログラムとサーバープログラムを同じコンピューターで実行します。 異なるネットワークコンピュータでそれらを実行する場合、変更されるのはIPアドレスだけです。この場合、127.0.0.1でlocalhostを使用します。
3. 簡単な例
ほとんどのbasic of examples involving a client and a serverで手を汚しましょう。 これは、クライアントがサーバーに挨拶し、サーバーが応答する双方向通信アプリケーションになります。
次のコードを使用して、GreetServer.javaというクラスでサーバーアプリケーションを作成しましょう。
この記事では、すべてのサーバーをどのように実行するかに注意を引くために、mainメソッドとグローバル変数を含めます。 記事の残りの例では、この種のより反復的なコードを省略します。
public class GreetServer {
private ServerSocket serverSocket;
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void start(int port) {
serverSocket = new ServerSocket(port);
clientSocket = serverSocket.accept();
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String greeting = in.readLine();
if ("hello server".equals(greeting)) {
out.println("hello client");
}
else {
out.println("unrecognised greeting");
}
}
public void stop() {
in.close();
out.close();
clientSocket.close();
serverSocket.close();
}
public static void main(String[] args) {
GreetServer server=new GreetServer();
server.start(6666);
}
}
次のコードを使用して、GreetClient.javaというクライアントも作成しましょう。
public class GreetClient {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String ip, int port) {
clientSocket = new Socket(ip, port);
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
public String sendMessage(String msg) {
out.println(msg);
String resp = in.readLine();
return resp;
}
public void stopConnection() {
in.close();
out.close();
clientSocket.close();
}
}
IDEのLet’s start the server;は、Javaアプリケーションとして実行するだけでこれを実行できます。
次に、単体テストを使用してサーバーに挨拶を送信します。これにより、サーバーが実際に応答として挨拶を送信することが確認されます。
@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
GreetClient client = new GreetClient();
client.startConnection("127.0.0.1", 6666);
String response = client.sendMessage("hello server");
assertEquals("hello client", response);
}
ここで何が起こっているのかを完全に理解していなくても心配しないでください。この例は、記事の後半で何を期待できるかを理解するためのものです。
次のセクションでは、この簡単な例を使用してsocket communicationを分析し、さらに多くの例を使用して詳細を詳しく説明します。
4. ソケットのしくみ
上記の例を使用して、このセクションのさまざまな部分をステップスルーします。
定義上、socketは、ネットワーク上の異なるコンピューターで実行されている2つのプログラム間の双方向通信リンクの1つのエンドポイントです。 ソケットはポート番号にバインドされるため、トランスポート層はデータの送信先のアプリケーションを識別できます。
4.1. サーバー
通常、サーバーはネットワーク上の特定のコンピューターで実行され、特定のポート番号にバインドされたソケットを持っています。 この例では、クライアントと同じコンピューターを使用し、ポート6666でサーバーを起動しました。
ServerSocket serverSocket = new ServerSocket(6666);
サーバーは、クライアントが接続要求を行うためのソケットを待機するだけです。 これは次のステップで発生します。
Socket clientSocket = serverSocket.accept();
サーバーコードがacceptメソッドを検出すると、クライアントがサーバーコードに接続要求を行うまでブロックします。
すべてがうまくいけば、サーバーは接続をacceptsします。 受け入れられると、サーバーは同じローカルポート6666にバインドされた新しいソケットclientSocketを取得し、リモートエンドポイントもクライアントのアドレスとポートに設定されます。
この時点で、新しいSocketオブジェクトはサーバーをクライアントと直接接続し、出力ストリームと入力ストリームにアクセスして、クライアントとの間でそれぞれメッセージを送受信できます。
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
これ以降、サーバーは、ストリームがソケットを閉じるまで、クライアントとメッセージを無限に交換できます。
ただし、この例では、サーバーは接続を閉じる前に挨拶応答のみを送信できます。つまり、テストを再度実行すると、接続は拒否されます。
通信の継続性を可能にするには、whileループ内の入力ストリームから読み取り、クライアントが終了要求を送信したときにのみ終了する必要があります。これは次のセクションで動作します。
新しいクライアントごとに、サーバーはaccept呼び出しによって返される新しいソケットを必要とします。 serverSocketは、接続されたクライアントのニーズに対応しながら、接続要求をリッスンし続けるために使用されます。 最初の例ではまだこれを許可していません。
4.2. クライアント
クライアントは、サーバーが実行されているマシンのホスト名またはIPと、サーバーがリッスンしているポート番号を知っている必要があります。
接続要求を行うために、クライアントはサーバーのマシンとポートでサーバーとのランデブーを試みます。
Socket clientSocket = new Socket("127.0.0.1", 6666);
また、クライアントはサーバーに対して自分自身を識別する必要があるため、システムが割り当てたローカルポート番号にバインドし、この接続中に使用します。 私たちはこれに自分たちで対処しません。
上記のコンストラクターは、サーバーにacceptedの接続がある場合にのみ新しいソケットを作成します。そうでない場合、接続拒否の例外が発生します。 作成に成功すると、そこから入力および出力ストリームを取得してサーバーと通信できます。
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
サーバーの入力ストリームがクライアントの出力ストリームに接続されているように、クライアントの入力ストリームはサーバーの出力ストリームに接続されています。
5. 継続的なコミュニケーション
現在のサーバーは、クライアントが接続するまでブロックし、クライアントからのメッセージをリッスンするために再度ブロックします。単一のメッセージの後、継続性を処理していないため接続を閉じます。
したがって、pingリクエストでのみ役立ちますが、チャットサーバーを実装したい場合、サーバーとクライアント間の継続的な往復通信が確実に必要になることを想像してください。
受信メッセージのサーバーの入力ストリームを継続的に監視するには、whileループを作成する必要があります。
クライアントから受信したメッセージをエコーバックすることを唯一の目的とするEchoServer.javaという新しいサーバーを作成しましょう。
public class EchoServer {
public void start(int port) {
serverSocket = new ServerSocket(port);
clientSocket = serverSocket.accept();
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
if (".".equals(inputLine)) {
out.println("good bye");
break;
}
out.println(inputLine);
}
}
ピリオド文字を受け取ったときにwhileループが終了する終了条件を追加したことに注意してください。
GreetServerの場合と同じように、mainメソッドを使用してEchoServerを開始します。 今回は、混乱を避けるために、4444などの別のポートで開始します。
EchoClientはGreetClientに似ているため、コードを複製できます。 明確にするためにそれらを分離しています。
別のテストクラスで、サーバーがソケットを閉じることなく、EchoServerへの複数の要求が処理されることを示すテストを作成します。 これは、同じクライアントからリクエストを送信している限り当てはまります。
複数のクライアントを扱うことは別のケースであり、これについては次のセクションで説明します。
サーバーとの接続を開始するためのsetupメソッドを作成しましょう。
@Before
public void setup() {
client = new EchoClient();
client.startConnection("127.0.0.1", 4444);
}
すべてのリソースを解放するためにtearDownメソッドを等しく作成します。これは、ネットワークリソースを使用するすべての場合のベストプラクティスです。
@After
public void tearDown() {
client.stopConnection();
}
次に、いくつかのリクエストでエコーサーバーをテストしましょう。
@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
String resp1 = client.sendMessage("hello");
String resp2 = client.sendMessage("world");
String resp3 = client.sendMessage("!");
String resp4 = client.sendMessage(".");
assertEquals("hello", resp1);
assertEquals("world", resp2);
assertEquals("!", resp3);
assertEquals("good bye", resp4);
}
これは、サーバーが接続を閉じる前に1回だけ通信する最初の例からの改善です。 now we send a termination signal to tell the server when we’re done with the session。
6. 複数のクライアントを持つサーバー
前の例が最初の例よりも改善されたのと同じように、それはまだそれほど素晴らしい解決策ではありません。 サーバーには、多くのクライアントと多くの要求を同時に処理する能力が必要です。
このセクションでは、複数のクライアントの処理について説明します。
ここで説明する別の機能は、同じクライアントがサーバーで接続拒否例外または接続リセットを取得することなく、再度切断および再接続できることです。 以前は、これを行うことができませんでした。
これは、サーバーが複数のクライアントからの複数の要求に対してより堅牢で回復力があることを意味します。
これを行う方法は、新しいクライアントごとに新しいソケットを作成し、クライアントが別のスレッドで要求するサービスを提供することです。 同時に処理されるクライアントの数は、実行中のスレッドの数と等しくなります。
メインスレッドは、新しい接続をリッスンするときにwhileループを実行します。
十分に話して、EchoMultiServer.java.という別のサーバーを作成しましょう。その中に、ソケット上の各クライアントの通信を管理するためのハンドラスレッドクラスを作成します。
public class EchoMultiServer {
private ServerSocket serverSocket;
public void start(int port) {
serverSocket = new ServerSocket(port);
while (true)
new EchoClientHandler(serverSocket.accept()).start();
}
public void stop() {
serverSocket.close();
}
private static class EchoClientHandler extends Thread {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClientHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
if (".".equals(inputLine)) {
out.println("bye");
break;
}
out.println(inputLine);
}
in.close();
out.close();
clientSocket.close();
}
}
whileループ内でacceptを呼び出すことに注意してください。 whileループが実行されるたびに、新しいクライアントが接続するまでaccept呼び出しをブロックし、次にこのクライアント用にハンドラースレッドEchoClientHandlerが作成されます。
スレッド内で発生するのは、以前にEchoServerで実行したことであり、単一のクライアントのみを処理していました。 したがって、EchoMultiServerはこの作業をEchoClientHandlerに委任して、whileループでより多くのクライアントをリッスンし続けることができるようにします。
サーバーのテストには引き続きEchoClientを使用します。今回は、サーバーから複数のメッセージを送受信する複数のクライアントを作成します。
ポート5555でmainメソッドを使用してサーバーを起動しましょう。
明確にするために、テストを新しいスイートに配置します。
@Test
public void givenClient1_whenServerResponds_thenCorrect() {
EchoClient client1 = new EchoClient();
client1.startConnection("127.0.0.1", 5555);
String msg1 = client1.sendMessage("hello");
String msg2 = client1.sendMessage("world");
String terminate = client1.sendMessage(".");
assertEquals(msg1, "hello");
assertEquals(msg2, "world");
assertEquals(terminate, "bye");
}
@Test
public void givenClient2_whenServerResponds_thenCorrect() {
EchoClient client2 = new EchoClient();
client2.startConnection("127.0.0.1", 5555);
String msg1 = client2.sendMessage("hello");
String msg2 = client2.sendMessage("world");
String terminate = client2.sendMessage(".");
assertEquals(msg1, "hello");
assertEquals(msg2, "world");
assertEquals(terminate, "bye");
}
これらのテストケースをいくつでも作成でき、それぞれが新しいクライアントを生成し、サーバーがそれらすべてを処理します。
7. 結論
このチュートリアルでは、an introduction to sockets programming over TCP/IPに焦点を当て、Javaで簡単なクライアント/サーバーアプリケーションを作成しました。
記事の完全なソースコードは、いつものように、GitHubプロジェクトにあります。