[Network] JAVA 서버와 안드로이드 클라이언트 소켓 프로그래밍



소켓 프로그래밍의 흐름

소켓 프로그래밍을 하려면 소켓이 어떻게 통신을 하는지 흐름을 알아야 한다. 

Client 와 Server에서 각각 소켓을 생성한다. 서버에서는 소켓을 생성한 후 Bind 를 통해 소켓에 주소를 할당해주고 Listen으로 연결 요청 가능한 상태로 만들어준다.
클라이언트에서 서버에 Connect 연결 요청을 보내면 서버에서는 해당 연결 요청에 문제가 없다고 판단하면 Accept 해서 클라이언트와 서버가 연결이 된다. 연결 후에 원하는 작업 (데이터 송수신) 을 진행한 후, 작업이 끝나면 Close로 소켓을 닫아 통신을 끝낸다. 아래의 링크에서 좀 더 자세한 내용을 확인할 수 있다.









서버와 클라이언트 구현

이 포스팅의 목적은 안드로이드 클라이언트와 서버의 통신을 공부하기 위함이다. 안드로이드이므로 당연히 클라이언트는 자바 또는 코틀린이 될 것이고, (물론 네이티브로 해서 C++를 이용할 수 있지만, 네이티브 프로그래밍은 해본적이 없고 자바로도 충분히 구현할 수 있다.) 서버는 어떤 언어를 사용하든 상관 없다.

필자는 게임할 때 쓰는 윈도우 데스크탑이 하나 있는데, 여기에 가상머신을 이용해 리눅스에서 서버를 구축해보았다. 윈도우와 유닉스 기반 운영체제는 소켓 프로그래밍 방식이 다른데, 윈도우에서는 <winsock2.h> 라는 헤더를 이용하고, 리눅스에서는 <sys/socket.h> 라는 헤더를 이용한다. 어차피 결과는 똑같이 나오니까 본인이 편한 운영체제를 선택하자.

원래는 데이터 송수신에 대한 코드도 넣어야하지만, 단순히 통신이 되는지 안되는지 파악하기 위한 테스트 서버 구축이 목적이었으므로 데이터 통신에 관한 코드는 생략했다. 


리눅스 서버 구현



#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

int main(){

    int                     server_socket;
    int                     client_socket;

    struct  sockaddr_in     server_addr;
    struct  sockaddr_in     client_addr;

    socklen_t               client_addr_size;


    // 소켓 생성 ////////////////////////////////////////////////////////////////////////////////////////////////////
    server_socket = socket(PF_INET, SOCK_STREAM, 0);             // PF_INET : IPv4 프로토콜     SOCK_STREAM : TCP 통신
    if(server_socket == -1) 
        cout << "socket error" << '\n';
    else
        cout << "socket succeed" << '\n';
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

    


    // 소켓 바인드 //////////////////////////////////////////////////////////////////////////////////////////////////
    memset(&server_addr, 0, 0, sizeof(server_addr));             // 소켓 구조체  
    server_addr.sin_family = AF_INET;                            // 소켓 프로토콜 체계 : IPv4
    // 포트와 IP를 리틀 엔디안 -> 빅 엔디안으로 변환
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);             // INADDR_ANY : 적당한 IP를 자동으로 넣어줌 
                                                                 // --> 적당한 호스트 바이트 순서를 네트워크 바이트 순서로 변환
    server_addr.sin_port = htons(9000);                          // 포트는 9000만 사용

    if(bind(server_socket, (struct sockaddr*) &server_addr, sizeof(server_addr)) == -1)
        cout << "bind error" << '\n';
    else cout << "bind succeed" << '\n';
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////




    // 연결 요청 상태 체크 ///////////////////////////////////////////////////////////////////////////////////////////
    if(listen(server_socket, 5) == -1)                           // 대기 가능한 최대 연결 개수를 5개로 잡음
        cout << "listen error" << '\n';
    else cout << "listen succeed" << '\n';
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////




    // 연결 요청 수락 //////////////////////////////////////////////////////////////////////////////////////////////
    client_addr_size = sizeof(client_addr);
    client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &client_addr_size);
    if(client_socket == -1)
        cout << "accept error" << '\n';
    else cout << "connect succeed" << '\n';
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////




    // 소켓 종료 /////////////////////////////////////////////////////////////////////////////////////////////////
    close(client_socket);
    close(server_socket);
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////


    return 0;
}





안드로이드 클라이언트도 단순히 연결 테스트용으로 역시 무척 간단하게 구현했다. 사용 언어는 자바로, 버튼을 눌렀을 때 화면 전환이 되면서 연결이 성공하면 TextView에 Succeed라는 문구를, 실패하면 Fail이라는 문구가 출력되게 했다.

