본문 바로가기
Android/Concepts

Socket을 이용한 서버와 클라이언트 통신

by JuHy_ 2020. 4. 23.

네트워킹이란?

요새는 인터넷을 사용하지 않는 앱을 찾기 힘들 정도로 거의 모든 앱에서 인터넷 통신이 들어간다.

이렇게 인터넷 상의 한 지점(서버)과 앱(클라이언트) 사이의 통신을 네트워킹이라고 한다.

 

클라이언트와 서버 간의 연결 방식으로는 여러가지가 있는데 몇가지를 알아보자.

 

먼저 클라이언트가 요청하고 서버는 응답하는 가장 기본적인 형태인 2-tier 모델이 있다.

 

다음으로 데이터의 양이 많아짐에 따라 데이터를 저장하는 데이터 서버를 따로 두고,

클라이언트에 요청을 받아 응답하며, 데이터 서버에 요청을 보내 응답을 받는 응용 서버를 두기도 한다.

이를 3-tier 모델이라고 한다.

 

그렇다면 이렇게 클라이언트와 서버 간 통신에 사용되는 네트워킹 기법을 알아보자.

 

Socket 통신이란?

Socket 통신이란 데이터를 요청하거나 응답을 보낼 때 Socket에 데이터를 담아 보내는 방식을 말한다.

그리고 데이터 통신을 하기 전 상호 연결을 확실히 하기 위한 규약으로 TCP/IP 규약을 사용하는데,

이를 따서 Socket 통신을 TCP/IP 통신이라고도 지칭하기도 한다.

 

사용법

안드로이드에서는 네트워킹을 사용할 때 반드시 thread를 사용해야 한다.

따라서 별도의 thread를 생성한 뒤 서버와 통신을 하고,

통신한 내용을 바탕으로 handler를 통해 UI를 업데이트 하는 방식으로 사용한다.

 

실제로 구현해보기 전에 Thread와 Handler에 대한 내용은 아래 글을 통해 미리 공부하자.

https://ju-hy.tistory.com/62

 

Thread와 Handler 사용법

Thread와 Handler란? 앱을 구현할 때 하나의 기능이 실행되는 중 다른 기능이 동시에 실행되어야 할 때가 있다. 예를 들어 우리가 게임을 하는 동안에 동시에 채팅도 할 수 있게 구현하고 싶다면 Thread를 사용하..

ju-hy.tistory.com

 

보통 서버는 안드로이드 앱이 아닌 자바 등의 언어로 구축하지만 편의상 이번에는 안드로이드 앱으로 구현하자.

 

클라이언트에서 데이터를 전송하면 이 데이터를 서버에서 받고,

받은 데이터를 다시 서버에서 전송하면 클라이언트에서 받도록 구현해보자.

 

이제 서버와 클라이언트 2개의 프로젝트를 생성하자.

그 다음에 두 프로젝트 모두에 인터넷 통신을 위한 INTERNET permission을 manifest에 등록하자.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.juhy.client">

    <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">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

이제 서버와 클라이언트를 구현해보자.

 

Server

