소켓 통신에서는 클라이언트와 서버 사이의 연결이 지속되고 실시간으로 양방향으로 데이터를 주고받는다. 오늘 할 일은 아래와 같다.
- PC에서 이클립스와 Java를 통해 서버를 만들고 소켓을 뚫어서 돌린다.
- 안드로이드 단말에서 위에서 만든 서버로 요청을 보내고 응답을 받는다.
여기서 주의해야할 것이 있다. PC로 서버를 돌릴 때, 공용 wifi를 사용했다면 IP주소는 192로 시작한다. 이 때는 안드로이드 역시 같은 공용 와이파이를 사용하는 상태라야 서버로 접근할 수 있다. 왜냐하면 공유기에 물려서 내부에서만 유효한 네트워크에 연결된 상태이기 때문에 외부에서 접근할 방법이 없기 때문이다. 이 부분을 모르고 포트포워딩을 하려다가 괜히 삽질로 시간만 날릴 수 있다.
먼저 이클립스에서 프로젝트를 생성하여 서버측 코드를 작성한다.
public class SocketServer {
public static void main(String[] args) {
int portNumber = 5555;
try {
System.out.println("서버를 시작합니다...");
ServerSocket serverSocket = new ServerSocket(portNumber); //포트번호를 매개변수로 전달하면서 서버 소켓 열기
System.out.println("포트 " + portNumber + "에서 요청 대기중...");
while(true) {
Socket socket = serverSocket.accept(); //클라이언트가 접근했을 때 accept() 메소드를 통해 클라이언트 소켓 객체 참조
InetAddress clientHost = socket.getLocalAddress();
int clientPort = socket.getPort();
System.out.println("클라이언트 연결됨. 호스트 : " + clientHost + ", 포트 : " + clientPort);
ObjectInputStream instream = new ObjectInputStream(socket.getInputStream()); //소켓의 입력 스트림 객체 참조
Object obj = instream.readObject(); // 입력 스트림으로부터 Object 객체 가져오기
System.out.println("클라이언트로부터 받은 데이터 : " + obj); // 가져온 객체 출력
ObjectOutputStream outstream = new ObjectOutputStream(socket.getOutputStream()); //소켓의 출력 스트림 객체 참조
outstream.writeObject(obj + " from server"); //출력 스트림에 응답 넣기
outstream.flush(); // 출력
socket.close(); //소켓 해제
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
서버 소켓의 accept()
메소드를 사용하면 클라이언트의 요청이 왔을 때 클라이언트 소켓의 연결정보를 확인할 수 있다. 여기서 참조한 소켓의 입력 스트림을 통해 입력된 데이터를 읽고 출력 스트림을 생성하여 소켓을통해 내보낸다. 마지막으로 소켓을 해제한다.
이제 클라이언트 측 코드를 안드로이드 스튜디오에서 작성할 차례다. 먼저 화면은 연결할 서버의 IP주소와 전송할 데이터를 위한 입력상자 2개와 클릭했을 때 전송을 하기위한 버튼을 하나 만든다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/addressInput"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:hint="서버 IP"
android:textSize="20dp"
android:layout_marginTop="200dp"
android:layout_gravity="center_horizontal"/>
<EditText
android:id="@+id/dataInput"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:hint="보낼 데이터"
android:textSize="20dp"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"/>
<Button
android:id="@+id/socketConnectBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="서버 전송"
android:textSize="24dp"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
클라이언트 자바 코드는 아래와 같이 작성한다.
public class MainActivity extends AppCompatActivity {
EditText addressInput; //호스트 IP 입력상자
EditText dataInput; //서버로 전송할 데이터 입력상자
String str;
String addr;
String response; //서버 응답
Handler handler = new Handler(); // 토스트를 띄우기 위한 메인스레드 핸들러 객체 생성
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addressInput = findViewById(R.id.addressInput);
dataInput = findViewById(R.id.dataInput);
Button socketConnectBtn = findViewById(R.id.socketConnectBtn);
/*
버튼을 클릭했을 때
1. 입력상자의 서버 IP 주소와 전송할 데이터 가져오기
2. 소켓통신을 위한 스레드의 매개변수로 넣어주어 스레드 객체 생성
3. 스레드 시작
*/
socketConnectBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addr = addressInput.getText().toString().trim();
str = dataInput.getText().toString();
SocketThread thread = new SocketThread(addr, str);
thread.start();
}
});
}
class SocketThread extends Thread{
String host; // 서버 IP
String data; // 전송 데이터
public SocketThread(String host, String data){
this.host = host;
this.data = data;
}
@Override
public void run() {
try{
int port = 5555; //포트 번호는 서버측과 똑같이
Socket socket = new Socket(host, port); // 소켓 열어주기
ObjectOutputStream outstream = new ObjectOutputStream(socket.getOutputStream()); //소켓의 출력 스트림 참조
outstream.writeObject(data); // 출력 스트림에 데이터 넣기
outstream.flush(); // 출력
ObjectInputStream instream = new ObjectInputStream(socket.getInputStream()); // 소켓의 입력 스트림 참조
response = (String) instream.readObject(); // 응답 가져오기
/* 토스트로 서버측 응답 결과 띄워줄 러너블 객체 생성하여 메인스레드 핸들러로 전달 */
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "서버 응답 : " + response, Toast.LENGTH_LONG).show();
}
});
socket.close(); // 소켓 해제
}catch(Exception e){
e.printStackTrace();
}
}
}
}
여기서 중요한 것이 있다.
안드로이드에서는 네트워킹을 반드시 새로운 스레드를 통해서 해야한다. 최신 버전부터 이렇게 변경되었다. 따라서 네트워킹을 통해 UI를 업데이트 할 때는 메인 스레드의 핸들러를 통해서 해야한다.
아무튼 버튼을 클릭했을 때 새로운 스레드를 만들고 소켓을 뚫은다음 소켓의 출력 스트림을 통해 데이터를 내보내주는 것이 전부이다. 이 때 소켓 생성 시 서버IP와 연결할 포트번호를 매개변수로 전달해주어야한다.
또한 네트워크 통신을 하기위해 매니페스트 파일을 열고 다음과 같이 인터넷 권한을 부여한다.
<uses-permission android:name="android.permission.INTERNET"/>
먼저 이클립스에서 클래스를 실행하면 다음과 같은 화면을 볼 수 있다.
이제 안드로이드 앱을 실행한 아래 사진과 같이 다음 IP주소와 보낼 데이터를 입력하고 보내면 된다.
서버 IP주소는 PC에서 네트워크 및 공유센터에서 확인하면 된다. 아무튼 코드를 작성한대로 서버에서 받은 데이터에 " from server" 라는 문자열을 덧붙여서 응답으로 보내는 것을 볼 수 있다.
이 때, 서버측으로 다시 돌아가서 출력 결과를 마저 확인해보자.
코드에서 System.out.println()
을 통해 클라이언트의 IP주소와 포트 번호를 확인할 수 있도록 해놓았었다. 또한 받은 데이터 역시 출력하도록 했었다. 그 결과를 위 출력화면에서 확인할 수 있다.
아주 간단한 기능만 하는 서버와 클라이언트를 작성했지만, 이 내용은 안드로이드에서 네트워크를 사용하기 위한 기본이 된다. 결국 모바일 게임등의 네트워크가 필요한 애플리케이션들은 모두 위의 내용을 기초하여 구현하는 것이기 때문이다.
'안드로이드' 카테고리의 다른 글
(안드로이드) 52 - RSS 피더 만들기 (0) | 2020.05.19 |
---|---|
(안드로이드) 51 - HTTP (0) | 2020.05.19 |
(안드로이드) 49 - 네트워크(Network) 개요 (0) | 2020.05.19 |
(안드로이드) 48 - AsyncTask (0) | 2020.05.19 |
(안드로이드) 47 - 메시지 큐(Message Queue)와 루퍼(Looper) (0) | 2020.05.19 |