소켓 통신은 대학교때도 프로젝트로 진행했던 기억이 난다.
라즈베리파이로 온도센서, 습도센서를 이용해서 소켓 통신을 하여 경보기를 만들었는데
그때는 c언어로 소켓통신을 만들었기에 어렵게 어렵게 완성했던 기억이 있다.
지금은 안드로이드 개발에 큰 관심을 가지고 있기도 하고 정리해두고 싶어서 이렇게 글을 쓴다.
직접 혼자서 만들어보면 좋겠지만 당장 라즈베리파이나 아두이노도 없어서 인터넷글을 보고 사용법에 대해서 공부하고 있다. 공부하다보니 Android Things 라는것도 있던데 기회가 되면 이것도 알아보고 싶다.
열심히 공부하며 글을 쓰고 있었는데 정말 소개가 잘 된 블로그가 있었다.아래 내용은 해당 출처에 있는 글이다.
출처 :ddangeun.tistory.com/31
TCP/IP 소켓 통신
TCP/IP란 호스트들이 상호 통신하기 위한 표준화된 프로토콜입니다.
- 프로토콜은 시스템간 어떻게 데이터 교환을 할것인지 정한 통신 규약입니다. 컴퓨터와 네트워크 기기가 상호 통신하기 위해, 규칙을 결정할 필요가 있는데, 이런 규칙이 프로토콜입니다.
인터넷과 관련된 규칙, 즉 프로토콜을 모은것을 TCP/IP라 부릅니다.
수 많은 프로그램들이 인터넷으로 통신하기 위해 TCP/IP 통신을 사용하고 있습니다. 사실상 인터넷 프로토콜을 대표하는 용어이며, 이를 이용해 컴퓨터를 연결하는 처계를 이더넷(Ethernet)이라 부릅니다.
TCP/IP에서 IP는 우리가 흔히 사용하는 IP(Internet Protocol)입니다.
규칙에 따라 컴퓨터에 인터넷 주소를 부여하는 것이 IP이며, 그 주소가 IP 주소 입니다.
이렇게 IP 주소를 할당받으면 어디서든지 해당 IP 주소의 컴퓨터에 접근할 수 있습니다.
IP로 해당 컴퓨터에 접근을 해서, 어느 프로그램과 통신을 해야하는지 구분할 때 사용하는 것이 포트(Port)입니다.
포트란 통신을 하기 위한 출입구이며, 각 프로그램들은 자신만의 포트 번호를 부여함으로써 각각의 통신 구분을 할 수 있습니다.
0~65535까지의 포트번호 중, 0~1023까지는 시스템에서 사용하는 포트임으로 사용하면 안되고, 1024~65535 사이의 번호를 사용할 수 있습니다.
소켓 프로그래밍
소켓이란 떨어져 있는 두 호스트를 연결해주는 도구를 말하며, 인터페이스 역할을 합니다.
데이터를 주고 받을 수 있는 구조체이고, 소켓을 통해 데이터 통로가 만들어 집니다.
역할에 따라 서버 소켓, 클라이언트 소켓으로 구분합니다.
서버 소켓은 클라이언트 소켓의 연결 요청을 대기하고, 연결 요청이 오면 클라이언트 소켓을 생성하여 통신이 가능하게 합니다.
클라이언트 소켓은 바로 사용가능하며, 실제로 데이터 송수신이 일어나는 것은 클라이언트 소켓입니다.
서버 소켓인지, 클라이언트 소켓인지에 따라 사용되는 API가 다릅니다.
-
목표
그러므로, 우리는 서버에서 할 일과 클라이언트에서 할 일 을 구분하여 코딩하여야 합니다.
이 글은 TCP/IP통신이 가능한 특정 하드웨어 기기에서 데이터를 받아오는 것을 전제로 합니다.
센서가 달린 특정한 하드웨어 기기와 TCP 소켓 통신하여 데이터를 받아 처리하여야 하기 때문에,
어플리케이션은 클라이언트가 되어 클라이언트 소켓 프로그래밍을 해야 합니다.
클라이언트 소켓을 만들어 하드웨어 기기의 IP주소에 연결을 요청하고,
약속된 커맨드(데이터)를 송신하여 하드웨어를 시작시키거나 데이터 요청을 하여 특정 데이터(센서 값)를 수신하여야 합니다.
또한 전제로는 어플리케이션이 실행되는 스마트폰(또는 태블릿)과 하드웨어는 같은 네트워크 상에 있어야 통신이 가능 합니다. (공유기 사용 등.)
- Android Manifest 추가
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
위의 Internet Permission을 아래처럼 추가합니다.
클라이언트 소켓 프로그래밍
- 소켓 create생성
- connect연결
- send/recv송수신
- close닫기
- 소켓 생성/연결
소켓을 생성하면서 생성자에 아이피 주소와 포트를 적어주면, 그 IP 주소로 연결을 요청할 수 있습니다.
Socket socket = new Socket("192.168.0.8", 35005);
단순히 생성자만 생각하고, connect()를 써서 연결하는 방법도 있습니다.
|
Socket socket = new Socket(); |
|
SocketAddress addr = new InetSocketAddress("192.168.1.1", 3333/*port*/); |
|
socket.connect(addr); |
하지만 위 구문만으로는 연결이 되지 않는데요, 위의 메니페스트 추가처럼 고려해야할 사항이 있습니다. 하나하나 추가해주세요.
1) 예외처리 구문
|
try { //클라이언트 소켓 생성 |
|
|
|
Socket socket = new Socket("192.168.0.8", 35005); |
|
|
|
} catch (UnknownHostException uhe) { |
|
// 소켓 생성 시 전달되는 호스트(www.unknown-host.com)의 IP를 식별할 수 없음. |
|
|
|
Log.e(TAG," 생성 Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 |
|
값 또는 호스트이름 사용)"); |
|
|
|
} catch (IOException ioe) { |
|
// 소켓 생성 과정에서 I/O 에러 발생. 주로 네트워크 응답 없음. |
|
|
|
Log.e(TAG," 생성 Error : 네트워크 응답 없음"); |
|
|
|
} catch (SecurityException se) { |
|
// security manager에서 허용되지 않은 기능 수행. |
|
|
|
Log.e(TAG," 생성 Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 |
|
의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)"); |
|
|
|
} catch (IllegalArgumentException le) { |
|
// 소켓 생성 시 전달되는 포트 번호(65536)이 허용 범위(0~65535)를 벗어남. |
|
|
|
Log.e(TAG," 생성 Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생. |
|
(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)"); |
|
|
|
} |
예외처리 구문을 만들지 않으면 빌드가 되지 않습니다. 단순히 써줄 수도 있지만 연결 에러에 따라 구분해 보았습니다.
2) 스레드 사용
주의 할 것은, 안드로이드에서는 소켓 연결을 처리할 때 스레드를 사용하여야 합니다. 최근 버전에서는 스레드를 사용하지 않으면 네트워킹 기능 자체가 동작하지 않습니다.
|
button1.setOnClickListener(new Button.OnClickListener() { |
|
|
|
public void onClick(View view) { |
|
// TODO : click event |
|
Toast.makeText(getApplicationContext(), "Connect 시도", Toast.LENGTH_SHORT).show(); |
|
String addr = ipNumber.getText().toString().trim(); |
|
|
|
ConnectThread thread = new ConnectThread(addr); |
|
thread.start(); |
|
|
|
} |
|
}); |
아이피 주소를 입력받아 연결 버튼을 입력하면 연결하도록 레이아웃을 만들었습니다.
버튼을 누르면 온클릭 리스너에서 연결하기 위한 ConnectThread를 시작합니다.
ConnectThread에서 소켓을 생성하고, 연결하도록 하였습니다.
OnCreate()함수 밑에 ConnectThread class를 만들어 줍니다.
|
class ConnectThread extends Thread { |
|
String hostname; |
|
|
|
|
|
public ConnectThread(String addr) { |
|
hostname = addr; |
|
} |
|
|
|
public void run() { |
|
try { //클라이언트 소켓 생성 |
|
|
|
int port = 35000; |
|
socket = new Socket(hostname, port); |
|
Log.d(TAG, "Socket 생성, 연결."); |
|
|
|
Toptext = (TextView) findViewById(R.id.text1); |
|
|
|
runOnUiThread(new Runnable() { |
|
|
|
public void run() { |
|
Toptext.setText("연결 완료"); |
|
Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_LONG).show(); |
|
} |
|
}); |
|
|
|
|
|
} catch (UnknownHostException uhe) { |
|
// 소켓 생성 시 전달되는 호스트(www.unknown-host.com)의 IP를 식별할 수 없음. |
|
|
|
Log.e(TAG, " 생성 Error : 호스트의 IP 주소를 식별할 수 없음. |
|
(잘못된 주소 값 또는 호스트 이름 사용)"); |
|
runOnUiThread(new Runnable() { |
|
public void run() { |
|
Toast.makeText(getApplicationContext(), "Error : 호스트의 IP 주소를 식별할 수 없음. |
|
(잘못된 주소 값 또는 호스트 이름 사용)", Toast.LENGTH_SHORT).show(); |
|
Toptext.setText("Error : 호스트의 IP 주소를 식별할 수 없음. |
|
(잘못된 주소 값 또는 호스트 이름 사용)"); |
|
} |
|
}); |
|
|
|
} catch (IOException ioe) { |
|
// 소켓 생성 과정에서 I/O 에러 발생. |
|
|
|
Log.e(TAG, " 생성 Error : 네트워크 응답 없음"); |
|
runOnUiThread(new Runnable() { |
|
public void run() { |
|
Toast.makeText(getApplicationContext(), "Error : 네트워크 응답 없음", |
|
Toast.LENGTH_SHORT).show(); |
|
Toptext.setText("네트워크 연결 오류"); |
|
} |
|
}); |
|
|
|
|
|
} catch (SecurityException se) { |
|
// security manager에서 허용되지 않은 기능 수행. |
|
|
|
Log.e(TAG, " 생성 Error : 보안(Security) 위반에 대해 |
|
보안 관리자(Security Manager)에 의해 발생. |
|
(프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)"); |
|
runOnUiThread(new Runnable() { |
|
public void run() { |
|
Toast.makeText(getApplicationContext(), "Error : 보안(Security) 위반에 대해 |
|
보안 관리자(Security Manager)에 의해 발생. |
|
(프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)", Toast.LENGTH_SHORT).show(); |
|
Toptext.setText("Error : 보안(Security) 위반에 대해 |
|
보안 관리자(Security Manager)에 의해 발생. |
|
(프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)"); |
|
} |
|
}); |
|
|
|
|
|
} catch (IllegalArgumentException le) { |
|
// 소켓 생성 시 전달되는 포트 번호(65536)이 허용 범위(0~65535)를 벗어남. |
|
|
|
Log.e(TAG, " 생성 Error : 메서드에 |
|
잘못된 파라미터가 전달되는 경우 발생. |
|
(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)"); |
|
runOnUiThread(new Runnable() { |
|
public void run() { |
|
Toast.makeText(getApplicationContext(), " Error : 메서드에 |
|
잘못된 파라미터가 전달되는 경우 발생. |
|
(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)", Toast.LENGTH_SHORT).show(); |
|
Toptext.setText("Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생. |
|
(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)"); |
|
} |
|
}); |
|
|
|
} |
|
|
|
} |
|
} |
중간중간 토스트 메시지를 띄우고 텍스트를 입력하는것은 UI를 입력하는 것이기 때문에, 스레드 안에서는 동작하지 않습니다. 그래서 별도의 runOnUiThread를 써 처리해주었습니다.
|
runOnUiThread(new Runnable() { |
|
|
|
public void run() { |
|
Toptext.setText("연결 완료"); |
|
Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_LONG).show(); |
|
} |
|
}); |
3) 허가 코드 작성
또한 연결에 앞서, 안드로이드 3.0 부터는 밑의 코드를 추가해 놓지 않으면 에러가 납니다.
|
int SDK_INT = android.os.Build.VERSION.SDK_INT; |
|
|
|
if (SDK_INT > 8){ |
|
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); |
|
StrictMode.setThreadPolicy(policy); |
|
} |
위의 사항들을 다 고려하여 소켓 연결에 성공하였습니다.
- 데이터 송신
제가 사용한 모듈은 연결 후 하드웨어에 시작하기 위한 AT Command를 보내주어야 합니다. (데이터 송신)
START 버튼을 누르면, AT Command(데이터)를 송신하고 센서값을 수신하기 위한 Thread를 또 만들어 처리하였습니다.
|
StartButton.setOnClickListener(new Button.OnClickListener() { |
|
|
|
public void onClick(View view) { |
|
StartThread sthread = new StartThread(); |
|
sthread.start(); |
|
|
|
} |
|
}); |
출력 스트림을 사용하면 데이터를 보낼 수 있습니다. OutputStream Class를 사용하여 데이터를 바이트 배열로 보냈습니다.
OutputStream | |
void close() | 출력 스트림(또는 이 스트림과 관련된 시스템 모두)을 닫는다. |
void flush() | flush하고 저장된 데이터를 write한다. |
void write(byte[] b) | b의 길이만큼 write한다. |
void wirte(byte[] b, int off, int len) | b를 offset인 off부터 len bytes만큼 write한다. |
abstract void write(int b) | b write |
|
// 데이터 송신 |
|
try { |
|
|
|
String OutData = "AT+START\n"; |
|
byte[] data = OutData.getBytes(); |
|
OutputStream output = socket.getOutputStream(); |
|
output.write(data); |
|
Log.d(TAG, "AT+START\\n COMMAND 송신"); |
|
|
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
Log.d(TAG,"데이터 송신 오류"); |
|
} |
- 데이터 수신
이 모듈은 Start AT Command를 보내면 데이터를 보내기 시작합니다. 그 데이터를 수신하기 위해 InputStream Class를 사용하였습니다.
InputStream | |
int available() | 입력 스트림으로부터 읽을수 있는 byte를 측정하여 리턴한다. |
void close() | 출력 스트림(또는 이 스트림과 관련된 시스템 모두)을 닫는다. |
void mark(int readlimit) | 이 입력 스트림의 현재 위치를 mark한다. |
boolean markSupoorted() | 이 입력 스트림이 mark와 reset method를 지원하는지 테스트한다. |
abstract int read() | 입력 스트림으로부터 다음 데이터 byte를 읽는다. |
int read(byte[] b, int off, int len) | offset인 off부터 len bytes만큼 byte 배열 b에 읽어온다. |
int read(byte[] b) | byte 배열 b에 입력스트림으로부터 다수의 byte를 읽어온다. |
void reset() |
이 입력 스트림을 mark 메소드가 마지막으로 호출 된 시점의 위치로 재배치합니다. |
void skip(long n) | 입력 스트림으로부터 n bytes의 데이터를 버리고 skip한다. |
|
// 데이터 수신 |
|
try { |
|
Log.d(TAG, "데이터 수신 준비"); |
|
|
|
while (true) { |
|
byte[] buffer = new byte[1024]; |
|
InputStream input = socket.getInputStream(); |
|
|
|
bytes = input.read(buffer); |
|
Log.d(TAG, "byte = " + bytes); |
|
tmp = byteArrayToHex(buffer); |
|
Log.d(TAG, tmp); |
|
} |
|
}catch(IOException e){ |
|
e.printStackTrace(); |
|
Log.e("ZENTRItest","데이터 수신 오류"); |
|
} |
받은 byte를 hexa로 변환해서 string으로 Log에 출력하기 위한 함수입니다.
|
public String byteArrayToHex(byte[] a) { |
|
StringBuilder sb = new StringBuilder(); |
|
for(final byte b: a) |
|
sb.append(String.format("%02x ", b&0xff)); |
|
return sb.toString(); |
|
} |
데이터를 송신하고 바로 수신하는 StartThread는 이렇게 됩니다.
|
class StartThread extends Thread{ |
|
|
|
|
|
public StartThread(){ |
|
} |
|
|
|
String tmp; |
|
|
|
public String byteArrayToHex(byte[] a) { |
|
StringBuilder sb = new StringBuilder(); |
|
for(final byte b: a) |
|
sb.append(String.format("%02x ", b&0xff)); |
|
return sb.toString(); |
|
} |
|
|
|
public void run(){ |
|
// 데이터 송신 |
|
try { |
|
|
|
String OutData = "AT+START\n"; |
|
byte[] data = OutData.getBytes(); |
|
OutputStream output = socket.getOutputStream(); |
|
output.write(data); |
|
Log.d(TAG, "AT+START\\n COMMAND 송신"); |
|
|
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
Log.d(TAG,"데이터 송신 오류"); |
|
} |
|
|
|
// 데이터 수신 |
|
try { |
|
Log.d(TAG, "데이터 수신 준비"); |
|
|
|
while (true) { |
|
byte[] buffer = new byte[1024]; |
|
InputStream input = socket.getInputStream(); |
|
|
|
bytes = input.read(buffer); |
|
Log.d(TAG, "byte = " + bytes); |
|
tmp = byteArrayToHex(buffer); |
|
Log.d(TAG, tmp); |
|
|
|
} |
|
}catch(IOException e){ |
|
e.printStackTrace(); |
|
Log.e(TAG,"데이터 수신 오류"); |
|
} |
|
|
|
|
|
} |
|
|
|
} |
- Socket close
소켓은 끝날 때 꼭 닫아서 연결 해제해줘야한다.
|
|
|
protected void onStop() { //앱 종료시 |
|
super.onStop(); |
|
try { |
|
socket.close(); //소켓을 닫는다. |
|
} catch (IOException e) { |
|
e.printStackTrace(); |
|
} |
'Android' 카테고리의 다른 글
Kotlin(코틀린) 에서 fragment 를 써보자 (0) | 2021.04.04 |
---|---|
ListView(리스트뷰) 자동 높이, 크기 조절 (0) | 2021.03.29 |
MVVM 패턴 설명 - 2(view Model) (0) | 2021.03.27 |
MVVM 패턴 - 설명(1) (View) (0) | 2021.02.11 |
안드로이드에서의 Parcelable vs Serializable (0) | 2021.01.28 |
안드로이드 DI 라이브러리 Koin 에 대해 (0) | 2021.01.24 |
안드로이드 MVVM 패턴 - 개요 (0) | 2021.01.24 |
마법같은 애니메이션 Lottie 에 대해서 (0) | 2021.01.19 |