티스토리 뷰
이전까지 싱글로 구현한 뿌요뿌요를 멀티로 확장해보자
기능 설계
Server
- socket을 통하여 C base 서버를 구현한다.
- 여러 사용자가 동시에 match가 가능하게 구현한다 (multi thread)
- 게임이 진행 중에 player들의 서로의 정보를 수신, 송신해주는 중계역 역할을 수행한다.
Client
- 상대방이 연결될때까지 wait
- 게임이 진행되는 도중 비동기적인 player 정보를 송수신 필요 (상대방의 공격, field update)
Server
Socket
리눅스 상에서 멀티 즉 통신을 구현하기 위하여 소켓을 사용한다. 소켓은 process끼리 통신이 가능하게 구현된 프로토콜이다.
우리가 사용할 network socket은 원하는 transport(tcp or udp) , ip procotol을 설정하고 server, client끼리 통신이 가능하게 한다.
socket은 운영체제의 kernel에서 관리하기 때문에 bind, listen 시 system call을 통해 kernel로 요청하게 된다.
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
// 소켓 생성
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
// 소켓 init
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(7001);
// 소켓 bind
if(bind(serv_sock,(struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind error");
// 소켓 listen
if(listen(serv_sock, 5) == -1)
error_handling("listen error");
위는 서버가 socket을 생성하고 kernel에게 해당 socket으로 통신이 가능하게 요청하는 코드이다.
소켓 생성
socket() 함수를 통해 소켓을 생성한다. 이때 소켓은 file descriptor로 serv_sock 에 정수를 저장한다. 또한 우리가 사용할 socket은 network 소켓(PF_INET)임과 TCP protocol(SOCK_STREAM)임을 인자로 넘겨준다. socket 생성에 실패한다면 -1을 반환한다.
소켓 설정
socket의 ip address와 port를 지정하기 위해 해당 정보를 할당한다. 이때 ip address를 32bit big edian으로 할당해야한다.
하지만 C의 기본 자료형은 little edian임으로 htonl 함수를 통해 host 주소를 big edian으로 변환한다. INADDR_ANY는 localhost(127.0.0.1)임을 의미한다. 마찬가지로 포트 정보도 big edian으로 변환한다.
소켓 bind
주소 및 port를 할당받은 socket을 사용할 준비를 한다. 이때 운영체제 내에서 동일한 포트를 사용하고 있으면 bind를 실패한다.
소켓 listen
bind가 끝난 socket을 연결을 받도록 대기한다. client에서 connect 요청을 보내면 서버에서 이를 감지하고 연결하게 된다. 이때 인자로 넘겨진 5는 client 대기 queue size로, 여러 connect 요청이 왔을 때 서버가 busy 상태라면 최대 5개까지의 요청들이 대기되고 나머진 disconnect된다.
while(1){
clnt_adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr,&clnt_adr_sz);
printf("client connect\n");
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++]= clnt_sock;
waiting_player++;
if(waiting_player ==2){
pthread_create(&t_id, NULL, handle_clnt, NULL);
pthread_detach(t_id);
waiting_player = 0;
printf("match start\n");
}
pthread_mutex_unlock(&mutx);
}
socket이 listen 상태이면 client에서 connect 요청을 보내게 된다. 서버는 accept함수에서 wait하다 client에서 connect 요청이 오게 되면 탈출하여 해당 요청을 처리하고 다시 accept으로 돌아와 다음 요청을 기다린다. 이 때 client 별로 하나의 socket을 생성하여 데이터를 주고 받게 된다.
우리가 구현할 서버는 두명의 client가 연결되면 둘을 묶어 match를 진행시킨다. 이후 해당 match는 thread를 생성하여 처리하며 main thread는 다음 client를 기다린다.
player temp_data;
int rvc_socket;
int snd_socket;
rvc_socket = ((int*) arg)[0];
snd_socket = ((int*) arg)[1];
while(1){
while(read(rvc_socket,(player *) &temp_data,sizeof(temp_data))<0);
write(snd_socket, (player *)&temp_data, sizeof(temp_data));
if (!temp_data.online)
break;
}
위의 코드는 두명의 client가 데이터를 주고받는 것을 처리한다. 서버는 두명의 client의 중계역으로 하나의 client에게서 온 player정보를 상대방 client에게 전송한다.
해당 모듈을 하나의 thread로 단방향으로 데이터를 전송하게 한다. 두개의 thread를 생성하여 양방향으로 데이터가 전송되게 구현한다.
Client
이제 client side에서 통신을 구현한다.
async vs sync
client side에서는 1초마다 블럭이 아래로 내려와야 하고, 사용자의 입력에 따라 interrupt가 상당히 많이 발생한다. 여러 event가 발생하는 상황에서 client는 field의 정보를 server에게 전송하고, 마찬가지로 상대방의 field 변화를 수신받아 화면상에 그리게 된다.
그렇다면 해당 기능을 수행하는 코드는 어느시점에 발생하게 해야할까,
- main flow에서 1초마다 블럭이 내려가게 하는 signal handler가 끝나는 시점에 call하여 1초마다 필드 정보를 동기화한다
- thread를 생성해 main flow와 관계없이 socket에서 read를 대기하고 있는다
위와 같이 두가지 방법을 생각해볼 수 있다. 전자는 synchronous, 후자는 asynchronous하다. 두가지 방법 모두 틀린 방법은 적절한 기능에 맞게 trade off 해야한다. 게임의 특성상 event의 발생 시 즉각적인 데이터 반영이 되어야하고 마찬가지로 상대의 event 발생 시 이를 반영해야 한다. 다만 너무 많은 데이터 전송 시 안정성 및 서버 부하가 커지니 적당히 조절한다.
전송 : player의 event(field update, quit) 발생 시 -> Block down 모듈에서 서버에 player field 정보 전송 (sync)
수신 : 서버로부터 상대방의 데이터 수신 대기 (async)
void BlockDown(int sig){
...
...
if(multi){
sendPlayerData();
}
process_flag = 0;
timed_out = 0;
}
1초 동안 주기적으로 call되는 blockDown 함수의 일부분으로 player의 event는 해당 모듈 내에서 처리된다. 함수 종료 전에 데이터 전송을 한다.
void * receiveData(void * arg){
int socket = *((int*)arg);
while(1){
if(gameOver)
break;
read(socket,(player*)&opPlayer,sizeof(opPlayer));
pthread_mutex_lock(&mutx);
if(!opPlayer.online)
break;
drawOpField();
refresh();
printOpScore();
if(opPlayer.score != op_score && !attack_flag){
attack_flag = 1;
attack_score = opPlayer.score - op_score;
op_score = opPlayer.score;
}
pthread_mutex_unlock(&mutx);
}
return NULL;
}
데이터를 수신하는 모듈이다. 루프문을 통하여 서버로부터 데이터를 수신하는 것을 대기하고 있는다. 이때, mutex lock을 걸어 main flow에서 도중에 opPlayer의 정보의 수정을 막는다.
멀티를 구현한 모습은 아래와 같다.
ttyd - Terminal
puyo.cspc.me
https://github.com/ljy2855/puyo_tetris
GitHub - ljy2855/puyo_tetris: PuyoPuyo Tetris game based terminal
PuyoPuyo Tetris game based terminal. Contribute to ljy2855/puyo_tetris development by creating an account on GitHub.
github.com
'프로젝트 > 뿌요뿌요 테트리스' 카테고리의 다른 글
[Puyo tetris] #2 뿌요뿌요 게임 로직을 코드로 구현하기 (signal) (2) | 2021.11.22 |
---|---|
[Puyo tetris] #1 터미널 상에서 뿌요뿌요를 구현해보자 (NCURSES) (0) | 2020.12.26 |
[Puyo tetris] #0 터미널 상에서 뿌요뿌요를 구현해보자 (게임 설명, 구현 목록) (0) | 2020.12.26 |
- Total
- Today
- Yesterday
- OpenSearch
- 백준
- 웹IDE
- pvm
- 해커톤
- FastAPI
- 코딩
- Web
- 토이프로젝트
- 싸지방
- codeanywhere
- ttyd
- io blocking
- pintos
- Python
- 리눅스
- 구름ide
- 프로젝트
- vector search
- 시간 초과
- 뿌요뿌요
- os
- 분할 정복
- 뿌요뿌요 테트리스
- 사이버정보지식방
- 정보보호병
- HNSW
- react
- C
- letsencrypt
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |