【Java】TCP通信を利用したデータ送受信

ゲームでも通信が使われることはあるなぁ。と思ったので本稿は基本的なTCP通信を実装する方法を紹介します。




TCP通信

TCP通信は1対1でデータを送受信できる基本的な通信手段の一つです。信頼性が必要な通信をする場合はTCP通信で実装します。コネクションを確立し、データの送受信をします。信頼性は確保されますが、UDP通信より処理速度が遅くなります。

例えばオンラインゲーム等で使うとしたら、ログイン処理等でしょうか。後はリアルタイム性が求められないものに関してもこちらが使われそうです。

皆さんがブラウザで見ているインターネット上のWebページを表示する場合などにもこのTCP通信が使用されています。

はい、座学には興味ないのでさっさとプログラムのほうに入っちゃいます。

Socketクラス

Socketクラスはサーバーへの接続や、データの送受信を行うことができます。

Socket sock = new Socket(IPアドレスまたはホスト名,ポート番号);

と指定すると指定の端末、ポート番号に接続を開始します。

接続した後はgetInputStreamメソッドで受信ストリームを取得できます。

InputStream in = getInputStream();
in.read(受信データを受け取るバイト配列);

接続した後にgetOutputStreamメソッドで送信ストリームを取得できます。

OutputStream out = OutputStream();
out.write(送信データバイト配列);

Socketクラスで必ず使用するのは主にこんなところでしょう。

ServerSocketクラス

ServerSocketクラスはサーバー側となるプログラムで接続要求を待ち受けるための機能があります。インスタンス化する際に接続要求を受けるポートを指定するだけです。

ServerSocket svsock = new ServerSocket(ポート番号);

インスタンス化したらacceptで接続待機を行います。このメソッドを実行すると接続要求があるまでスレッドが停止します。そして接続要求があったら、クライアントとデータをやり取りするためのSocketオブジェクトを取得できます。ここまで来たら、普通にgetOutputStream、getInputStreamを使用することで送受信できますね。

Socket sock = svsock.accept();//接続待ち
InputStream in = sock.getInputStream();

基本的な送受信サンプルプログラム

それでは、さっそくPC同士でメッセージをやり取りするプログラムを作っちゃいましょう。

Server.javaを実行すると接続待機状態となり、Client.javaを実行すると接続して、送信し、切断するという流れになります。

念のためServer.javaを実行してからClient.javaを実行してください。

Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Client {

	public static void main(String[] args) {
		try{
			//送信先のIPアドレス(ドメインなどの名前)とポートを指定
			Socket sock = new Socket("localhost",10000);
			
			//送信ストリームの取得
			OutputStream out = sock.getOutputStream();
			
			//送信データ
			String sendData = "てすとですよ";
			
			//文字列をUTF-8形式のバイト配列に変換して送信
			out.write(sendData.getBytes("UTF-8"));
			
			//送信データの表示
			System.out.println("「"+sendData+"」を送信しました。");
			
			//送信ストリームを表示
			out.close();
			
			//終了
			sock.close();
		
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
Server.java
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;

public class Server {

	public static void main(String[] args) {
		try{
			//サーバーのポート番号を指定
			ServerSocket svSock = new ServerSocket(10000);
			
			//アクセスを待ち受け
			Socket sock = svSock.accept();
			
			//受信データバッファ
			byte[] data = new byte[1024];
			
			//受信ストリームの取得
			InputStream in = sock.getInputStream();
			
			//データを受信
			int readSize = in.read(data);
			
			//受信データを読み込んだサイズまで切り詰め
			data = Arrays.copyOf(data, readSize);
			
			//バイト配列を文字列に変換して表示
			System.out.println("「"+new String(data,"UTF-8")+"」を受信しました。");
			
			//受信ストリームの終了
			in.close();
			
			//サーバー終了
			svSock.close();
			
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
Client.java実行結果

「てすとですよ」を送信しました。

Server.java実行結果

「てすとですよ」を受信しました。

テストプログラムでは同一PCで動作させることを前提としていますが、別PC間でも当然やり取りできます。Client.javaのIPアドレスをサーバーPCのIPアドレスにしてみましょう。

PCのIPアドレスの調べかたはこちらを参照してください。

ファイアウォールで通信が禁止されている場合は、ポート解放するか、一時的に無効化して試してみましょう。

ファイアウォールを一時的に無効化する場合はこちらを参照ください。

テスト後は必ずファイアウォールを有効化しておいてください。

バイト配列をベースとした送受信なので、主に画像を送信したい場合や音声を送信したい場合はこの方法を元にプログラムすることになります。

int型やString型を簡単に送受信する方法

int型やString型、double等の基本型と文字列型は専用のストリーム(DataInputStreamやDataOutputStream)にラップしてやることで簡単に処理できるようになります。試しにいろいろ送受信してみるサンプルを作りました。

Client.java
import java.io.IOException;
import java.io.DataOutputStream;
import java.net.Socket;

public class Client {

	public static void main(String[] args) {
		try{
			//送信先のIPアドレス(ドメインなどの名前)とポートを指定
			Socket sock = new Socket("localhost",10000);
			
			//送信ストリームの取得(DataOutputStreamでラップ)
			DataOutputStream out = new DataOutputStream(sock.getOutputStream());
			
			//送信データ
			int intData = 100;
			String strData = "消炭にしてやるんだから!";
			double dblData = 1.983;
			
			//int型送信
			out.writeInt(intData);
			
			//String型送信
			out.writeUTF(strData);
			
			//double型送信
			out.writeDouble(dblData);
			
			System.out.println("基本型を送信しました。");
			
			//送信ストリームを表示
			out.close();
			
			//終了
			sock.close();
		
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
Server.java
import java.io.IOException;
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

	public static void main(String[] args) {
		try{
			//サーバーのポート番号を指定
			ServerSocket svSock = new ServerSocket(10000);
			
			//アクセスを待ち受け
			Socket sock = svSock.accept();
			
			//受信ストリームの取得(DataInputStreamでラップ)
			DataInputStream in = new DataInputStream(sock.getInputStream());
			
			//int型データを受信
			int intData = in.readInt();
			
			//String型データを受信
			String strData = in.readUTF();
			
			//double型データを受信
			double dblData = in.readDouble();
			
			//受信データの表示
			System.out.println("「"+intData+"」を受信しました。");
			System.out.println("「"+strData+"」を受信しました。");
			System.out.println("「"+dblData+"」を受信しました。");
			
			//受信ストリームの終了
			in.close();
			
			//サーバー終了
			svSock.close();
			
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
Client.java実行結果

基本型を送信しました。

Server.java実行結果

「100」を受信しました。
「消炭にしてやるんだから!」を受信しました。
「1.983」を受信しました。

このサンプルではint、String、doubleを順番に送受信してみました。

もちろん他の基本型を送受信するメソッドもあるので下記の公式ドキュメントを参考にしてみてください。

DataInputStream
DataOutputStream

※通信以外でも使えるので機会があったら使ってみてください。

クラスオブジェクトを送受信する方法

クラスオブジェクトを直接送受信する手段も存在します。基本的には上記で紹介した方法と同じようにラップするだけです。ラップするのはObjectInputStream、ObjectOutputStreamです。ただし対象となるデータはSerializableインターフェースを実装している必要があります。

それではこちらのサンプルも見てみましょう。

Client.java
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;

public class Client {

	public static void main(String[] args) {
		try{
			//送信先のIPアドレス(ドメインなどの名前)とポートを指定
			Socket sock = new Socket("localhost",10000);
			
			//送信ストリームの取得(ObjectOutputStreamでラップ)
			ObjectOutputStream out = new ObjectOutputStream(sock.getOutputStream());
			
			//送信データ
			CharaData reimu = new CharaData("れいむ",4700,4500);
			CharaData sharuthia = new CharaData("しゃるてぃあ",5800,3800);
			CharaData watashi = new CharaData("わー!ワタシのスマートフォンがぁぁ!!",1,1);
			
			//キャラデータ送信
			out.writeObject(reimu);
			out.writeObject(sharuthia);
			out.writeObject(watashi);
			
			System.out.println("キャラデータを送信しました。");
			
			//送信ストリームを表示
			out.close();
			
			//終了
			sock.close();
		
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
Server.java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

	public static void main(String[] args) {
		try{
			//サーバーのポート番号を指定
			ServerSocket svSock = new ServerSocket(10000);
			
			//アクセスを待ち受け
			Socket sock = svSock.accept();
			
			//受信ストリームの取得(ObjectInputStreamでラップ)
			ObjectInputStream in = new ObjectInputStream(sock.getInputStream());
			
			//キャラデータ受信
			CharaData reimu = (CharaData)in.readObject();
			CharaData sharuthia = (CharaData)in.readObject();
			CharaData watashi = (CharaData)in.readObject();
			
			//受信データの表示
			System.out.println("「"+reimu+"」を受信しました。");
			System.out.println("「"+sharuthia+"」を受信しました。");
			System.out.println("「"+watashi+"」を受信しました。");
			
			//受信ストリームの終了
			in.close();
			
			//サーバー終了
			svSock.close();
			
		}catch(IOException e){
			e.printStackTrace();
		}catch(ClassNotFoundException e){
			e.printStackTrace();
		}
	}
}
CharaData.java
import java.io.Serializable;

public class CharaData implements Serializable{
	public String name;
	public int hp;
	public int mp;
	public CharaData(String name, int hp, int mp){
		this.name = name;
		this.hp = hp;
		this.mp = mp;
	}
	public String toString(){
		return "name:"+name+" HP:"+hp+" MP:"+mp;
	}
}
Client.java実行結果

キャラデータを送信しました。

Server.java実行結果

「name:れいむ HP:4700 MP:4500」を受信しました。
「name:しゃるてぃあ HP:5800 MP:3800」を受信しました。
「name:わー!ワタシのスマートフォンがぁぁ!! HP:1 MP:1」を受信しました。

サンプルではCharaData.javaというクラスデータを送受信しました。

この記事で紹介したサンプルですが実際ゲームのデータのやり取りで利用すると速度に問題が出てくるかもしれません。

そんなときはBufferedOutputStream、BufferedInputStreamをさらにラップさせて利用してみましょう。

例えばこんな感じです。

//受信ストリームの取得(ObjectInputStream、BufferedInputStreamでラップ)
ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(sock.getInputStream()));

うーむ。通信を使用したゲームも1本くらい作ってみたいなぁ。

この際一時的にレンタルサーバ借りて外部公開して・・・て一緒にやる相手がいないから無理か。。。。

Java

Posted by nompor