JavaとJavaScriptでwebブラウザとのソケット通信①

# 背景

[更新]：[APIを使用して簡単に実装した](https://qiita.com/take4eng/items/ca6d25ac9bdd33d6bb67)

ネット上で多人数同時通信のものを作成するため、ソケット通信について実践したことを記述。 
チャットやオンラインゲームのような、複数でのリアルタイム通信を希望。

「webブラウザ」で実行できるようなプログラムを「Java」で組みたかったのだが、ググってもあまり出てこないので記述しておく。
(知識不足でネットワークプログラミングという言葉も知らなかったため、ググり方に問題があるかも…)  

データの読み書き等の超基本的なところからスタート。

## ソケット通信
通信方法の1種。
サーバーとクライアントに対して、HTTPのgetアクセスやpostアクセスのように単一方向への通信ではなく、リアルタイムでの双方向通信が可能。
⇒ サーバープログラムとクライアントプログラムの2つが必要。  

詳しくはググってほしい。

# 実行環境
- windows10
- eclipse (Tomcat8.5)
- Google Chrome

# 言語
サーバープログラム：Java
クライアントプログラム：JavaScript

# 成果物
1. webブラウザで動くチャットアプリ

# 開発手順
1. クライアント側のプログラムを組む
2. サーバー側のプログラムを組む
3. クライアントプログラムからサーバーへアクセス
4. サーバーからの応答があればOK

# 実践項目
1. クライアントエコーによるソケット通信
2. 単一クライアントとサーバーのチャットアプリ
3. 複数クライアントとサーバーのチャットアプリ

##  1. クライアントエコーによるソケット通信
クライアント側のプログラムはJavaScriptで記述する。
ここはほぼパクり。
サーバー側のプログラムは必要なし。

```html
<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<title>WebSocket通信</title>
		<script type="text/javascript">
		
			var wSck= new WebSocket("echo.websocket.org");// WebSocketオブジェクト生成
			
			wSck.onopen = function() {//ソケット接続時のアクション
				document.getElementById('show').innerHTML += "接続しました。" + "<br/>";
			};
			
			wSck.onmessage = function(e) {//メッセージを受け取ったときのアクション
				document.getElementById('show').innerHTML += e.data + "<br/>";
			};
			
			var sendMsg = function(val) {//メッセージを送信するときのアクション
				var line = document.getElementById('msg');//入力内容を取得
				wSck.send(line.value);//ソケットに送信
				line.value = "";//内容をクリア
			};
		</script>
	</head>

	<body>
		<div
			style="width: 500px; height: 200px; overflow-y: auto; border: 1px solid #333;"
			id="show"></div>
		<input type="text" size="80" id="msg" name="msg" />
		<input type="button" value="送信" onclick="sendMsg();" />
	</body>
</html>
```

### コードの解説
1．Webソケットオブジェクトを生成し、引数にアドレスを代入。
`var 変数 = new WebSocket("ws://IPアドレス:ポート番号");`

今回はアドレスに「echo.websocket.org」を使用。
⇒クライアントから送信したものをそのまま返してくれる。
　サーバープログラムを作成しなくてもソケット通信が可能なため便利。

2．ソケット接続時のアクションを記述
`ソケット.onopen = function() {・・・};`
ソケット接続に成功したら、まずこの関数が実行される。

3．メッセージを受け取ったときのアクションを記述
`ソケット.onmessage = function() {・・・};`
サーバーからデータが送られて来たらこの関数が実行される。

4．メッセージを送る内容を記述
`ソケット.send(・・・);`

### 実行結果
![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/99cbeead-96c2-6381-61da-3a152a5aa3c5.jpeg)
![キャプチャ2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/84e69eb2-dbd1-72ae-0da2-66176c0672f1.jpeg)
![キャプチャ3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/76d4ab55-489b-6687-247b-1b7fbceaa4d1.jpeg)


送信した内容がそのまま架空サーバーから送り返されて表示される。

## 2. 単一クライアントとサーバーのチャットアプリ
架空サーバーじゃなく、実際にサーバーを作成してクライアントからの送信内容をそのまま返信する。
サーバー側のプログラムをJavaで記述する。

```Java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;

class SocketFree {

	private static ServerSocket sSck;// サーバー用のソケット
	private static Socket sck;// 受付用のソケット
	private static InputStream is;// 入力ストリーム
	private static InputStreamReader isr;// 入力ストリームを読み込む
	private static BufferedReader in;// バッファリングによるテキスト読み込み
	private static OutputStream os;// 出力ストリーム
	
	public static void main(String[] args) {
		try {
			sSck=new ServerSocket(60000);//サーバーソケットのインスタンスを作成(ポートは60000)
			System.out.println("サーバーに接続したよ！");
			sck=sSck.accept();//接続待ち。来たらソケットに代入。
			System.out.println( "参加者が接続しました！");
			
			// 必要な入出力ストリームを作成する
			is=sck.getInputStream();//ソケットからの入力をバイト列として読み取る
			isr=new InputStreamReader(is);//読み取ったバイト列を変換して文字列を読み込む
			in=new BufferedReader(isr);//文字列をバッファリングして(まとめて)読み込む
			os=sck.getOutputStream();//ソケットにバイト列を書き込む
			
			//クライアントへ接続許可を返す(ハンドシェイク)
			handShake(in , os);
			
			//ソケットへの入力をそのまま返す　※125文字まで(126文字以上はヘッダーのフレームが変化する)
			echo(is , os);
			
		} catch (Exception e) {
			System.err.println("エラーが発生しました: " + e);
		}
	}
	
	//クライアントへ接続許可を返すメソッド(ハンドシェイク)
	public static void handShake(BufferedReader in , OutputStream os){
		String header = "";//ヘッダーの変数宣言
		String key = "";//ウェブソケットキーの変数宣言
		try {
			while (!(header = in.readLine()).equals("")) {//入力ストリームから得たヘッダーを文字列に代入し、全行ループ。
				System.out.println(header);//1行ごとにコンソールにヘッダーの内容を表示
				String[] spLine = header.split(":");//1行を「:」で分割して配列に入れ込む
				if (spLine[0].equals("Sec-WebSocket-Key")) {//Sec-WebSocket-Keyの行
					key = spLine[1].trim();//空白をトリムし、ウェブソケットキーを入手
				}
			}
			key +="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";//キーに謎の文字列を追加する
			byte[] keyUtf8=key.getBytes("UTF-8");//キーを「UTF-8」のバイト配列に変換する
			MessageDigest md = MessageDigest.getInstance("SHA-1");//指定されたダイジェスト・アルゴリズムを実装するオブジェクトを返す
			byte[] keySha1=md.digest(keyUtf8);//キー(UTF-8)を使用してダイジェスト計算を行う
			Encoder encoder = Base64.getEncoder();//Base64のエンコーダーを用意
			byte[] keyBase64 = encoder.encode(keySha1);//キー(SHA-1)をBase64でエンコード
			String keyNext = new String(keyBase64);//キー(Base64)をStringへ変換
			byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
		            + "Connection: Upgrade\r\n"
		            + "Upgrade: websocket\r\n"
		            + "Sec-WebSocket-Accept: "
		            + keyNext
		            + "\r\n\r\n")
		            .getBytes("UTF-8");//HTTP レスポンスを作成
			os.write(response);//HTTP レスポンスを送信
		} catch (IOException e) {
			System.err.println("エラーが発生しました: " + e);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("エラーが発生しました: " + e);
		}
	}

	//ソケットへの入力を無限ループで監視するメソッド
	public static void echo(InputStream is , OutputStream os) {
		try{
			while(true) {
				byte[] buff = new byte[1024];//クライアントから送られたバイナリデータを入れる配列
				int lineData =is.read(buff);//データを読み込む
				for (int i = 0; i < lineData - 6; i++) {
					buff[i + 6] = (byte) (buff[i % 4 + 2] ^ buff[i + 6]);//7バイト目以降を3-6バイト目のキーを用いてデコード
				}
				String line = new String(buff, 6, lineData - 6, "UTF-8");//デコードしたデータを文字列に変換
				byte[] sendHead = new byte[2];//送り返すヘッダーを用意
				sendHead[0] = buff[0];//1バイト目は同じもの
				sendHead[1] = (byte) line.getBytes("UTF-8").length;//2バイト目は文字列の長さ
				os.write(sendHead);//ヘッダー出力
				os.write(line.getBytes("UTF-8"));//3バイト目以降に文字列をバイナリデータに変換して出力
				
				if (line.equals("bye")) break;//「bye」が送られたなら受信終了
			}
		} catch (Exception e)  {
			System.err.println("エラーが発生しました: " + e);
		}
	}
}
```

### コードの解説
1．ソケットやストリーム等の必要な変数を宣言。

2．サーバーソケットのインスタンスを作成。
`ServerSocket 変数 = new ServerSocket(ポート番号)`

3．acceptメソッドでクライアントからの接続を待機。

4．接続されたクライアントの入出力ストリームを作成する。

5．クライアントからの接続要求に対してレスポンスを返す。(handShakeメソッド)
>リクエストのヘッダーに必要な情報が記述されているため、編集して送り返す。
編集内容の詳細はコードのコメント及び参照サイトを参考。
要は、リクエストのヘッダーにある『WebSocketキー』を送り返す必要がある。
⇒ **『ハンドシェイク』**

6．ソケット接続後、無限ループでクライアントからの入力をそのまま返す。(echoメソッド)
>クライアントから送信されたデータはバイナリデータとしてエンコードされている。
サーバー側でデコードし、再度エンコードしてクライアントへ送信する。
デコード内容の詳細はコードのコメント及び参照サイトを参考。
※送信された文字列の長さによってヘッダーのフレームが変化するので、ここでは簡単のため125文字以下に限定する。


7．クライアントから「bye」が送信されるとループを抜け受信終了

### 実行結果

1. サーバープログラムを起動
2. ポート番号を指定し、localhostへ接続  

![キャプチャ3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/1031acc1-4e72-192a-1b00-1cff27e85f32.jpeg)

※クライアントプログラムのアドレスを変更しておく(今回は60000番)
`var 変数 = new WebSocket("ws://127.0.0.1:60000");`

架空サーバー(echo.websocket.org)を使用したときと同様の結果が得られた。
今回はエコーするだけだが、サーバー側でデータをいろいろ弄って返信することも可能。

## 複数クライアントとサーバーのチャットアプリ
サーバープログラムを編集し、複数のクライアントからの接続を許可する。
また、各クライアント間でリアルタイム通信となるようにする。

```Java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;

class SocketFree {

	private static ServerSocket sSck;// サーバー用のソケット
	private static Socket[] sck;// 受付用のソケット
	private static InputStream[] is;// 入力ストリーム
	private static InputStreamReader[] isr;// 入力ストリームを読み込む
	private static BufferedReader[] in;// バッファリングによるテキスト読み込み
	private static OutputStream[] os;// 出力ストリーム
	private static ClientThread user[];//各クライアントのインスタンス
	private static int member;// 接続しているメンバーの数
	
	public static void main(String[] args) {
		int n=0;
		int maxUser=100;
		//各フィールドの配列を用意
		sck=new Socket[maxUser];
		is=new InputStream[maxUser];
		isr=new InputStreamReader[maxUser];
		in=new BufferedReader[maxUser];
		os=new OutputStream[maxUser];
		user=new ClientThread[maxUser];
				
		try {
				sSck=new ServerSocket(60000);//サーバーソケットのインスタンスを作成(ポートは60000)
				System.out.println("サーバーに接続したよ！");
				while(true) {
					sck[n]=sSck.accept();//接続待ち。来たらソケットに代入。
					System.out.println( (n+1)+"番目の参加者が接続しました！");
					
					// 必要な入出力ストリームを作成する
					is[n]=sck[n].getInputStream();//ソケットからの入力をバイト列として読み取る
					isr[n]=new InputStreamReader(is[n]);//読み取ったバイト列を変換して文字列を読み込む
					in[n]=new BufferedReader(isr[n]);//文字列をバッファリングして(まとめて)読み込む
					os[n]=sck[n].getOutputStream();//ソケットにバイト列を書き込む
					
					//クライアントへ接続許可を返す(ハンドシェイク)
					handShake(in[n] , os[n]);
					
					//各クライアントのスレッドを作成
					user[n] = new ClientThread(n , sck[n] , is[n] , isr[n] , in[n] , os[n]);
					user[n].start();
					
					member=n+1;//接続数の更新
					n++;//次の接続者へ
				}
			} catch (Exception e) {
				System.err.println("エラーが発生しました: " + e);
		}
	}
	
	//クライアントへ接続許可を返すメソッド(ハンドシェイク)
	public static void handShake(BufferedReader in , OutputStream os){
		String header = "";//ヘッダーの変数宣言
		String key = "";//ウェブソケットキーの変数宣言
		try {
			while (!(header = in.readLine()).equals("")) {//入力ストリームから得たヘッダーを文字列に代入し、全行ループ。
				System.out.println(header);//1行ごとにコンソールにヘッダーの内容を表示
				String[] spLine = header.split(":");//1行を「:」で分割して配列に入れ込む
				if (spLine[0].equals("Sec-WebSocket-Key")) {//Sec-WebSocket-Keyの行
					key = spLine[1].trim();//空白をトリムし、ウェブソケットキーを入手
				}
			}
			key +="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";//キーに謎の文字列を追加する
			byte[] keyUtf8=key.getBytes("UTF-8");//キーを「UTF-8」のバイト配列に変換する
			MessageDigest md = MessageDigest.getInstance("SHA-1");//指定されたダイジェスト・アルゴリズムを実装するオブジェクトを返す
			byte[] keySha1=md.digest(keyUtf8);//キー(UTF-8)を使用してダイジェスト計算を行う
			Encoder encoder = Base64.getEncoder();//Base64のエンコーダーを用意
			byte[] keyBase64 = encoder.encode(keySha1);//キー(SHA-1)をBase64でエンコード
			String keyNext = new String(keyBase64);//キー(Base64)をStringへ変換
			byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
		            + "Connection: Upgrade\r\n"
		            + "Upgrade: websocket\r\n"
		            + "Sec-WebSocket-Accept: "
		            + keyNext
		            + "\r\n\r\n")
		            .getBytes("UTF-8");//HTTP レスポンスを作成
			os.write(response);//HTTP レスポンスを送信
		} catch (IOException e) {
			System.err.println("エラーが発生しました: " + e);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("エラーが発生しました: " + e);
		}
	}
	
	//各クライアントからのデータを全クライアントに送信するメソッド
	public static void sendAll(int number , byte[] sendHead , String line){
		try {
			for (int i = 0; i <member ; i++) {
				os[i].write(sendHead);//ヘッダー出力
				os[i].write(line.getBytes("UTF-8"));//3バイト目以降に文字列をバイナリデータに変換して出力
				System.out.println((i+1)+"番目に"+(number+1)+"番目のメッセージを送りました！" );
			}
		} catch (IOException e) {
			System.err.println("エラーが発生しました: " + e);
		}
	}
}

class ClientThread extends Thread{
	//各クライアントのフィールド
	private int myNumber;
	private Socket mySck;
	private InputStream myIs;
	private InputStreamReader myIsr;
	private BufferedReader myIn;
	private OutputStream myOs;
	
	//コンストラクタでインスタンスのフィールドに各値を代入
	public ClientThread(int n , Socket sck , InputStream is , InputStreamReader isr , BufferedReader in , OutputStream os) {
		myNumber=n;
		mySck=sck;
		myIs=is;
		myIsr=isr;
		myIn=in;
		myOs=os;
	}
	
	//Threadクラスのメイン
	public void run() {
		try {
			echo(myIs , myOs , myNumber);
		} catch (Exception e) {
			System.err.println("エラーが発生しました: " + e);
		}
	}
	
	//ソケットへの入力を無限ループで監視する　※125文字まで(126文字以上はヘッダーのフレームが変化する)
	public void echo(InputStream is , OutputStream os , int myNumber) {
		try{
			while(true) {
				byte[] buff = new byte[1024];//クライアントから送られたバイナリデータを入れる配列
				int lineData =is.read(buff);//データを読み込む
				for (int i = 0; i < lineData - 6; i++) {
					buff[i + 6] = (byte) (buff[i % 4 + 2] ^ buff[i + 6]);//7バイト目以降を3-6バイト目のキーを用いてデコード
				}
				String line = new String(buff, 6, lineData - 6, "UTF-8");//デコードしたデータを文字列に変換
				byte[] sendHead = new byte[2];//送り返すヘッダーを用意
				sendHead[0] = buff[0];//1バイト目は同じもの
				sendHead[1] = (byte) line.getBytes("UTF-8").length;//2バイト目は文字列の長さ
				
				SocketFree.sendAll(myNumber , sendHead , line);//各クライアントへの送信は元クラスのsedAllメソッドで実行
				
				if (line.equals("bye")) break;//「bye」が送られたなら受信終了
			}
		} catch (Exception e)  {
			System.err.println("エラーが発生しました: " + e);
		}
	}
}
```

### コードの解説
1．クライアントのナンバリングや接続数を把握するためのフィールド変数を追加。

2．複数のクライアントに対応するため、各フィールドを配列形式にする。
main関数で配列の初期化を行い、これまでの変数を編集。

3．複数クライアントを同時並行で処理するため、Threadクラスを継承したClientThreadクラスを作成する。
ハンドシェイクまではmain関数内で処理。
それ以降は各クライアント毎にClientThreadインスタンスを作成して並行処理。
>Threadクラス：main関数から分岐して同時に処理することができる。
.start();でrun()が実行される謎仕様。

4．ClientThreadクラスにecho関数を移動。
データ受信 ⇒ デコード ⇒ データ送信
この内データ送信はmain関数が所属するクラスで実行するため、新しくsendAllメソッドを作成。
※データ送信は全クライアントに対して実行。
　全クライアントを扱っているのはmain関数のクラス。

5．その他、メソッドの型や変数名等の細かい修正。


### 実行結果

1. サーバープログラムを起動
2. 複数のブラウザからlocalhostへ接続  

![キャプチャ5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/0b60e0e4-e223-5b7c-86db-098035899159.jpeg)

1つのクライアントから送信すると、全てのブラウザで同じテキストが表示される。
どのクライアントからテキストを送信しても表示結果は同じ。

# その他
## 改善点
・ブラウザを閉じたときのエラー処理
「bye」を送信すると監視のループを抜けてエラーなく終了できるが、監視中にブラウザを閉じるとエラーが発生しサーバーが停止してしまう。

・closeメソッドの追記
本来はソケット通信を終了する際はcloseメソッドを使用して切断するが、今回は使用していない。

・デコード処理の強化
デコードの処理を甘えた為、125文字以下の通信しかできない。
126文字以上になるとwebソケットのフレームが変化するため、場合分けが必要になってくる。

・セキュリティ関連の強化
今回は何も対策しておらずセキュリティがガバガバなので、送受信の方法を考える必要がある。

## 備考
・ポートの解放
プログラム内で指定したポートが既に使用されている場合、サーバー起動時にエラーが発生する。
1. コマンドプロンプトで`>netstat -ano`を実行
2. 指定したローカルアドレスからPIDを確認
3. タスクマネージャーのプロセスからPDIのアプリケーションを確認
4. 必要ない物ならプロセスを終了してポートを開放
![キャプチャ6.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634805/782aa6ac-8a8d-e2ee-f650-906f6e4b6fd8.jpeg)

・ハンドシェイク
ソケット通信を行う前に、クライアントとサーバー間で合意を取る必要がある。
クライアントからサーバーへ接続要求があるなら、それに対してレスポンスを返す必要がある。
レスポンスの内容は参考ページに詳しく記述されている。

・受信データのデコード
webソケットのデータにはフレームフォーマットが存在し、そのフレームに沿ってバイナリデータが送信されている。
データをデコードするには、フレーム構造を理解して1バイト毎に変換していく必要がある。
フレームフォーマットの詳細やデコード方法は参考ページに詳しく記述されている。

## 感想
思った以上にややこしい。

ググってもなかなか出てこないし、マイナーな方法？
よく見かけるのは、サーバープログラムを「Node.js」で記述したもの。
ドットインストールの動画ではNode.jsで作成しており、レスポンスやデコード等の複雑な処理はしていなかった。
多分、Node.jsの勉強をした方が早い。

Javaでも、アノテーションを利用するとデコード処理が無くても通信できるような記事も見かけたが、今回はパスした。
パスしない方が良かったかも。

プログラミングの勉強をしていたはずが、いつの間にかプロトコルの勉強になっていた・・・
初学者が首を突っ込むところじゃない気がしたが、勉強になったので良し。

間違えているところがあったらスマン。
気が付けば修正する。
改善すべき点もかなり多いので、可能なら更新していく。

# 参考ページ
- [かなりマネさせてもらったサイト](http://javait.hatenablog.com/entry/2016/02/20/193338)
- [複数クライアントの対応が分かりやすいサイト](https://yoslab.net/netprog/index.php)
- [レスポンスヘッダーとデコードについて詳しいサイト1](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_Java)
- [レスポンスヘッダーとデコードについて詳しいサイト2](http://jxck.hatenablog.com/entry/20120725/1343174392)
- [webソケット全般に詳しいサイト](https://www.jstage.jst.go.jp/article/itej/67/2/67_102/_pdf/-char/ja)
- [webソケット全般+アノテーション使ってるサイト](http://labs.opentone.co.jp/?p=2522)
- [解説少なめだがかなり細かいところまで作成しているサイト](http://sanofc.blogspot.com/2014/08/javawebsocket.html)
- [JavaScriptでのクライアントプラグラム作成](https://www.sejuku.net/blog/70583)