Go Version : 1.21.5 2024 컴퓨터공학과 캡스톤디자인 - Next Reality

이전 내용 : 2024-04-24-로그 데이터를 이용한 동기화 기획

UDP 테스트용 서버 제작

package main
 
import (
	"fmt"
	"net"
)
 
func main() {
	// UDP 서버를 Listen할 주소 설정
	address, err := net.ResolveUDPAddr("udp", ":8080")
	if err != nil {
		fmt.Println("Error resolving address:", err)
		return
	}
 
	// UDP 연결 생성
	connection, err := net.ListenUDP("udp", address)
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	defer connection.Close()
 
	fmt.Println("UDP server is listening on", address)
 
	// 무한 루프를 통해 클라이언트로부터 메시지를 받음
	buffer := make([]byte, 1024)
	for {
		n, clientAddress, err := connection.ReadFromUDP(buffer)
		if err != nil {
			fmt.Println("Error reading:", err)
			continue
		}
 
		// 받은 메시지 출력
		fmt.Println("Received message from", clientAddress, ":", string(buffer[:n]))
 
		// 클라이언트에게 다시 메시지를 보냄
		_, err = connection.WriteToUDP([]byte("Message received"), clientAddress)
		if err != nil {
			fmt.Println("Error replying:", err)
			continue
		}
	}
}
 

접속 플레이어 관리

접속한 플레이어를 확인하기 위해, 플레이어의 주소(ip:port)와 플레이어 아이디를 키-값으로 짝지어서 저장함

// net address <-> user 용이하게 하기 위해 양방향으로 만듬
var AddrUser map[*net.UDPAddr]string = make(map[*net.UDPAddr]string)
var UserAddr map[string]*net.UDPAddr = make(map[string]*net.UDPAddr)
 
var UserMapid map[string]string = make(map[string]string) // 유저가 들어가있는 방 번호
var MapidUserList map[string][]string = make(map[string][]string) // 방 안에 있는 플레이어 리스트

스키마 수신 및 파싱

스키마(로그 데이터)에 대한 기획 (2024-04-24-로그 데이터를 이용한 동기화 기획)에 맞게 구현하기 위해, 우선 받아온 데이터에 대해 파싱하는 작업을 거치게 했음

package controller
 
import (
	"strings"
)
 
// 모든 receiveMessage 는 commandName, senduserId, sendTime을 필수적으로 가지며,
// commandName에 따라 otherMessage의 길이가 달라질 수 있음.
// 따라서, 우선적으로 otherMessage의 길이를 검사함.
type ReceiveMessage struct {
	CommandName  string
	SendUserId   string
	SendTime     string
	OtherMessage []string
}
 
func MessageParser(msg string) ReceiveMessage {
	// fmt.Println("MessageParser messages : ", messages)
 
	splitCommand := strings.Split(msg, "$")
	parsingMessage := strings.Split(splitCommand[1], ";")
 
	var _otherMessage []string = nil
 
	if len(parsingMessage) > 2 {
		_otherMessage = parsingMessage[2:]
	}
 
	recvMsg := ReceiveMessage{
		CommandName:  splitCommand[0],
		SendUserId:   parsingMessage[0],
		SendTime:     parsingMessage[1],
		OtherMessage: _otherMessage,
	}
 
	return recvMsg
}

스키마 리스너 제작

스키마에서 주요 동작의 종류를 확인할 수 있는 부분은, 달러 표시($) 이전에 나타나는 커맨드 부분임. 리스너는 하나의 커맨드 종류 당 하나씩 존재하도록 했고

// Listener는 ReceiveMessage와 연결 정보를 받고
// 서버에 유효한 결과를 도출했는지 bool로 반환함
type listeners func(ReceiveMessage, *net.UDPAddr) bool

리스너라는 사용자 지정 타입을 만들고, String > listeners 관계가 되는 Map을 만듬

var ListenerMap = map[string]listeners{
	"PlayerJoin":  PlayerJoin,
	"PlayerLeave": PlayerLeave,
	"PlayerMove":  PlayerMove,
}

커맨드에 따른 추가 메시지 검사

스키마는 커맨드의 종류에 따라 뒤에 붙는 메시지의 개수가 달라짐. 이를 검사하는 함수를 만들어 리스너 실행 초반에 검사하도록 함

func otherMessageLength(commandName string) int {
	switch commandName {
	case "PlayerJoin", "PlayerMove":
		return 2
	case "PlayerLeave":
		return 0
	}
	return 0
}

PlayerJoin

플레이어가 방에 입장했을 때 서버로 송신하는 스키마다. 따라서, 서버에서는 해당 플레이어의 아이디와 주소를 접속자 리스트에 추가하는 작업이 필요함

func PlayerJoin(m ReceiveMessage, addr *net.UDPAddr) bool {
	// PlayerJoin 메시지 형태 :
	// PlayerJoin$sendUserId;sendTime;sendUserNickname;MapId
	// otherMessage length: 2
	if len(m.OtherMessage) == otherMessageLength(m.CommandName) { 
		mapid := m.OtherMessage[1] 
 
		if _, exist := UserAddr[m.SendUserId]; !exist { 
			UserAddr[m.SendUserId] = addr
			AddrUser[addr] = m.SendUserId
			UserMapid[m.SendUserId] = mapid
			MapidUserList[mapid] = append(MapidUserList[mapid], m.SendUserId)
			fmt.Println(
				aurora.Sprintf(
					aurora.Green("Success : User [%s] joined Map [%s]\n"), m.SendUserId, mapid))
			return true
		} else {
			fmt.Println(
				aurora.Sprintf(
					aurora.Yellow("Error : User [%s] is already in this game.\n"), m.SendUserId))
 
			return false
		}
	} else {
		fmt.Println(
			aurora.Sprintf(
				aurora.Yellow("Error : PlayerJoin Message Length Error : need 2, received %d\n"), len(m.OtherMessage)))
		return false
	}
}

접속 플레이어 키-값 쌍에 플레이어가 없으면 접속 허용

PlayerLeave

func PlayerLeave(m ReceiveMessage, addr *net.UDPAddr) bool {
	// PlayerLeave 형태 :
	// PlayerLeave$sendUserId;sendTime
	// otherMessage length : 0
	if len(m.OtherMessage) == otherMessageLength(m.CommandName) {
		if _, exist := UserAddr[m.SendUserId]; exist {
 
		} else {
			fmt.Println(
				aurora.Sprintf(
					aurora.Yellow("Error : Cannot Found User [%s]"), m.SendUserId))
			return false
		}
	}
 
	return false
}

접속 플레이어 키-값 쌍에 플레이어가 있으면 퇴장 허용

PlayerJoin

// PlayerMove 형태 :
// PlayerMove$sendUserId;sendTime;position;rotation
// otherMessage length : 2
func PlayerMove(m ReceiveMessage, addr *net.UDPAddr) bool {
	return false
}

PlayerJoin Listener는 일단 만들어만 둠

현재까지의 서버 동작 과정

  1. 클라이언트가 전송한 String 스키마를 수신한다.
  2. String 스키마를 MessageParser()를 사용해 ReceiveMessage 형태로 파싱한다.
  3. ReceiveMessage 스키마의 CommandName에 맞는 리스너를 실행한다.