안드로이드 UI 및 기능에 대한 설명은 생략하기로 하고, 안드로이드 내에서 소켓 통신을 할 때 주의해야 할 것들을 적어 보았다.



1. 인터넷 접근 권한 설정

소켓 통신을 하려면 인터넷 연결은 필수다. AndroidManifest.xml에 아래의 접근 권한을 추가하자.
<uses-permission android:name="android.permission.INTERNET" />


2. android.os.NetworkOnMainThreadException

안드로이드 내에서의 소켓 통신은 스레드가 필수적으로 사용된다. 스레드에서 통신을 하지 않고 OnCreate 등에서 통신 코드를 때려박으면 필연적으로 android.os.NetworkOnMainThreadException 가 발생한다. 따라서 아래에서 설명하는 것처럼 스레드 내에서 클라이언트 코드를 불러와야 한다.


3. java.net.ConnectException

연결 오류에 관해선 connection refused, connect failed: ETIMEDOUT (Connection timed out) 등 여러가지 유형이 있는데, ConnectException이 대부분 잘못된 IP에 접근했거나, 방화벽 문제, 포트 포워딩에 대한 문제였다. IP에 관한 문제라면 아래 글을 참고해보자. 해당 글로 해결이 안 될 수도 있지만, 조금이나마 도움이 되길 바란다.



아래는 안드로이드 스튜디오에서 소켓 통신 코드이다. 원래는 MainActivity와 연결된 activity_main.xml 에 버튼을 하나 주고, 해당 버튼을 누르면 ClientTestAcivity 로 넘어가서 연결 요청을 보내는데 화면 전환에 대한 기능은 생략하고 통신에 대한 내용을 집중적으로 보도록 하자.



안드로이드 클라이언트 구현


ClientTestActivity.java

여기서 중요한 점은 Thread를 이용해 소켓 통신을 처리하는 클래스인 Client 클래스를 불러오고 있다는 것이다. 스레드 내에서 해당 부분을 처리하지 않으면 위에서 얘기했던 오류가 발생하게 된다.

package com.example.brawlkat;

import android.os.Bundle;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

public class ClientTestActivity extends AppCompatActivity {

    TextView textView;

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

        textView = (TextView)findViewById(R.id.result);

        GetUserDataThread getUserDataThread = new GetUserDataThread();
        getUserDataThread.start();
    }

    private class GetUserDataThread extends Thread {

        public void run(){
            Client client = new Client();
            textView.setText(client.clientTest());
        }
    }
}





Client.java

C++ 와 달리 자바는 소켓 통신 구현이 좀 더 간단하다. C++에서는 소켓을 생성하고, 서버 IP와 PORT에 대한 정보를 구조체에 묶어서 connect를 해줘야하는데, 자바는 사실상 
ia = InetAddress.getByName("IP");
socket = new Socket(ia, 9000);
이 두줄로 소켓 생성 및 서버 구조체 생성 및 연결 요청을 모두 처리해준다. 심지어
socket = new Socket("IP", port); 
로 1줄로도 줄일 수 있다. 그 아래는 C++ 로 클라이언트를 구현한 코드다. 비교하면서 공부해보자.

package com.example.brawlkat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;

public class Client {

    String res;
    Socket socket = null;            //Server와 통신하기 위한 Socket
    InetAddress ia = null;

    public String clientTest(){

        try {
            ia = InetAddress.getByName("여기에 본인의 IP 주소를 적으면 된다.");    //서버로 접속
            socket = new Socket(ia,9000);

            System.out.println(socket.toString());

            res = "succeed";
        }catch(IOException e) {
            e.printStackTrace();
            res = "fail";
        }


        return res;
    }
}





Client.cpp

#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

int main(){

    int                     client_socket;
    struct sockaddr_in      server_addr;
    
    // 소켓 생성 ///////////////////////////////////////////////////////////////////////////
    client_socket = socket(PF_INET, SOCK_STREAM, 0);
    if(client_socket == -1) cout << "socket error" << '\n';
    else cout << "connect succeed" << '\n';
    //////////////////////////////////////////////////////////////////////////////////////



    // 서버 IP & PORT 저장 /////////////////////////////////////////////////////////////////
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("접속할 IP");
    server_addr.sin_port = htons(9000);
    //////////////////////////////////////////////////////////////////////////////////////


    // 연결 요청 ///////////////////////////////////////////////////////////////////////////
    if(connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
        cout << "connect error" << '\n';
    else cout << "connect succeed" << '\n';
    //////////////////////////////////////////////////////////////////////////////////////


    // 소켓 종료 ///////////////////////////////////////////////////////////////////////////
    close(client_socket);
    //////////////////////////////////////////////////////////////////////////////////////
    return 0;
}





댓글