Computer >> 컴퓨터 >  >> 프로그래밍 >> Android

Android 소켓 프로그래밍: 서버-클라이언트 간 데이터 송수신 구현하기

이 튜토리얼은 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 기기에 설치하여 테스트해야 합니다. 같은 와이파이 네트워크에 연결된 두 대의 기기(또는 에뮬레이터와 기기)를 준비하세요.

  1. Android Studio에서 서버 프로젝트를 열고 Run 아이콘(Android 소켓 프로그래밍: 서버-클라이언트 간 데이터 송수신 구현하기)을 클릭하여 서버 앱을 기기에 설치/실행합니다.
  2. 서버 앱이 실행되면 화면에 표시되는 IP 주소와 포트(8080)를 확인합니다.
  3. 클라이언트 프로젝트를 열고 실행한 뒤, 클라이언트 앱의 IP 입력란에 서버의 IP 주소를, 포트 입력란에 8080을 입력하고 Connect To Server 버튼을 누릅니다.
  4. 양쪽 모두 "Connected" 메시지가 뜨면 메시지를 주고받을 수 있습니다.

실행 화면 예시

서버 앱 초기 화면:

Android 소켓 프로그래밍: 서버-클라이언트 간 데이터 송수신 구현하기

클라이언트 앱 초기 화면:

Android 소켓 프로그래밍: 서버-클라이언트 간 데이터 송수신 구현하기

연결 후 서버 화면:

Android 소켓 프로그래밍: 서버-클라이언트 간 데이터 송수신 구현하기


참고 사항:

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