본문 바로가기
GameMaker강좌[GMS2]/네트워크강좌

[게임메이커 강좌-네트워킹][GMS2] 채팅 프로그램 만들기-3-메시지 송수신

by 타락카얀 2024. 12. 23.
728x90

 

 

GAME MAKER 강좌

 

KAYAN

 

 

 

 

 

 

◈ 메시지를 보내고 받기

 

일반적으로 메시지 패킷은 버퍼로 구성합니다.

강좌에서 메시지를 보낼 때 데이터 기본 순서는 아래와 같습니다.

 
◎ 패킷 보내기 순서
   1. 버퍼 설정(송신 데이터 저장 위치)
   2. 패킷 이벤트 번호
   3. 송신 데이터
   4. 패킷에 바인딩하여 보내기
 

 

이것은 클라이언트나 서버에서 패킷을 보낼 때는 순서가 같으며, 순서를 다르게 하더라도 일관된 순서로 구성하는 것이 좋습니다.

 

버퍼는 아래의 함수로 작성, 또는 저장한 버퍼를 읽을 수 있습니다.

 
◎ buffer_write(buffer, type, value) : 버퍼 쓰기
◎ buffer_read(buffer, type) : 버퍼 읽기
 

 

buffer_write로 값을 버퍼에 저장하고, buffer_read로 저장한 값을 읽습니다.

버퍼를 읽을 때는, 버퍼에서 작성한 버퍼 타입, 작성한 순서가 동일 해야 합니다.

아래는 버퍼 쓰기/읽기 공통으로 사용할 수 있는 버퍼 타입입니다.

 
◎ 버퍼 타입
 
buffer_u8 : [1byte] 비부호(unsigned) 정수. 0 ~ 255
buffer_s8 : [1byte] 부호(signed) 정수. -128 ~ 127 (0은 양수)
 
buffer_u16 : [2byte] 16 비트 비부호 정수. 0 ~ 65,535
buffer_s16 : [2byte] 16 비트 부호 정수. -32,768 ~ 32,767 (0은 양수)
 
buffer_u32 : [4byte] 32 비트 비부호 정수. 0 ~ 4,294,967,295
buffer_s32 : [4byte] 32 비트 부호 정수. -2,147,483,648 ~ 2,147,483,647 (0은 양수)
 
buffer_u64 : [8byte] 64 비트 비부호 정수.(미지원)
 
buffer_f16 : [2byte] 16 비트 플로트(float). 실수 +/- 65504 (미지원)
buffer_f32 : [4byte] 32 비트 플로트(float). 실수 +/-16777216
buffer_f64 : [8byte] 64 비트 플로트(float). 
 
buffer_bool : [1byte] true(1) 또는 false(0)
buffer_string : [N/A]문자열
 
buffer_text : 널 종료 문자(\0)를 제외한 문자열. 널 종료 문자(null terminating character)
 

 

버퍼를 저장할 때, buffer_u16으로 저장했으면 버퍼를 읽을 때, buffer_u16으로 엑세스해야 합니다.

버퍼의 데이터를 여러개일 경우, 저장한 순서대로 버퍼 타입을 읽어야 오류가 발생하지 않습니다.

 

예를 들어, 아래와 같은 순서로 작정했다면,


buffer_seek( global.net_buffer, buffer_seek_start, 0 ); //버퍼를 맨 처음부터 기록함
buffer_write( buff, buffer_bool, global.Sound );
buffer_write( buff, buffer_bool, global.Music );
buffer_write( buff, buffer_s16, obj_Player.x );
buffer_write( buff, buffer_s16, obj_Player.y );
buffer_write( buff, buffer_string, global.Player_Name );
 

 

버퍼를 읽을 때, 동일한 버퍼 타입 순서로 읽어야 합니다.


buffer_seek( global.net_buffer, buffer_seek_start, 0 );
global.Sound = buffer_read( buff, buffer_bool );
global.Music = buffer_read( buff, buffer_bool );
obj_Player.x = buffer_read( buff, buffer_s16 );
obj_Player.y = buffer_read( buff, buffer_s16 );
global.Player_Name = buffer_read( buff, buffer_string );
 

 

이것을 응용해서 클라이언트가 서버에 접속했을 때, 서버로 클라이언트의 이름을 보낸다면,

 
obj_client 오브젝트 - Create 이벤트
 
 
var server = network_connect( global.client_socket , "127.0.0.1", 6510 );
if server < 0{
//연결 실패했을 때
instance_destroy( );
show_message( "서버 연결에 실패했습니다." );
room_goto( room_open );
}
else{
//연결 되었을 때
ds_list_add( global.chat_list, "서버에 연결 되었습니다!" );
 
//----- ▼ 클라이언트에서 서버로 메시지 보내기 
 
buffer_seek( global.net_buffer, buffer_seek_start, 0 ); //버퍼를 맨 처음부터 기록함
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호( 숫자 )
buffer_write( global.net_buffer , buffer_string, global.playername ); //플레이어 이름을 보냄( 문자열 )
 
network_send_packet( global.client_socket, global.net_buffer, buffer_tell( global.net_buffer ) ); //패킷 보내기
 
//----- ▲ 클라이언트에서 서버로 메시지 보내기
}
 

 

이와 같이 할 수 있습니다.

먼저 버퍼를 처음부터 기록하도록 설정합니다.

그리고 서버에서 확인할 이벤트 번호, 이벤트에서 처리할 플레이어 이름을 순차적으로 버퍼에 저장합니다.

물론 다른 데이터도 버퍼에 저장할 수 있습니다.

어째든 송신할 데이터가 준비되었다면, 마지막으로 network_send_packet 함수를 사용하여 버퍼를 바인딩해 서버로 보냅니다.

 

이제 보낸 데이터를 처리해봅시다.

클라이언트에서 서버로 패킷을 정상적으로 보내게 되면, 서버에서는 [네트워크 비동기 이벤트(Network Async Event)]가 발동됩니다.

 

(▲ 네트워크 비동기 이벤트)

 

"서버에 접속한 클라이언트가 메시지를 보내왔어여~" 라며 알림을 받는 것과 같습니다.

서버의 [네트워크 비동기 이벤트]에 수신 이벤트는 아래와 같이 구성됩니다.

 

 
obj_server 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
 
//--------- 서버 IP 체크
if n_id = global.local_socket{
var _ip = ds_map_find_value( async_load, "ip" );
global.local_server_IP = _ip;
exit;
}
//--------- 서버 IP 체크
 
 
if n_id == global.server_socket{
//--------- 클라이언트 연결/해제 체크
 
//연결 어쩌고... 해제 저쩌고... 이벤트가 있었으나 길어서 잠시 생략함.
 
//--------- 클라이언트 연결/해제 체크
}
else{
//--------- ▼ 클라이언트 데이터 수신

var sock = ds_map_find_value( async_load, "id" ); //클라이언트 소켓
var t_buffer = ds_map_find_value( async_load, "buffer" ); //버퍼
 
var cmd_type = buffer_read( t_buffer, buffer_u16 ); //수신 패킷 이벤트 번호
 
var pos = ds_list_find_index( global.socketlist, sock ); //저장한 소켓 목록
 
switch ( cmd_type ){
 
case 1:
//수신 이벤트
break;
 
case 2:
//수신 이벤트
break;
 
case 5:
//수신 이벤트
break;
}
//--------- ▲ 클라이언트 데이터 수신
}
 

 

서버에서 클라이언트로 부터 받은 패킷은 아래와 순서대로 처리합니다.

 
◎ 패킷 받기 순서
   1. 수신한 버퍼가 있는지 확인
   2. 수신된 패킷 이벤트 번호
   3. 수신 데이터
 

 

중요한 것은 패킷을 보내는 순서와 받는 순서가 같아야 합니다.

 

클라이언트에서 1번 이벤트 번호로 송신했기 때문에 서버에서 1번 수신 이벤트에서 데이터를 처리하도록 합니다.

 
obj_server 오브젝트 - Async - Networking 이벤트
 
 
case 1:
var _name = buffer_read( t_buffer, buffer_string ); //플레이어 이름
ds_map_set( global.player_map, "player_"+string( sock ), string( _name ) ); //해당 플레이어의 이름을 저장함
 
//서버 챗팅 적용
var t = "["+_name+"]님이 접속하셨습니다.";
ds_list_add( global.chat_list, t );
 
break;
 

 

이 정보를 다른 클라이언트들에게 보내줄 수 있습니다.

 

 
obj_server 오브젝트 - Async - Networking 이벤트
 
 
case 1:
var _name = buffer_read( t_buffer, buffer_string ); //플레이어 이름
ds_map_set( global.player_map, "player_"+string( sock ), string( _name ) ); //해당 플레이어의 이름을 저장함
 
//서버 챗팅 적용
var t = "["+_name+"]님이 접속하셨습니다.";
ds_list_add( global.chat_list, t );
 
//---------- ▼ 클라이언트에게 접속자 정보를 보냄
 
//클라이언트들에게 보낼 접속자 정보( 접속자 소켓 목록 )
if !( ds_map_exists( global.player_map, "player_order" ) ){ ds_map_add_list( global.player_map, "player_order", global.socketlist ); }
else{ ds_map_replace_list( global.player_map, "player_order", global.socketlist ); }
 
var _map = json_encode( global.player_map ); //클라이언트 정보가 저장된 DS 맵을 버퍼로 보내기 위해 json 문자열로 인코딩함.
 
//클라이언트에 패킷을 보내 적용
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호
 
buffer_write( global.net_buffer , buffer_string, global.playername ); //서버 유저 이름
buffer_write( global.net_buffer , buffer_string, t ); //챗
 
buffer_write( global.net_buffer , buffer_string, _map ); //클라이언트 정보
 
for ( var i = 0; i < ds_list_size( global.socketlist ); i+ = 1; ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}
 
//---------- ▲ 클라이언트에게 접속자 정보를 보냄
break;
 

 

서버에서 보낸 정보를 클라이언트에서도 처리해봅시다.

 

서버에서 클라이언트에게 패킷을 보내면 서버와 마찬가지로 클라이언트의 [네트워크 비동기 이벤트]가 발동됩니다.

 
obj_client 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket{
//--------- 서버로 부터 받은 패킷 데이터
var type = ds_map_find_value( async_load, "type" );
 
if ( type == network_type_data ){

//--------- ▼ 수신 이벤트

var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 ); //수신 패킷 이벤트 번호
switch ( cmd_type ){
 
case 1:
//수신 이벤트
break;
 
case 2:
//수신 이벤트
break;
 
case 5:
//수신 이벤트
break;
 
}
//--------- ▲ 수신 이벤트

}
//---------
}
 
 

 

서버에서 1번 이벤트 번호로 송신했기 때문에 클라이언트 1번 수신 이벤트에서 데이터를 처리하도록 합니다.

 
obj_client 오브젝트 - Async - Networking 이벤트
 
 
case 1:
global.server_name = buffer_read( t_buffer, buffer_string );
var t = buffer_read( t_buffer, buffer_string );
 
ds_map_destroy( global.player_map );
global.player_map = json_decode( buffer_read( t_buffer, buffer_string ) ); //접속자들 정보
 
ds_list_add( global.chat_list, t );
 
break;
 
 

 

 

 

 

 

◈ 클라이언트 연결/해제 설정

 

다음 클라이언트가 서버에 접속했을 때와 연결 해제 했을 때, 정리해봅시다.

 
obj_server 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
 
if n_id = global.local_socket{
var _ip = ds_map_find_value( async_load, "ip" );
global.local_server_IP = _ip;
exit;
}
 
if n_id == global.server_socket{
//----------------
var type = ds_map_find_value( async_load, "type" );
 
switch( type ){
case network_type_connect://현재 소켓에 새로운 클라이언트 접속
 
var sock = ds_map_find_value( async_load, "socket" );
ds_list_add( global.socketlist, sock );
 
//--------- ▼ 클라이언트가 처음 접속했을 때 서버에서 유저 ID를 할당함
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 2 ); //패킷 이벤트 번호
 
buffer_write( global.net_buffer , buffer_u16, sock ); //접속자에게 유저 id 할당
buffer_write( global.net_buffer , buffer_string, "환영합니다!" );
network_send_packet( sock, global.net_buffer, buffer_tell( global.net_buffer ) );
 
//--------- ▲ 클라이언트가 처음 접속했을 때 서버에서 유저 ID를 할당함
break;
 
case network_type_disconnect://현재 소켓에 클라이언트 연결 종료
var sock = ds_map_find_value( async_load, "socket" );
 
var t, pos;
pos = ds_list_find_index( global.socketlist, sock );
ds_list_delete( global.socketlist, pos ); //클라이언트 소켓
 
//--------- ▼ 연결 해제된 클라이언트의 정보를 다른 클라이언트들에게 송신함
t = "["+string( ds_map_find_value( global.player_map, "player_"+string( sock ) ) )+"]님이 나갔습니다.";
ds_list_add( global.chat_list, t );
 
if ds_map_exists( global.player_map, "player_"+string( sock ) )
{ ds_map_delete( global.player_map, "player_"+string( sock ) ); }
 
//클라이언트들에게 보낼 접속자 정보
var _map = json_encode( global.player_map );
 
buffer_seek( global.net_buffer, buffer_seek_start, 0 );
buffer_write( global.net_buffer , buffer_u16, 1 ); //패킷 이벤트 번호
 
buffer_write( global.net_buffer , buffer_string, global.playername ); //서버 관리자 닉네임
buffer_write( global.net_buffer , buffer_string, t ); //챗
buffer_write( global.net_buffer , buffer_string, _map ); //클라이언트 정보
 
for ( var i = 0; i < ds_list_size( global.socketlist ); i+ = 1; ){
network_send_packet( ds_list_find_value( global.socketlist, i ), global.net_buffer, buffer_tell( global.net_buffer ) );
}
//--------- 연결 해제된 클라이언트의 정보를 다른 클라이언트들에게 송신함
break;
}
//----------------
}
 

 

클라이언트가 서버에 접속했을 때 먼저 유저 ID를 할당해줍니다.

연결 해제 했을 때는 해당 클라이언트의 정보를 삭제하고, 수정된 정보를 다른 클라이언트에게 통보해줍니다.

 

연결 해제에서 보내는 정보는 이전에 작성한 클라이언트 1번 수신 이벤트로 보내는 정보와 동일 합니다.

다른 점이 있다면 연결 해제된 클라이언트 정보를 삭제한 갱신된 내용을 보내는 점입니다.

 

서버에 클라이언트 처음 접속했을 때는 메시지를 클라이언트 2번 수신 이벤트로 보냈기 때문에,

클라이언트 [네트워크 비동기 이벤트]의 2번 수신 이벤트에서 패킷을 처리합니다.

 

 
obj_client 오브젝트 - Async - Networking 이벤트
 
 
var n_id = ds_map_find_value( async_load, "id" );
if n_id == global.client_socket{
//서버로 부터 받은 패킷 데이터
// == == == == == == == == == == == == == ==
var type = ds_map_find_value( async_load, "type" );
 
if ( type == network_type_data ){
 
 
//데이터 핸들링
// == == == == == == == == == == == == == ==
var t_buffer = ds_map_find_value( async_load, "buffer" );
var cmd_type = buffer_read( t_buffer, buffer_u16 );
 
switch ( cmd_type ){
 
//--------- 클라이언트 세부 정보 갱신
case 1:
global.server_name = buffer_read( t_buffer, buffer_string ); //서버 유저 이름
var t = buffer_read( t_buffer, buffer_string );
 
ds_map_destroy( global.player_map );
global.player_map = json_decode( buffer_read( t_buffer, buffer_string ) ); //접속자들 정보
 
ds_list_add( global.chat_list, t );
 
break;
//--------- 클라이언트 세부 정보 갱신
 
//--------- ▼ 서버에 처음 접속했을 때
case 2:
var _id = buffer_read( t_buffer, buffer_u16 ); //할당받은 유저 ID( 클라이언트 )
var t = buffer_read( t_buffer, buffer_string ); //채팅 정보
 
global.user_id = _id; //현재 클라이언트 유저 ID
 
ds_list_add( global.chat_list, t ); //채팅목록
break;
//---------▲ 서버에 처음 접속했을 때
 
 
case 5:
//수신 이벤트
break;
 
}
// == == == == == == == == == == == == == ==
}
// == == == == == == == == == == == == == ==
}
 
 

 

여기까지 클라이언트가 접속했는지, 또는 나갔는지 확인이 가능할 거에요.

메시지를 잘 전달하는지 테스트해봅시다.

 

 

 

 

 

 

 

 

300x250

댓글