본문 바로가기
Android/Android

Android(11)_Python Server & Android Client Thread

by 미티치 2016. 8. 15.


우선 통신, 소켓 등에 대해 자세히 설명하자면 너무 길어지기 때문에 예제 위주로 기초적인 TCP 통신을 설명하고자 합니다. 서버-클라이언트 TCP 소켓 통신을 할 때 이 네트워크에서 디바이스와 디바이스가 어떻게 정보를 주고받는지에 대해 감이 안올 수 있기 때문에 다음 그림으로 설명할게요!






우선 Client가 Server의 위치를 어떻게 알고 가느냐? 

Server의 IP주소와 이 Server와 Client를 연결시킬 포트번호를 이용해서 찾아갑니다. 예를들어 IP주소는 집주소와 같습니다. 세상에서 하나밖에 없는 주소로, Client가 전 세계에 연결되어있는 네트워크에서 내가 원하는 서버를 찾기 위해서는 이 IP 주소를 이용해서 찾을 수 있습니다. 근데 여기서 포트번호는 왜 필요할까? 우리가 그럼 Naver의 서버 IP주소만 알면 서버에 바로 접근할 수 있느냐? 아닙니다. 서버는 허용된 클라이언트가 서버에 접근할 수 있도록 수 많은 통로를 갖고있습니다. 이 통로들이 바로 포트(PORT)입니다. 서버가 가진 수많은 포트 중 '나는 7777, 7778 포트를 열어놓을거야' 라고 지정해서 열어놓은 포트로만 클라이언트가 접근할 수 있습니다. 따라서 클라이언트에서 서버로 접근하기 위해서는 IP주소 뿐만아니라 서버와 약속되어 서버가 열어놓은 포트번호를 알아야만 서버와 연결을 하여 통신을 할 수 있습니다.


그렇다면 내가 만든 서버에서 포트 번호를 랜덤으로 아무거나 지정해서 사용할 수 있느냐? 라고 묻는다면 답은 'NO' 입니다. 포트 번호는 크게 세 종류로 구분이 되는데 다음과 같습니다.




0 ~ 1023번 : 잘 알려진 포트 (Well-Known Port)

  - 대표적으로 텔넷(23), DNS(53), HTTP(80), NNTP(119), TLS/SSL 방식의 HTTP(443)

1024 ~ 49151번 : 등록된 포트 (Registered Port)

49152 ~ 65535번 : 동적 포트 (Dynamic Port)




통신에서 가장 많이 쓰이는 TPC/IP 프로토콜을 사용하는 응용프로그램들은 통신을 할 경우 IANA에서 지정한 포트를 사용하는데, 이 포트번호들이 잘 알려진 포트로, 잘 알려진 포트번호는 열기 위해서는 루트권한이 있어야합니다.

 

1024~49151는 중복방지를 위해서 IANA에 등록은 되어있으나 IANA의 통제를 받지 않기 때문에 우리가 임의로 서버를 구축해서 TCP 통신을 하고자 할때 이 범위 안에서 포트번호를 사용하면 됩니다. 동적포트는 IANA에 등록도 되어있지 않고 통제도 받지 않는 포트로, 어떠한 프로세스에 의해서도 사용이 가능하며 임시 포트번호라고도 합니다.


그렇다면 이제 TCP/IP 프로토콜에서 Client가 Server를 IP주소를 이용해서 어떻게 찾는지 알게되었습니다. 그럼 이제 TCP 통신을 해야하는데 여기서 중요한건 바로 '소켓'입니다. 소켓이란 단말간에 통신을 하고자 할 때 정보를 주고받기 위해 연결된 통신 통로의 각 끝부분인 통신 접속점, 즉 창구라고 생각하면 될 것 같습니다. 따라서 통신을 하려면 서버든 클라이언트든 소켓을 만들어주고 그 소켓을 이용해서 통신을 하게됩니다. 소켓을 이용해서 서버와 클라이언트 사이의 흐름(Stream)을 받아 정보를 주고받으면 통신이 되는 겁니다.


서버 측에서 ServerSocket을 만들어 accept() 메소드를 이용해서 클라이언트와의 연결을 기다립니다. 이 accept 메소드의 반환형이 클라이언트 소켓인데, 정확하게는 이 반환형이 Socket입니다. 우리가 Client에서 Socket clientSocket = new Socket(ip, port); 해서 생성한 소켓으로 Server와 통신을하죠? 사실 말이 서버-클라이언트라서 서버에서는 대단한 ServerSocket을 생성하고 뭐 이런 것처럼 보이지만 사실 서버에서도 서버 소켓을 이용해서 포트를 열어놓고 클라이언트와의 연결이 되는 것을 기다리는데까지만 역할을 하고, 이 ServerSocket.accept() 메소드를 통해 클라이언트와 연결이 되느 순간 서버에서 클라이언트 소켓을 생성해서 client 측의 클라이언트 소켓과 통신을 합니다. 다시한번 강조하자면 사실 이 클라이언트 소켓도 정확하게 말하면 그냥 소켓이죠. 



