이 튜토리얼은 Android에서 소켓(Socket)을 사용하여 서버와 클라이언트 간에 데이터를 주고받는 방법을 설명합니다. 별도의 서버 프로젝트와 클라이언트 프로젝트 두 개가 필요합니다.
서버(Server) 프로젝트
1단계: 새 프로젝트 생성
Android Studio에서 File → New Project를 선택하고 필요한 세부 정보를 입력하여 새 프로젝트를 생성합니다.
2단계: 레이아웃 XML 구성 (res/layout/activity_main.xml)
서버의 IP 주소, 포트, 연결 상태, 메시지 기록, 입력 필드, 전송 버튼을 포함하는 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvIP"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/tvPort"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tvIP"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/tvConnectionStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tvPort"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/tvMessages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/etMessage"
android:layout_below="@+id/tvConnectionStatus"
android:inputType="textMultiLine"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<EditText
android:id="@+id/etMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/btnSend"
android:hint="Enter Message"
android:inputType="text" />
<Button
android:id="@+id/btnSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="SEND" />
</RelativeLayout>
3단계: 메인 액티비티 구현 (src/MainActivity.java)
서버 소켓을 열고 클라이언트 연결을 대기하며, 수신된 메시지를 화면에 표시하고 전송하는 로직을 포함합니다. 참고: 최신 Android 개발에서는 android.support 대신 androidx 라이브러리 사용을 권장합니다.
package com.server.myapplication.server;
import android.annotation.SuppressLint;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {
ServerSocket serverSocket;
Thread Thread1 = null;
TextView tvIP, tvPort;
TextView tvMessages;
EditText etMessage;
Button btnSend;
public static String SERVER_IP = "";
public static final int SERVER_PORT = 8080;
String message;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvIP = findViewById(R.id.tvIP);
tvPort = findViewById(R.id.tvPort);
tvMessages = findViewById(R.id.tvMessages);
etMessage = findViewById(R.id.etMessage);
btnSend = findViewById(R.id.btnSend);
try {
SERVER_IP = getLocalIpAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
Thread1 = new Thread(new Thread1());
Thread1.start();
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
message = etMessage.getText().toString().trim();
if (!message.isEmpty()) {
new Thread(new Thread3(message)).start();
}
}
});
}
private String getLocalIpAddress() throws UnknownHostException {
WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
assert wifiManager != null;
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int ipInt = wifiInfo.getIpAddress();
return InetAddress.getByAddress(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ipInt).array()).getHostAddress();
}
private PrintWriter output;
private BufferedReader input;
class Thread1 implements Runnable {
@Override
public void run() {
Socket socket;
try {
serverSocket = new ServerSocket(SERVER_PORT);
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.setText("Not connected");
tvIP.setText("IP: " + SERVER_IP);
tvPort.setText("Port: " + String.valueOf(SERVER_PORT));
}
});
try {
socket = serverSocket.accept();
output = new PrintWriter(socket.getOutputStream());
input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.setText("Connected\n");
}
});
new Thread(new Thread2()).start();
} catch (IOException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private class Thread2 implements Runnable {
@Override
public void run() {
while (true) {
try {
final String message = input.readLine();
if (message != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.append("client: " + message + "\n");
}
});
} else {
Thread1 = new Thread(new Thread1());
Thread1.start();
return;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Thread3 implements Runnable {
private String message;
Thread3(String message) {
this.message = message;
}
@Override
public void run() {
output.write(message);
output.flush();
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.append("server: " + message + "\n");
etMessage.setText("");
}
});
}
}
}
4단계: 매니페스트 권한 설정 (AndroidManifest.xml)
와이파이 상태 접근, 네트워크 상태 접근, 인터넷 권한이 필요합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:label="Server">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
클라이언트(Client) 프로젝트
1단계: 새 프로젝트 생성
별도의 새 프로젝트를 생성합니다 (File → New Project).
2단계: 레이아웃 XML 구성 (res/layout/activity_main.xml)
서버 IP와 포트를 입력받는 필드, 연결 버튼, 메시지 표시 영역, 입력 필드, 전송 버튼으로 구성됩니다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
tools:context=".MainActivity">
<EditText
android:id="@+id/etIP"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="IP-Address"
android:inputType="text" />
<EditText
android:id="@+id/etPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/etIP"
android:hint="Port No"
android:inputType="number" />
<Button
android:id="@+id/btnConnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/etPort"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:text="Connect To Server" />
<TextView
android:id="@+id/tvMessages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/etMessage"
android:layout_below="@+id/btnConnect"
android:inputType="textMultiLine"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium" />
<EditText
android:id="@+id/etMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/btnSend"
android:hint="Enter Message"
android:inputType="text" />
<Button
android:id="@+id/btnSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="SEND" />
</RelativeLayout>
3단계: 메인 액티비티 구현 (src/MainActivity.java)
서버 IP와 포트를 입력받아 연결을 시도하고, 메시지 송수신을 처리합니다.
package com.client.myapplication.client;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {
Thread Thread1 = null;
EditText etIP, etPort;
TextView tvMessages;
EditText etMessage;
Button btnSend;
String SERVER_IP;
int SERVER_PORT;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etIP = findViewById(R.id.etIP);
etPort = findViewById(R.id.etPort);
tvMessages = findViewById(R.id.tvMessages);
etMessage = findViewById(R.id.etMessage);
btnSend = findViewById(R.id.btnSend);
Button btnConnect = findViewById(R.id.btnConnect);
btnConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tvMessages.setText("");
SERVER_IP = etIP.getText().toString().trim();
SERVER_PORT = Integer.parseInt(etPort.getText().toString().trim());
Thread1 = new Thread(new Thread1());
Thread1.start();
}
});
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = etMessage.getText().toString().trim();
if (!message.isEmpty()) {
new Thread(new Thread3(message)).start();
}
}
});
}
private PrintWriter output;
private BufferedReader input;
class Thread1 implements Runnable {
public void run() {
Socket socket;
try {
socket = new Socket(SERVER_IP, SERVER_PORT);
output = new PrintWriter(socket.getOutputStream());
input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.setText("Connected\n");
}
});
new Thread(new Thread2()).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Thread2 implements Runnable {
@Override
public void run() {
while (true) {
try {
final String message = input.readLine();
if (message != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.append("server: " + message + "\n");
}
});
} else {
Thread1 = new Thread(new Thread1());
Thread1.start();
return;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Thread3 implements Runnable {
private String message;
Thread3(String message) {
this.message = message;
}
@Override
public void run() {
output.write(message);
output.flush();
runOnUiThread(new Runnable() {
@Override
public void run() {
tvMessages.append("client: " + message + "\n");
etMessage.setText("");
}
});
}
}
}
4단계: 매니페스트 권한 설정 (AndroidManifest.xml)
인터넷 권한과 네트워크 상태 접근 권한만 필요합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.client.myapplication.client">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="com.client.myapplication.client.MainActivity"
android:label="Client">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
앱 실행 및 테스트
두 프로젝트 모두 실제 Android 기기에 설치하여 테스트해야 합니다. 같은 와이파이 네트워크에 연결된 두 대의 기기(또는 에뮬레이터와 기기)를 준비하세요.
- Android Studio에서 서버 프로젝트를 열고 Run 아이콘(
)을 클릭하여 서버 앱을 기기에 설치/실행합니다. - 서버 앱이 실행되면 화면에 표시되는 IP 주소와 포트(8080)를 확인합니다.
- 클라이언트 프로젝트를 열고 실행한 뒤, 클라이언트 앱의 IP 입력란에 서버의 IP 주소를, 포트 입력란에
8080을 입력하고 Connect To Server 버튼을 누릅니다. - 양쪽 모두 "Connected" 메시지가 뜨면 메시지를 주고받을 수 있습니다.
실행 화면 예시
서버 앱 초기 화면:

클라이언트 앱 초기 화면:

연결 후 서버 화면:

참고 사항:
- 두 기기가 동일한 로컬 네트워크(와이파이)에 연결되어 있어야 합니다.
- 방화벽이나 공유기 설정에 따라 포트(8080)가 차단될 수 있습니다.
- 실제 상용 앱 개발 시에는 스레드 관리, 예외 처리, 연결 끊김 시 재연결 로직, 보안(SSL/TLS) 등을 더 견고하게 구현해야 합니다.
- 최신 Android 버전(API 28+)에서는 네트워크 보안 구성으로 인해 평문 HTTP/소켓 통신이 차단될 수 있으니
android:usesCleartextTraffic="true"설정이 필요할 수 있습니다.