package com.juhy.server;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ServerThread thread = new ServerThread();
                thread.start();
            }
        });
    }

    class ServerThread extends Thread {
        @Override
        public void run() {
            int port = 5001;

            try {
                ServerSocket server = new ServerSocket(port);
                Log.d("ServerThread", "Server Started.");

                while(true){
                    Socket socket = server.accept();

                    ObjectInputStream instream = new ObjectInputStream(socket.getInputStream());
                    Object input = instream.readObject();
                    Log.d("ServerThread", "input: " + input);

                    ObjectOutputStream outstream = new ObjectOutputStream(socket.getOutputStream());
                    outstream.writeObject(input + " from server.");
                    outstream.flush();
                    Log.d("ServerThread", "output sent.");

                    socket.close();
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

Thread의 run() 함수 내부에 클라이언트로부터 데이터를 받아 다시 돌려주는 코드를 작성해보자.

 

먼저 ServerSocket에 port 번호를 넣어 객체를 생성해준다.

(port 번호는 이미 사용중인 번호를 사용하면 안되며, 클라이언트와의 통신을 위해선 같은 번호를 사용해야 한다)

 

그 다음 반복문을 시작해 accept() 함수를 통해 클라이언트로부터 데이터가 오길 기다린다.

데이터가 들어왔다면 accept()가 끝나고 자동으로 다음 코드가 실행된다.

 

socket에서 InputStream을 받아와 객체에 넣어준 뒤 readObject()를 통해 데이터를 읽어와 로그를 찍고,

 

OutputStream에 다시 writeObject()를 통해 데이터를 써준 뒤 flush()를 통해 버퍼에 담긴 정보를 보낸다.

 

그리고 마지막으로 소켓을 닫아주면 된다.

 

Client

package com.juhy.client;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ClientThread thread = new ClientThread();
                thread.start();
            }
        });
    }

    class ClientThread extends Thread {
        @Override
        public void run() {
            String host = "localhost";
            int port = 5001;

            try {
                Socket socket = new Socket(host, port);

                ObjectOutputStream outstream = new ObjectOutputStream(socket.getOutputStream());
                outstream.writeObject("Hello!");
                outstream.flush();
                Log.d("ClientStream", "Sent to server.");

                ObjectInputStream instream = new ObjectInputStream(socket.getInputStream());
                Object input = instream.readObject();
                Log.d("ClientThread", "Received data: " + input);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

클라이언트도 대부분의 코드는 비슷하지만 일부 다른 부분이 있다.

 

서버에서는 socket을 accept() 함수를 통해 받아왔지만,

클라이언트에서는 서버 주소와 port 번호를 통해 Socket 객체를 생성한다.

 

그리고 서버는 데이터를 받은 뒤 보냈지만, 클라이언트는 데이터를 보낸 뒤 받도록 구현한다.

OutputStream에 전송할 데이터를 담아 보낸 뒤, InputStream을 통해 데이터를 읽어 로그를 찍어보았다.

 

 

이제 앱을 실행해보자.

 

먼저 서버 앱의 버튼을 눌러 서버를 실행시켜보자.

 

그 다음 클라이언트 앱의 버튼을 눌러 클라이언트를 실행해보자.

클라이언트의 버튼을 누를 때마다 서버로 데이터를 보내고,

서버에서 데이터를 받아 다시 클라이언트로 보내면,

클라이언트에서 데이터를 받아 보여주는 과정이 반복된다.

 

 

Handler 추가

handler.post(new Runnable() {
    @Override
    public void run() {
        textView.setText(input.toString());
    }
});

Handler 객체와 TextView 객체를 전역변수로 생성하고 위 코드를 클라이언트 마지막 부분에 넣어주자.

 

 

서버에서 보낸 데이터를 정상적으로 수신하여 TextView에 띄워주는 것을 볼 수 있다.

 

 

※ Server 부분 Service로 구현하기

Server가 동작하는 부분을 Activity에서 구현할 경우 앱이 background로 이동 시 종료될 가능성이 있다.

따라서 종료되지 않는 Service로 구현하는 것이 좋다.

 

Service에 대한 내용은 아래 글을 참고.

https://ju-hy.tistory.com/49

 

Service의 기본적인 사용법

Service란? 그동안 다루었던 Activity와 다르게 Service는 화면에 보이지 않는다. Service는 Activity와 같이 안드로이드의 컴포넌트이기 때문에 onCreate(), onDestroy() 메소드를 갖고 있다. 그리고 사용할 때..

ju-hy.tistory.com

 

ServerService

package com.juhy.server;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerService extends Service {
    public ServerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();

        ServerThread thread = new ServerThread();
        thread.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    class ServerThread extends Thread {
        @Override
        public void run() {
            int port = 5001;

            try {
                ServerSocket server = new ServerSocket(port);
                Log.d("ServerThread", "Server Started.");

                while(true){
                    Socket socket = server.accept();

                    ObjectInputStream instream = new ObjectInputStream(socket.getInputStream());
                    Object input = instream.readObject();
                    Log.d("ServerThread", "input: " + input);

                    ObjectOutputStream outstream = new ObjectOutputStream(socket.getOutputStream());
                    outstream.writeObject(input + " from server.");
                    outstream.flush();
                    Log.d("ServerThread", "output sent.");

                    socket.close();
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Service 클래스로 thread 정의 부분을 옮겨준 뒤 onCreate에서 thread를 시작하도록 한다.

 

MainActivity

package com.juhy.server;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, ServerService.class);
                startService(intent);
            }
        });
    }

}

MainActivity에서는 버튼 클릭 시 ServerService가 시작하도록 구현하면 된다.

 

 

Reference

[부스트코스]안드로이드 프로그래밍

https://www.edwith.org/boostcourse-android

'Android > Concepts' 카테고리의 다른 글

Volley 라이브러리 사용법  (0) 2020.04.26
HttpURLConnection을 이용한 HTTP 통신  (0) 2020.04.25
AsyncTask 사용법  (0) 2020.04.23
Thread와 Handler 사용법  (1) 2020.04.23
Navigation Drawer Activity 살펴보기  (3) 2020.04.17