기본적으로 응용프로그램은 우리가 짜는 소스 코드대로 줄줄이 읽어나가는 방식대로 명령을 실행합니다. 근데 이렇게 TCP/IP 프로토콜을 이용해서 통신을 하는 응용 프로그램이 있을 때, 이 응용프로그램이 서버와 통신을 하면서 동시에 통신과는 무관하게 사용자의 입력을 받아 처리하는 작업이 있다고 가정합시다. 이때 소스를 순서대로만 읽어나간다면 서버와 연결이 될때까지 기다렸다가 연결이 되면 다음 소스를 읽어 서버에 요청을 보내고 서버와의 작업이 끝나면 사용자의 입력을 받아 처리하게 될 것입니다. 다시 말하면 두 가지 작업이 동시에 따로따로 이루어져야 하는데 그러지 못하고 한 작업이 끝나면 또 다른 작업이 실행되고 그 작업이 끝나면 나머지 작업이 실행되는 불상사가 일어나게 될 것입니다. 그래서 통신 응용프로그램을 만들게 될 땐 서버와의 통신 작업과 별개로 동시에 사용자가 이 응용프로그램에서 다른 작업을 할 수 있도록 Thread를 이용하는 것입니다.


대부분의 서버는 하나의 서버가 하나의 클라이언트에서만 접속하는게 아니라 수 많은 클라이언트가 서버에 접속하도록 만들어져 있습니다. 그렇다면 서버 입장에서 봤을 때, 다중 클라이언트가 접속한 상황에서 여러 클라이언트들이 동시에 여러 작업을 하는 경우 작업을 하나씩 하나씩 순서대로 처리하는 것이 아니라 하나의 클라이언트가 접속하면 Thread를 추가로 하나 생성해서 각 클라이언트들이 수행하는 작업을 각각의 Thread 안에서 처리하게 해야하는 것입니다.



여기까지는 기본적인 TCP 통신에 대해 개념적인 접근이었습니다. 아래의 예제에서는 이러한 통신 과정을 Server는 파이썬으로, Client는 Android로 구현한 것입니다. 


사실 Client를 Java로 구현했다면 그냥 소켓을 생성하고 Thread를 이용해서 입출력을 수행하면서 바로 값을 보거나 화면을 변경할 수 있지만 Android에서는 UI를 메인스레드에서만 컨트롤 할 수 있기 때문에 Thread를 생성, 사용하면서 UI를 컨트롤 하기 위해서는 Handler를 사용해야합니다.

