* 이번 시간에는 socket을 이용해서 서버(리눅스) - 안드로이드(클라이언트) 통신을 해서 간단한 쿵쿵따 앱을 만들어 보겠습니다.
먼저 간단한 사용자 시나리오는 아래 그림과 같습니다.
전체적인 구성은 리눅스 서버 - 안드로이드 클라이언트입니다.
- 리눅스 환경은 우분투 쉘을 사용했습니다. 서버 IP 주소는 제 컴퓨터 IP 주소로 했습니다.
- 우분투 쉘은 윈도우 10 기준, Microsoft Store에서 Ubuntu를 검색하여 설치할 수 있습니다. putty 터미널 사용해도 무방합니다.
시스템 흐름도는 간단하게 먼저 잡아봤습니다.
클라이언트에서 먼저 첫 번째 제시어(라디오)를 게임 시작과 함께 서버에 보냅니다. 순서는 먼저 접속한 클라이언트부터 메시지를 보낼 수 있게 했습니다. 클라이언트 1은 오징어를 먼저 보내고 서버에서는 read 함수를 통해 받고 버퍼에 저장하고 버퍼에 저장된 문자열을 write 함수를 통해 서버에 접속된 모든 클라이언트에게 전송합니다. 클라이언트 2는 어린이를 보내고 마찬가지로 서버에서 받고, 다시 모든 클라이언트에게 보냅니다. 만약 사용자가 잘못된 답을 입력했다면 클라이언트 측에서 lose라는 문자열을 서버로 전송해 서버는 패배와 승리자를 구분하고 각각의 클라이언트들에게 소식을 전송합니다. strmp 함수가 lose 문자열을 탐색하여 있을 시 그 문자열을 보낸 클라이언트에게만 패배 메세지를 전송하고 나머지 클라이언트에게는 승리 메세지를 전송합니다.
서버 소스 코드(select 함수)
- 우분투 쉘에서 vi 에디터를 사용하여 c 파일을 작성합니다.(기본적으로 리눅스를 다룰 줄 알아야겠죠?)
- vi 파일명.c
- :wq로 저장
- 컴파일 명령어 : gcc -o 실행 파일명 실행 파일명. c
- 서버 실행 명령어 : ./실행파일명 포트번호
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
#define MAXCLIENT 5
void error_handling(char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
int iCNum = 4;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
int iaClient[MAXCLIENT];
if (argc != 2){
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1 )
error_handling("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while(1){
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
memset(buf, 0, sizeof(buf));
int j = 0;
if((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if(fd_num == 0)
continue;
for(i = 0; i < fd_max + 1; i++){
if(FD_ISSET(i, &cpy_reads)){
if(i == serv_sock){
adr_sz = sizeof(clnt_adr);
clnt_sock =
accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client : %d \n", clnt_sock);
for(int k = j; k < j+1; k++)
{
iaClient[k] = clnt_sock;
printf("배열에 저장된 값 : %d\n",iaClient[k]);
}
j++;
}
else
{
//read message
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0){
//close request
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else{
char *str1 = "lose";
if(strcmp(buf, str1) == 0)
{
if( i == 4 )
{
write(i, "패배", str_len);
for(int z = i+1; z <= clnt_sock; z++)
{
write(z, "승리", str_len);
}
}
else if(i == 5)
{
//write(i, "패배", str_len);
//write(i-1, "승리", str_len);
for(int z = 4; z <= clnt_sock; z++)
{
if( z == 5 )
write(z, "패배", str_len);
else
write(z, "승리", str_len);
}
}
else if(i == 6)
{
for(int z = 4; z <= clnt_sock; z++)
{
if( z == 6 )
write(z, "패배", str_len);
else
write(z, "승리", str_len);
}
}
else if(i == 7)
{
for(int z = 4; z <= clnt_sock; z++)
{
if( z == 7 )
write(z, "패배", str_len);
else
write(z, "승리", str_len);
}
}
}
else{
if(iCNum == i)
{
for(int z = 4; z <= clnt_sock; z++){
write(z, buf, str_len);
}
printf("%d 번 클라이언트의 message : %s\n",i, buf);
printf("sendmessage : %s\n", buf);
if(iCNum == clnt_sock)
iCNum = 4;
else
iCNum ++;
}
else
{
write(i, "차례X", str_len);
}
}
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
}
클라이언트 소스 코드
MainActivity
package com.example.socket_kung;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.media.Image;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
public class MainActivity extends AppCompatActivity {
ImageButton playButton ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
playButton = (ImageButton) findViewById(R.id.playButton);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent startIntent = new Intent(getApplicationContext(), PlayActivity.class);
startActivity(startIntent);
//MainActivity.this.finish();
}
});
}
}
PlayActivity
- 여기서 핵심은 byteordering입니다.
- 서버에 데이터를 전송할 때 byteOrdering 처리를 해야 합니다. java와 c는 바이트를 메모리에 적재하는 방식이 다르기 때문입니다. 서버에서는 메모리를 적재할 때 littleEndian 방식으로 하기 때문에 littleEndian으로 변환하여 데이터를 전송해야 합니다.
- 그리고 서버에서 받은 메시지를 수신할 때, 필요한 문자 외에 쓰레기 값도 함께 오는데, 이 이유를 찾지 못했습니다. 혹시 아시는 분은 댓글 달아주시면 감사하겠습니다. 그래서 저는 필요한 문자의 자릿수까지 잘라서 사용하고 쓰레기 값은 제외시켰습니다. String 객체의 subString 함수를 사용해 첫 번째 자리부터 해당되는 자리까지 잘라서 사용합니다.
package com.example.socket_kung;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.SpannableString;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PlayActivity extends AppCompatActivity {
private int port = 포트번호입력자리;
EditText inputText;
ImageButton sendButton, finishButton;
TextView outPutText, resultText;
Handler handler = new Handler();
Socket sock;
String contents, readData, imread, youread;
int lastIndex;
char lastchar;
boolean gameset = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_play);
//UI 초기화화
init();
//액티비티 실행을 하자마자 서버 연결
connectedServer();
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
sendtoServer();
}
});
finishButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
PlayActivity.this.finish();
new Thread(new Runnable() {
@Override
public void run() {
try {
sock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
});
}
public void init(){
inputText = (EditText) findViewById(R.id.inputText);
sendButton = (ImageButton) findViewById(R.id.sendButton);
outPutText = (TextView) findViewById(R.id.outputTextView);
outPutText.setText("라디오");
finishButton = (ImageButton) findViewById(R.id.container);
resultText = (TextView) findViewById(R.id.resultTextView);
}
public void connectedServer(){
String data = outPutText.getText().toString();
Toast.makeText(getApplicationContext(), "서버에 연결됨." , Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
try{
//소켓연결
sock = new Socket("IP주소입력칸", port);
//서버 연결 메세지 전송을 위한 outputstream
OutputStream os = sock.getOutputStream();
//byte ordering을 위한 byteBuffer 선언
ByteBuffer sendByteBuffer = null;
sendByteBuffer = ByteBuffer.allocate(100);
sendByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
sendByteBuffer.put(data.getBytes());
sendByteBuffer.put(new byte[data.getBytes().length + 1]);
//outputstream객체를 통해 write
os.write(sendByteBuffer.array());
os.flush();
//서버에서 메세지를 받기 위해 inputstream객체 선언
BufferedInputStream bis = new BufferedInputStream(sock.getInputStream());
byte[] buff = new byte[1024];
int read = 0;
while(true){
if(sock == null)
break;
read = bis.read(buff,0,1024);
if(read < 0)
break;
byte[] tempArr = new byte[read];
System.arraycopy(buff, 0, tempArr, 0, read);
readData = new String(tempArr);
readData = readData.substring(0,3);
printClientLog(readData);
youread = readData.substring(0,2);
String win = "승리";
if(youread == null || youread.equals(win)){
printResultLog("승리");
}
System.out.println("연결요청 : read : " + youread);
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
public void sendtoServer(){
//데이터 전송 담당하는 메소드
final String data = inputText.getText().toString(); //정답입력
contents = outPutText.getText().toString();
lastIndex = contents.length() -1;
lastchar = contents.charAt(lastIndex);
System.out.println(contents);
//서버에게 데이터를 전송한다.
if(data.charAt(0) != lastchar){
gameset = false;
Toast.makeText(getApplicationContext(), "졌습니다. ",Toast.LENGTH_SHORT).show();
//게임결과메소드 호출
resultGame("lose");
System.out.println(contents);
}
else if(data.length() != 3){
Toast.makeText(getApplicationContext(), "3글자만 입력해주세요.", Toast.LENGTH_LONG).show();
}
else{
new Thread(new Runnable() {
@Override
public void run() {
try{
OutputStream os = sock.getOutputStream();
ByteBuffer sendByteBuffer = null;
sendByteBuffer = ByteBuffer.allocate(100);
sendByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
sendByteBuffer.put(data.getBytes());
sendByteBuffer.put(new byte[data.getBytes().length + 1]);
os.write(sendByteBuffer.array());
os.flush();
BufferedInputStream bis = new BufferedInputStream(sock.getInputStream());
byte[] buff = new byte[1024];
int read = 0;
while(true){
if(sock == null)
break;
read = bis.read(buff,0,1024);
if(read < 0)
break;
byte[] tempArr = new byte[read];
System.arraycopy(buff, 0, tempArr, 0, read);
readData = new String(tempArr);
readData = readData.substring(0,3);
printClientLog(readData);
System.out.println("송수신 : read : " + readData);
youread = readData.substring(0,2);
String win = "승리";
System.out.println("두글자바꾼거 : " + youread);
if(youread == null || youread.equals(win)){
printResultLog("승리");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
public void resultGame(String msg){
final String resultMsg = msg;
new Thread(new Runnable() {
@Override
public void run() {
try{
OutputStream os = sock.getOutputStream();
ByteBuffer sendByteBuffer = null;
sendByteBuffer = ByteBuffer.allocate(100);
sendByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
sendByteBuffer.put(resultMsg.getBytes());
sendByteBuffer.put(new byte[resultMsg.getBytes().length + 1]);
os.write(sendByteBuffer.array());
os.flush();
BufferedInputStream bis = new BufferedInputStream(sock.getInputStream());
byte[] buff = new byte[1024];
int read = 0;
while(true){
if(sock == null)
break;
read = bis.read(buff,0,1024);
if(read < 0)
break;
byte[] tempArr = new byte[read];
System.arraycopy(buff, 0, tempArr, 0, read);
imread = new String(tempArr);
imread = imread.substring(0,2);
printResultLog(imread);
System.out.println("결과 : read : " + imread);
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
public void printClientLog(final String data){
handler.post(new Runnable() {
@Override
public void run() {
outPutText.setText("");
outPutText.setText(data);
}
});
}
public void printResultLog(final String data){
handler.post(new Runnable() {
@Override
public void run() {
outPutText.setText("");
resultText.setText(data);
if(data == "승리"){
showDialog("승리");
}else{
showDialog("패배");
}
}
});
}
public void showDialog(String data) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(data);
System.out.println(data);
builder.setMessage("게임을 다시 하시겠습니까?");
builder.setPositiveButton("예",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
builder.setNegativeButton("아니오",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
PlayActivity.this.finish();
new Thread(new Runnable() {
@Override
public void run() {
try {
sock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
});
builder.show();
}
}
manifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.socket_kung">
<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/Theme.Socket_Kung">
<activity android:name=".PlayActivity"></activity>
<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>
main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/sock_image1"
tools:context=".MainActivity">
<ImageButton
android:id="@+id/playButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="524dp"
android:background="@drawable/play"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.496"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
play.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/back"
tools:context=".PlayActivity">
<ImageButton
android:id="@+id/sendButton"
android:layout_width="86dp"
android:layout_height="49dp"
android:layout_marginStart="300dp"
android:layout_marginTop="592dp"
android:background="@android:drawable/ic_menu_send"
android:text="보내기"
android:textColor="@color/white"
android:textSize="20dp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/resultTextView"
android:layout_width="285dp"
android:layout_height="360dp"
android:layout_marginTop="80dp"
android:gravity="center_horizontal"
android:padding="50dp"
android:textSize="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/container"
android:layout_width="66dp"
android:layout_height="64dp"
android:layout_marginStart="340dp"
android:layout_marginTop="16dp"
android:background="@drawable/ic_baseline_sensor_door_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="218dp"
android:layout_height="145dp"
android:layout_marginStart="96dp"
android:layout_marginTop="80dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:drawable/sym_action_chat" />
<EditText
android:id="@+id/inputText"
android:layout_width="243dp"
android:layout_height="51dp"
android:layout_marginStart="44dp"
android:layout_marginTop="592dp"
android:background="#ffffff"
android:ems="10"
android:gravity="center"
android:hint="정답을 입력해주세요."
android:inputType="textPersonName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/outputTextView"
android:layout_width="285dp"
android:layout_height="360dp"
android:layout_marginTop="80dp"
android:gravity="center_horizontal"
android:padding="50dp"
android:text="라디오"
android:textStyle="bold"
android:textColor="@color/purple_700"
android:textSize="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
게임 플레이 영상을 한번 보실까요?
용량 때문에 부득이하게 에뮬레이터를 2개로 시연해봤습니다. 실제로는 4개의 클라이언트가 게임을 즐길 수 있습니다. 여러분들은 소스코드를 수정하여 여러 명이 즐길 수 있는 쿵쿵따 게임을 만들어 보세요!
이상 부족하고 초라한 게임을 봐주셔서 감사합니다.
'Android' 카테고리의 다른 글
[부스트코스:안드로이드 프로그래밍] 프로젝트 B . 좋아요와 한줄평 리스트 (0) | 2021.03.01 |
---|---|
[안드로이드 스튜디오 : TextView, EditText, Button 사용하기 with Kotlin] (0) | 2021.02.18 |
[부스트코스:안드로이드 프로그래밍] 프로젝트 A . 영화 상세 화면 만들기 (0) | 2021.02.02 |
[안드로이드 스터디 : 수강신청 앱 만들기2] (0) | 2021.01.30 |
[안드로이드 스터디 : 수강신청 앱 만들기] (2) | 2021.01.30 |