( Handler 추가 자료 http://arabiannight.tistory.com/entry/331 )






< Python Sever >


[ main.py ] 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import tcpServer
import executer
import Queue
import time
 
# make public queue
commandQueue = Queue.Queue()
 
# init module
andRaspTCP = tcpServer.TCPServer(commandQueue, ""35357)
andRaspTCP.start()
 
 
# set module to executer
commandExecuter = executer.Executer(andRaspTCP)
 
 
while True:
    try:
        command = commandQueue.get()
        commandExecuter.startCommand(command)
    except:
        pass
 
 
#while True:
#    time.sleep(3)
#    andRaspTCP.sendAll("321\n")
    
 
cs

- 서버에서 Arduino와의 확장을 고려해서 짠 소스라서 executer로 명령을 보내서 executer에서 처리하지만, 사실 이렇게 복잡하게 할 필요는 없다.



[ executer.py ]

1
2
3
4
5
6
7
8
class Executer:
    def __init__(self, tcpServer):
        self.andRaspTCP = tcpServer
 
    def startCommand(self, command):
 
        if command == "123\n":
            self.andRaspTCP.sendAll("321\n")
cs



[ tcpServer.py ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import socket, threading
import tcpServerThread
 
class TCPServer(threading.Thread):
    def __init__(self, commandQueue, HOST, PORT):
        threading.Thread.__init__(self)
 
        self.commandQueue = commandQueue
        self.HOST = HOST
        self.PORT = PORT
        
        self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.serverSocket.bind((self.HOST, self.PORT))
        self.serverSocket.listen(1)
 
        self.connections = []
        self.tcpServerThreads = []
 
    def run(self):
        try:
            while True:
                print 'tcp server :: server wait...'
                connection, clientAddress = self.serverSocket.accept()
                self.connections.append(connection)
                print "tcp server :: connect :", clientAddress
    
                subThread = tcpServerThread.TCPServerThread(self.commandQueue, self.tcpServerThreads, self.connections, connection, clientAddress)
                subThread.start()
                self.tcpServerThreads.append(subThread)
        except:
            print "tcp server :: serverThread error"
 
    def sendAll(self, message):
        try:
            self.tcpServerThreads[0].send(message)
        except:
            pass
 
cs



[ tcpServerThread.py ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import socket, threading
 
class TCPServerThread(threading.Thread):
    def __init__(self, commandQueue, tcpServerThreads, connections, connection, clientAddress):
        threading.Thread.__init__(self)
 
        self.commandQueue = commandQueue
        self.tcpServerThreads = tcpServerThreads
        self.connections = connections
        self.connection = connection
        self.clientAddress = clientAddress
 
    def run(self):
        try:
            while True:
                data = self.connection.recv(1024).decode()
 
                # when break connection
                if not data:
                    print 'tcp server :: exit :',self.connection
                    break
 
 
                print 'tcp server :: client :', data
                self.commandQueue.put(data)
        except:
            self.connections.remove(self.connection)
            self.tcpServerThreads.remove(self)
            exit(0)
        self.connections.remove(self.connection)
        self.tcpServerThreads.remove(self)
 
    def send(self, message):
        print 'tcp server :: ',message
        try:
            for i in range(len(self.connections)):
                self.connections[i].sendall(message.encode())
        except:
             pass
 
cs








< Android Client >



 [ MainActivity.java ]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.example.tcptest5;
 
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
 
public class MainActivity extends AppCompatActivity {
 
    Button btn;
    TextView tv;
 
    //  TCP연결 관련
    private Socket clientSocket;
    private BufferedReader socketIn;
    private PrintWriter socketOut;
    private int port = 7777;
    private final String ip = "223.195.222.138";
    private MyHandler myHandler;
    private MyThread myThread;
 
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // StrictMode는 개발자가 실수하는 것을 감지하고 해결할 수 있도록 돕는 일종의 개발 툴
        // - 메인 스레드에서 디스크 접근, 네트워크 접근 등 비효율적 작업을 하려는 것을 감지하여 
     //   프로그램이 부드럽게 작동하도록 돕고 빠른 응답을 갖도록 함, 즉  Android Not Responding 방지에 도움
     StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);
 
        try {
            clientSocket = new Socket(ip, port);
            socketIn = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            socketOut = new PrintWriter(clientSocket.getOutputStream(), true);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        myHandler = new MyHandler();
        myThread = new MyThread();
        myThread.start();
 
        btn = (Button) findViewById(R.id.btn);
        tv = (TextView) findViewById(R.id.tv);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                socketOut.println(123);
            }
        });
    }
 
    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    // InputStream의 값을 읽어와서 data에 저장
                    String data = socketIn.readLine();
                    // Message 객체를 생성, 핸들러에 정보를 보낼 땐 이 메세지 객체를 이용
                    Message msg = myHandler.obtainMessage();
                    msg.obj = data;
                    myHandler.sendMessage(msg);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
}
 
    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            tv.setText(msg.obj.toString());
        }
    }
}
 
cs


- 39~40 : 안드로이드 버전 3.0 이상부터는 인터넷 연결은 쓰레드나 핸들러에서 처리하도록 정책이 바뀌었다. 그래서 UI 쓰레스에서 인터넷 연결을 시도하면(HttpURLConnection과 같은 것으로) 실행 타임에서 에러가 발생한다. 그런데 위와 같은 코드를 인터넷 연결을 시도하는 코드 앞에 표시해 두면 안드로이드 버전 3.0 이상에서도 정상적으로 잘 실행이 된다.  

( StrictMode 정보 출처 http://uljavajoe.blogspot.kr/2012/11/30-ui-thread-runtime.html )



 [ AndroidManifest.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.tcptest5">
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        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>
 
    <uses-permission android:name="android.permission.INTERNET"/>
 
</manifest>
cs


 


[ activity_main.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.tcptest5.MainActivity">
 
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn"
        android:text="gogo"/>
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="-"
        android:id="@+id/tv"/>
</LinearLayout>
cs


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

Android(12)_동적 화면  (0) 2016.08.16
Android_잡다한 문법  (0) 2016.07.31
Android(9)_Slide Menu  (1) 2016.07.26
Android 흑과백 게임  (0) 2016.06.13
Android(7)_Push  (0) 2016.06.01