REST ( Representational State Trasnfer) 공식표준 스펙은 아닌데
원래 있던 HTTP 통신에서 재사용 하여 데이터 송수신 규칙을 만든것 => CRUD
CRUD 를 Blazor API 서버를 활용하여 작업 한것
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedData.Models;
using System.Collections.Generic;
using System.Linq;
using WebAPI.Data;
namespace WebAPI.Controllers
{
//www.google.com 이건 전체 주소
//www.google.com/api/ranking 이건 세부 라우팅 주소로 전체 주소에서 구분지어 들어갈 때 사용됨
//그 다음 어떤걸 하는지 지정 할 수 있다 GET, POST, PUT...)
//Create : POST 방식 api/ranking => body 에 실제 정보를 담는다 , 아이템 생성 요청
//Read 시에는 보통 Get 을 사용한다
//GET : api/ranking => 전체 아이템을 가져온다 , api/ranking/1 id가 1인 아이템을 가져온다
//get /api/ranking 모든 id를 주세요
//get /api/ranking/1 id 중에서 1전을 주세요 라는 요청
//get 은 body 에 정보를 넣지 않고 이 요청 하나로 처리 된다
//update : put 으로 사용됨
//PUT : api/ranking (put은 보안 무제로 일반적인 웹에서 사용되지 않는다)
//put 은 body 에 정보를 넣어서 요청을 보낸다
//delete /api/ranking/1 이렇게 삭제 할 수 있는데 보안 문제로 웹에서 쓰지 않는다
//id=1 번인 아이템 삭제 요청
//api controller 는 c# 객체를 반환하는 것이 가능하다
//null 반환하면 클라에서 204 Response (No Content) 를 받는다
//String을 반호나하면 => text/plain 타입으로 반환한다
//나머지는 json 형태로 반환한다
//localhost:portnumber/api/ranking 이 요청이라는 얘기인데 RankingController 이것이 [controller] 부분에서 ranking 으로 변환됨
[Route("api/[controller]")]
[ApiController]
public class RankingController : ControllerBase
{
ApplicationDbContext _context;
public RankingController(ApplicationDbContext context)
{
_context = context;
}
//create, body 에 보낼때는 [frombody 를 넣어주면 된다]
[HttpPost]
public GameResult AddGameResult([FromBody] GameResult gameResult)
{
_context.GameResultList.Add(gameResult);
_context.SaveChanges();
return gameResult;
}
//update
[HttpPut]
public bool UpdateGameResult([FromBody] GameResult gameResult)
{
var findResult = _context.GameResultList.Where(item => item.Id == gameResult.Id).FirstOrDefault();
if(findResult == null)
{
return false;
}
findResult.UserName = gameResult.UserName;
findResult.Score = gameResult.Score;
_context.SaveChanges();
return true;
}
//read
//ranking
[HttpGet]
public List<GameResult> GetGameResults()
{
List<GameResult> results = _context.GameResultList.OrderByDescending(item=> item.Score).ToList();
return results;
}
//ranking/1
[HttpGet("{id}")]
public GameResult GetGameResult(int id)
{
GameResult result = _context.GameResultList.Where(item => item.Id == id).FirstOrDefault();
return result;
}
//delete
[HttpDelete("{id}")]
public bool DeleteGameResult(int id)
{
var findResult = _context.GameResultList.Where(item => item.Id == id).FirstOrDefault();
if (findResult == null)
{
return false;
}
_context.GameResultList.Remove(findResult);
_context.SaveChanges();
return true;
}
}
}
와이파이에서는 100ms 에서 불안정하면 200~300ms 까지도 불안정하게 발생하게 된다
보통 게임에서 30~60 프레임 기준을 많이 사용하는데
60프레임 기준에선 한프레임당 16ms 정도 의 시간이 걸린다
이 시간을 넘어가면 사람들이 인지는 하기 시작하고
30프레임에선 한 프레임당 33 ms 인데 33ms 가 넘어가면 거의 모든 사람들이 부자연 스럽다고 인지하게 된다
[예측과 보정]
가지고 있는 정보들을 통해서 미리 예측을 하고 정보를 받았을때 예측과 결과가 다르다면 보정을 해주는 방식
ex ) 타격이 먼저 되고 때리는 모션이 나중에 나가는 경우
논타겟의 경우 시점 분리가 된다 : 스킬 시전 시점과 스킬 피격 대상 선정 사이에 차이가 발생
만약 스킬 영역에 스킬을 시작하고 스킬이 발생 하는데 까지 시간이 걸리는데 영역안에 있던 대상이 밖으로 도망가면 피격이 되면 안된다, 이때는 피격 당하는 대상의 시점만 미뤄두고 나중에 데미지가 가해지게 하면 되지만 시점분리에 필요한 요구사항이 점점 늘어난다면?
ex) 스킬 대상이 넉백 되면서 데미지는 그 도중에 되면 좋겠다
: 1. 시작은 따로 2. 그다음에 넉백. 3 그다음에 데미지 이렇게 3개로 분리가 된다
영역은 하나인데 대상을 3번에 걸처서 타격해야 하는 상황이 발생한다
더 나아가 쿨 다운은 선딜 후 타격 전에 돌리고 싶다
내가 스킬 시전하다가 넉백을 당하면 취소가 되야하고
기를 모으는 시간에 따라 효과가 다르게 하고 싶다
돌진하면서 주변에 있는 적들을 차례로 밀치고 싶다
등등등.. 요구 사항이 점점 더 많아짐..
이때 생각해 볼 수 있는거 스킬 스테이지 방식이다
스킬을 여러 단계로 나누어서 구성 하는 것을 말한다
그런데 문제는 결정된 시간이 전달이 될때 받는 쪽에서 지연이 되면 내가 원하는 타이밍에 전달되지 않게 된다
아래처럼 동작하겠지 라고 생각하고 만들었는데
네트워크를 타는 순간 스킬 구성이 달라지게 된다
이 처럼 시간 간격에 딜레이가 생기면서 타격이 늦게 일어나거나 여러 가지 시점이 흔들리는 일들이 벌어지게 된다
그래서 나온게 예측 가능하게 만들자
여러 스테이지 결과를 미리 결정 및 공유하고
이에 맞춰 각자 서버와 클라가 스킬 스테이지를 진행한다
그래서 이때 미리 계산 할 수 있는 그룹들을 미리 묶어 두어서 처리하게 된다
그것이 스킬 스테이지 플로우 도입
CapturingTarget 영역 안의 대상 선정
이나 중요하게 반드시 시점이 나뉘어야 하는 것들만 플로우를 나누는 기준이 되고 나머지 스테이지들은 묶어서 미리 계산을 하도록 처리
이걸
이렇게 묶음으로 써
원래 4번 전송 되던걸 두번만 전송하게 되고 묶음 안에서는 어색함 없이진행 가능
문제점 : 플로우가 길다보면 발생할 수 있는문제가 중간에 취소 하면 취소가 늦게 가는 경우
피격을 하지 않았는데 피격 연출이 나온다던가 하는 문제
해결 방안 => 정상 타격이 완료 되면 데미지 숫자를 붉은색으로, 그렇지 않으면 파란색으로 표시하여 대미지가 안들어간것으로 보이게끔 처리
또는 스킬 연출과 별개의 추가 연출 구간을 만들어서
정상타격 완료 시 : 대미지 플로터로 확정 연출
정상타격 취소로 판정시(늦게 패킷이 오면) : 대미지 플로터를 모래 연출처럼 흩어 버리기
방식으로 처리
논 타겟일때 움직이면 맞고 가만히 있으면 맞을때도 있고 안맞을때도 있는데
=> 타격자 시점에서는 상대 캐릭터가 스킬 범위에 들어갔는데 피격자에서는 안맞는 문제가 보임 즉 내 캐릭터가 공격을 하는데 상대 캐릭터가 스킬 범위에 들어왔음에도 불구하고 공격이 가해지지 않음
=> 피격자 시점에서는 타격자가 공격 당하는 상대로 스킬을 논타겟으로 썼지만 상대가 스킬 범위에 들어가 있지 않아서 공격을 받지 않은 상황으로 보이는 중
이건 플레이어 위치가 타격자와 피격자 시점에서 위치가 살짝 다를때 발생 할 수 있다
서버상에서도 피격자의 위치가 실제 달라서 발생
타격자 시점에서 상대방 위치를 보면 조금 더 왼쪽으로 가 있는 상태
이때 이동 시스템이 어떻게 되어 있는지 알 필요가 있다
폴리드 타일기반으로 이동하는 시스템
셀도 없고 방향도 특정되지 않고 자율롭게 돌아다닐 수 있는 구조
위치를 짧은 시간내에 동기화 할 수 있는 모델은 없었다는 것 MMORPG 라
대규모 전투에서 통신량이 큼으로 유저가 많을 수록 제곱에 비례해서 늘어나기 때문에..
[이동에 관한 히스토리]
클라가 서버에게 이동 할래 라는 패킷을 날리면 서버가 판단해서 이동을 시키는 구조
결국 클라이언트에서 먼저 이동하도록 변경
즉시 예상하지 못하는 상황에서 멈춤을 할때 를 생각해 보면 다른 유저, 서버에서의 해당 유저, 내클라이언트에서 다른 유저를 멈출때의 위치가 모두 달라지게 된다
즉 피어들간의 위치 정보의 오차가 커진다
멈추는 클라이언트에서는 빨간 위치에서 멈추라고 했지만 서버와 다른 클라에서 모두 다 조금씩 늦게 받게 되어 느려지는 현상이 발생 할 수 있다
순간적으로 변경한 명령이 다음 이동 이다 라고 하면
이동의 시작 구간에서 보정을 진행하면 된다 이때는 이동을 하는 와중에 스킬을 잘 맞고 위치 오차도 적은 편이다
하지만 바로 멈추라고 하면 다음 이동이 없다, 즉 멈추라고 하면 다음 이동이 없어 보정할 곳이 없다
=> 만약 원래 멈춰야 하는 곳보다 더 갔을때 캐릭터를 뒤로 당기면 이동이 어색해 지기 때문에 그 자리에 있을 수 밖에 없도록 프라시아에서는 작업을 했었고 다음 이동 명령때 다시 보정을 한다를 컨셉이였었다 => 그러다보니 생각하는 위치들이 다 달랐던 것
해결 : 구간이 없어도 강제로 맞춰줘야 되는 부분이기 때문에 기본적인 해결책 부터 적용 시작
방안 :
멈추면 그 자리로 그냥 옮기자
핑이 안정적일때는 40ms 에서 120ms 까지는 그래도 안정적으로 추가 이동명령으로 위치 보정을 해도 어색하지 않고 추가로 멈춰야 할 위치까지 이동해서 보정이 가능하다
그런데 이미 이동을 멈춰야 하는 위치보다 더 가 있을때 이걸 당기는건 게임을 망치기 때문에
그 자리에 멈출 수 밖에 없다 즉 50% 확률이 보정이 되거나 안되거나 하는 상황
정확(무보정)의 수치를 200ms 뒤 까지에서 실행되게 해야하는 것이 목표인데
즉 대부분이 보정이 될 수 있게 처리를 해야 하는데
이렇게 처리 하기 위해선 내쪽에서
다른 개체의 이동 요청이 왔을때 120에 받았는데 마치 200에 받은 것 처럼 처리해 주면 된다
즉 다른 개체의 이동 정보는 한 80ms 늦게 실행 시킨다
이렇게 하면 보정을 더 과하게 해야하긴 하지만 아예 보정을 할 수 없는 케이스는 거의 사라지게 된다
위의 문제는 이동에 관한 갑자기 멈춤에 관한 문제였음으로 상대 캐릭터 이동에 대한걸 80ms 정도 지연시켜 받아서 이동에 대한 보정 처리를 해주면 결과적으로 200ms 내에 이동 보정이 됨으로 타격은 정확한 처리가 된다 => 문제는 해결됐다 수치상 80ms 이긴 한데 상황에 따라 다를 수도 있으니 동적으로 다르게 주는 것도 생각해 볼만한 문제
그런데 이동하는 중에는 오차가 좀 더 커진 셈이긴 하지만 캐릭터가 스킬을 안맞는 크리티컬한걸 피했다고 볼 수 있다
정리
예측을 활용 하는 동기화 방식은 어느정도 활용 중
하지만 모든 정보를 알리는 방식으로 동기화를 할 경우 과다한 네트워크 통신, 느린 반응성등의 문제가 발생 할 수 있다
시간의 흐름에 따른 상태를 계산해 낼 수 있는 일부 상태만 알림을 통해 동기화 처리 한다 => 정해진 규칙에 따라 현재 상태를 도출한다
네트워크를 통하면 외부 요인으로 인해 상태가 변경 되었을 때 이 정보를 내가 늦게 알게 되는 경우가 자주 발생한다,
내가 계산해서 예측을 해서 가지고 있는 현재 상태랑 믿을 수 있는 소스 보틍은 서버와 상태가 서로 다를 경우 현재 내가 가지고 있는 상태를 실제 상태로 자연스럽게 맞추는 작업을 보정이라고 한다
만약 더이상 자주 알려주기 힘들다면 고려해볼만 한 것은
이른 시점에 결정적인 상태를 만든다, 먼저 계산해서 알려준다 는것
도출해낸 결과가 정확한게 가끔 많이 틀릴 수 있다면 엄청 많이 틀리는 것을 좀 완화하고 대신에 평소에는 조금 정확성이 떨어지더라도
워스트 케이스를 없애는 게 더 좋을 수 있다
다만 정확성을 너무 포기하면 보정이 힘들어진다
구간이 없다면 80ms 정도 늦게 받아서 처리 하는 것처럼 구간을 만들어서 구간을 활용하는 전략들을 고민하면 좋을 것
예측에는 Extrapolation 외삽법이 주로 쓰이고 보간에는 interpolation 이 주로 쓰인다
--------------------
별도 추가 부분
--------------------
외삽법 : 외삽법이 뭔지 오래 되서 잘 기억이 안날 수 있음으로 복습차원에서..
검은색이 원래 데이터이고 파란색이 예측하는데이터인데 점3개에 대해서 보간을 하면 원래 검은색과 거의 유사하게 파란색이 나오는데 그 미래에 대한 4번째 점에 대해선 오차가 심해 질 수 있다는 외삽법의 단점이 있다
고차다항식의 보간법의 위험성 : 항이 많아지고 차수가 높아 질 수록 식이 정교해 질것이라는 기대가 있지만 오히려 않좋은 결과가 나올 수도 있다
점선이 정확한 값인데 점 4개를 갖고 4차 다항식을 만들 수 있고 그림을 그리면 검은 선이 나오는데 형태는 조금 유사해도 오차가 나온다
이때 더 많은 점을 도입해서 10차 다항식으로 접합을 해보면
runge 함수를 다항식으로 묘사를 해봤더니 양 끝단에 오차가 상당히 크게 나온걸 알 수 있다
즉 다항식 보간법을 쓸때 고치일 수록 정확하다고 볼 수 없다는 것이 Runge 라는 사람이 만든 Runge 함수이다, 오히려 낮은 차수 일때 더 유사할때가 있다는 것
이글의 원문 :
● 발표분야: 프로그래밍 ● 발표자: 넥슨코리아 정성훈 / Nexon Korea Sunghoon Jung ● 권장 대상: 프로그래머, 시스템 디자이너 ● 키워드: #네트워크 #동기화 #연출 #프라시아전기
이때 서버는 타기에 필요한 검사를 함 캐릭터의 이동 방향을 보고 탈것이 캐릭터를 태우러 갈 수 있는지 확인 필요 이때 네비게이션 메시 상에서도 유효한 경로인지 같이 검사를 한다
부르기 패킷(만날위치포함) : 준비가 끝나면 문제가 없다 판단 되면,
모든 클라에게 탈것에 대한 만날 위치포함하여 부르기 알람 메세지를 보낸다 그리고 탈것이 어디로 갈지에 대한 정보도 포함한다
부르기에 필요한 시간을 기다렸다가 랑데부 알람 메세지를 클라에게 보내기 위해 서버에서 예약한다
랑데부 패킷 : (랑데부는 만남, 만나는 장소라는 뜻)
클라에서 랑데부를 받으면 클라에서 타기지점으로 뛰기 시작한다
탈것을 스폰하고 캐릭터를 향해 달리게 한다 캐릭터는 이 지점을 향해 달려 가는 연출을 한다 이때 캐릭터는 탈것을 부르면서도 계속 달리는 중임으로 부르기 상태에서부터 다른 이동의 간섭을 막고 탈곳과 만나기 위한 곳으로 달려 나간다 이때 캐릭터와 탈것은 계속 달리는 중이다
오르기요청 : 클라가 캐릭터가 탈 준비가 완료 되면 서버에 알려주는 방식 클라에서 서버로 알리는데 => 캐릭터와 탈것은 계속 달리는 중이기 때문에 서버와 메시지를 주고 받는 동안에도 위치가 계속 바뀌고 있는 중임으로 이때 오차가 발생 할 수 있는데 이를 줄이기 위해
캐릭터와 탈것의 위치를 서버로 같이 전달한다
서버에서는 탈것의 위치가 탈것에 올라타는 캐릭터의 새 위치가 되기 때문에 이 둘의 범위가 약속한 범위 안쪽으로 들어오면 모든 클라에게 오르는중패킷을 보낸다
나 자신은 이미 이전에 오르는 연출을 시작 하기 때문에 오르기요청에 대한 응답 패킷은 무시하지만 다른 클라는 오르는중 패킷을 받으면 오르는 연출을 시작한다
오르기 완료 패킷 : 오르는 중이 완료 되는 시간이 정해져 있고 이 시간이 완료 되면 상태와 오르기완료패킷을 클라들에게 보내어 오르기를 완료한다,
위 이미지인데 타는 경로를 아래 처럼 생각 해볼 수 있다
캐릭터 오르는 연출
랑데부 에서
캐릭터와 탈것이 각각 어느 위치에 있을때 오르기가 시작되는지를 결정해야한다 캐릭터가 오르기 시작 애니가 나가고 -> 탈것 기준에서 오르기 시작한 지점과 오르기 완료된 지점까지 완료 되면 이때 약간 더 앞으로 전진하면 원래 캐릭터가 이동하던 직직방향과 만나게 된다, 그리고 오르기 완료시까지 플레이어 입력을 일시적으로 제한한다 일시적으로 제한되는 사이에 연출이 일어나기 때문에 이때의 시간은 기획자가 알아서 제어하도록 테이블로 빼놓음, 말이나 탈것에 따라 연출이 달라질 수 있기 때문 그리고 타는 동안 속력은 기존 그대로 고정한다
오르기 완료
캐릭터 액터를 탈것 액터에 부착하는 방식
캐릭터와 탈것의 위치와 방향은 다르지만 일단 붙여놓고 애니메이션을 재생 하면서 일정시간동안 위치와 방향의 차이가 0이 되게끔 한다
이 시간은 오르기 애니메이션 시간 중에서 캐릭터가 탈것에 닿을 때까지의 시간 , 이때 위치와 방향은 형태와 상관 없이 선형적으로 처리
이때 카메라는 캐릭터 위치를 따라가게 한다음 타기가 완료되면 원래 카메라를 제어하던 곳으로 반환 시켜준다
모바일에서 인터넷이 일시적으로 안되는 상황을 대비해 현재 탈것 상태를 다시 알려주는 처리를 예방 , 클라는 서버에서 주는 상태대로 클라이언트 상황을 맞춰 적용해줘야함 => 현재 위치에서 다음 목표 지점이 다이렉트로 나오는 방식
길이 좁거나 막혀 있다면 탈것이 올 수가 없으니 이때는 제자리에서 바로 탈것을 타도록 한다
캐릭터가담당 서버가 바뀌는 지역으로 이동하는 경우
타려는 와중에 서버 이주가 진행 된다면 서버에서 바로 오르기 완료 상태로 바꾸고 클라이언트에 알려주는 상태로 처리 => 임의의 상태로 바로 전환되게 처리 하는 것이 맞아 떨어짐
클라위치와 탈것이 만나지 못하는 거리로 탈것이 지나간다면 그때는 타지 못하고 지나가고 지나간 이후에 제자리에서 바로 탄 상태가 되도록 처리
내리기 처리 : 빠르게 플레이로 돌아갈 수 있는데 집중
오르기완료 상태라면 내리기 처리를 시작
내리는 중, 내리기 완료 패킷을 서버에서 순차적으로 보내준다
내리기과정은 특별히 진행 될게 없고 서버에서 보내주는대로 처리하기면 하면 됨
내릴때 현재 바라보고 있는 방향으로 달려 나가면서 내리도록 처리하고 위치도 탈것 상태를 전달하면서 같이 모든 클라에게 전달하여 모든 클라이언트에서 같은 곳이 될 수 있도록 처리
내릴때는 탈때와의 반대 과정으로 처리 : 캐릭터와 탈것은 때어내면서 캐릭터 위치는 고정하고 이쪽으로 캐릭터가 이동 되게 하면서 애니메이션 재생
내리는 중 패킷을 클라가 받은 것이라면 내리기 완료 패킷이 오는것은 확정적이라서 먼저 내려서 걸어가고 조작 될 수있게 처리 하면 자연스러워짐 예를 들어 말에서 바로 내리는 순간 공격이 가능하다
곡선이동 자동이동 길잡이
곡선이동 :
캐릭터의 이동은 기본적으로 직선이동이다, 이동 회전이 있긴 한데 이동 경로가 딱딱하다 , 이동회전은 느낌이 딱딱하다
그래서 먼거리를 갈때는 먼저 큰 길을 찾아가고 목적지에 가까운 곳까지 큰길을 통해 이동하는 길 찾기 방식을 사용, 이러면 계속해서 큰길들의 분기점을 지나게 된다
이 분기점을 지날때 급격히 방향이 꺽이는 경우들이 많다
탈것을 타고 빠르게 지나갈때는 문제가 더 잘보이는데 이때 곡선 형태로 이동 할 수 있는 것을 추가, 이 지역은 내비메쉬가 깔린 지역이다
이런 점들에선 곡선형태로 분이점을 지나도록 처리
베지어 곡선 사용 2차 곡선이 항상 중심을 향해 들어오고 중심에서 가도록 하기위해 A1, B1 을 잡는데 각 직선에서 ¼ 지점정도에 만든다 중간 지점을 만들기 위해 B0 지점을 임의로 만든다
이때 샘플값이 균일한 곡선이 나오진 않는데
이것은 시간이 아닌 이동거리를 통해 곡선의 이동 거리를 찾으면 됨
2차 베지어 곡선은 특정 구간의 거리를 계산식 하나로 구할 수 있는 특성이 있다(이것이 다른 곡선과 달리 거리에 대한 이점이 있는 곡선이라 이걸 선택함)
거리를 통해 위치를 찾을때는 정확한 지점을 찾긴 어려우니 근사 값으로 찾음(뉴턴랩스방법) : 뉴턴법은 식조작으로 풀지못하는 식의해의 근사 값을 구하는 법으로 접선으로 구한다
멀리서부터 접선을 그리다 보면 빨간점의 해와 접접 가까워지면서 해를 구하는 것
뉴턴방법 = 뉴턴-랩슨 방법 이라한다 x0 에 해당하는 fx 의 접선을 구하고 이 접선과 만나는 x 축의 점 x1 에 대해 다시 기울기를 구해 이것을 반복하는 방식 으로 근사 해를 구하는 방식중 가장 빠르다
이것을 이용해서 거리기반의 위치를 구한다 spline component는 에르미트 곡선을 사용한다 에르미트 곡선은 거리에 따른 위치 값을 바로 얻어 올 수 있어서 이걸 쓰면 됨
자동길잡이 : 캐릭터의 이동 경로를 미리 보여주는 기능
자동이동이 시작 되는 지점에서 이동해야 하는 경로가 대부분 결정되기 때문에 이 경로를 미리 보여주는 것 이건 나한테만 보여지는 것으로 splinecomponent 를 그대로 이용해서 표현이 가능하다
이글의 원문 :
프라시아 전기에 멋진 탈것 만들기 멋지게 타고, 멋지게 달리고, 멋지게 내리자
● 발표분야: 프로그래밍 ● 발표자: 넥슨코리아 이연석 / Nexon Korea Yeonseok Yi ● 권장 대상: 게임 프로그래머, 클라이언트 프로그래머, UE4 프로그래머
command 패턴으로 서버에서 처리할 명령을 바로 처리하지 않고 우선 처리할 명령을 Queue 에 먾어 넣어 모아두었다가
이 방식은 서버가 처리 할수 있을때 처리 하는 방식으로 직접적으로 명령을 다수의 스레드가 한번에 처리 할때 lock 을 걸어야 하는 상황에서의 부하를 줄일 수 있다
설명
이전까지는 클라이언트 세션을 연결되자마자 바로 GameRoom의 List에 넣어주고 있었지만, 이번에는 JobQueue 개념을 이용해 Queue에 일단 GameRoom의 List에 넣어주는 작업을 '예약'해주고 있다.
코드
ServerCore
JobQueue.cs
public interface IJobQueue
{
void Push(Action job);
}
public class JobQueue : IJobQueue
{
Queue<Action> _jobQueue = new Queue<Action>();
object _lock = new object();
bool _flush = false; // 큐에 쌓인걸 '자신이' 실행할건지. 누군가 하고 있으면 자신은 하지 않는다.
public void Push(Action job)
{
bool flush = false;
lock(_lock)
{
_jobQueue.Enqueue(job);
if (_flush == false)
flush = _flush = true;
}
if (flush)
Flush();
}
void Flush()
{
while(true)
{
// 하나씩 꺼내는 와중에도 다른 애가 Push해서 JobQueue에다 넣을 수 있기 때문에 Pop을 할 때 lock을 잡아줘야 한다.
Action action = Pop();
if (action == null)
return;
action.Invoke();
}
}
Action Pop()
{
lock(_lock)
{
if(_jobQueue.Count == 0)
{
_flush = false;
return null;
}
return _jobQueue.Dequeue();
}
}
}
public class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 10)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(backlog);
for (int i = 0; i < register; i++)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
}
...
Server
GameRoom.cs
class GameRoom : IJobQueue
{
List<ClientSession> _sessions = new List<ClientSession>();
JobQueue _jobQueue = new JobQueue();
public void Push(Action job)
{
_jobQueue.Push(job);
}
public void Broadcast(ClientSession session, string chat)
{
// 이 부분은 다른 쓰레드와 공유하고 있지 않음
S_Chat packet = new S_Chat();
packet.playerId = session.SessionId;
packet.chat = $"{chat} 나는 {packet.playerId}";
ArraySegment<byte> segment = packet.Write();
foreach (ClientSession s in _sessions)
s.Send(segment);
}
public void Enter(ClientSession session)
{
_sessions.Add(session);
session.Room = this;
}
public void Leave(ClientSession session)
{
_sessions.Remove(session);
}
}
ClientSession.cs
class ClientSession : PacketSession
{
public int SessionId { get; set; }
public GameRoom Room { get; set; }
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Program.Room.Push(() => Program.Room.Enter(this));
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
public override void OnDisconnected(EndPoint endPoint)
{
SessionManager.Inst.Remove(this);
if(Room != null)
{
Room.Push(() => Room.Leave(this));
Room = null;
}
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
PacketHandler.cs
class PacketHandler
{
public static void C_ChatHandler(PacketSession session, IPacket packet)
{
C_Chat chatPacket = packet as C_Chat;
ClientSession clientSession = session as ClientSession;
if (clientSession.Room == null)
return;
GameRoom room = clientSession.Room;
// 할 일을 바로 해주는게 아니라 Push
room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
}
}
Server의 Main에서 Listener를 Init할때 ClientSession 객체를 바로 생성해주는 것이 아닌, SessionManager를 통해 Generate해서 모든 세션을 관리하는 방식을 채택했다. 세션마다 고유 번호를 할당하고 id를 키값으로 Dictionary에 ClientSession을 넣어서 생성, 찾기, 삭제를 할 때마다 lock을 걸어 동시 접근을 차단하는 식으로 구현했다.
서버에는 하나의 GameRoom이 존재하며, 하나의 GameRoom에는 List타입의 _sessions가 존재한다. 그리고 GameRoom에서 어떤 하나의 클라이언트가 메시지를 서버에게 전송하면 서버는 입장 중인 모든 세션 객체에 그 메시지를 Broadcast한다.
하나의 ClientSession이 서버와의 접속이 끊어질 경우, OnDisconnected함수가 호출되어 SessionManager.Remove와 해당 세션이 참여 중인 GameRoom이 있을 경우 GameRoom.Leave() 함수 호출을 통해 퇴장하도록 했다.
10명의 유저가 한 공간에 존재한다고 할때, 한 명의 유저가 메시지를 전송하면, 그 공간 안에 10명의 유저에게 뿌려줘야 한다. 그렇다면 10명의 유저가 동시에 메시지를 하나씩 전송한다면, 10*10=100번의 패킷을 전송해야한다는 말이다. 시간복잡도로 말하자면 O(n^2)가 된다. 그래서 n을 100, 1000으로 늘리면 늘릴수록 서버에 부담이 갈 수 있다.
코드를 그대로 실행해보면 Broadcast 부분에서 수많은 작업자 스레드가 lock에서 대기하고 있는 모습을 볼 수 있다. 하나의 쓰레드가 foreach문을 다 돌 때까지 다른 쓰레드가 대기를 할 수 밖에 없는 상황인데, 최악인 것은 쓰레드가 처리를 못하고 시간을 끌면 작업자 쓰레드를 새로 만들어버리는 악순환이 발생한다는 것이다.
따라서 모든 로직을 lock을 잡아 실행하는 것이 아니라 GameRoom에 Queue를 하나 만들고, 쓰레드들이 일감을 queue에 넣어두고 대기하지 말고 각자 할 일을 하러 가게끔 만들면 된다. 그런 큐를 JobQueue라고 한다.
protobuf 에서 제공하는 컴파일로 컴파일 하게 되면 직렬화하여 넣을 수 있는 구조가 만들어지고
다음 처럼 C++ 에서 사용 하여 데이터를 넣을 수 있다
add_buffs() 는 데이터를 내무적으로 추가하고 포인터를 얻어와 data 포인터를 통해서 데이터를 쓰는 방식이다
sendbuffer 에 패킷 데이터를 밀어 넣기
->Buffer()); //위 코드 중 짤린 부분
S_TEST 패킷을 만들어 데이터를 넣은 모습이다
실제 패킷 보내기처리
다음은 역질렬화방법이다
헤더 만큼은 건너 뛰고 S_TEST::pkt 에 데이터를 담아온다
프로토토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한, 직렬화 데이타 구조 (Serialized Data Structure)이다. C++,C#, Go, Java, Python, Object C, Javascript, Ruby 등 다양한 언어를 지원하며 특히 직렬화 속도가 빠르고 직렬화된 파일의 크기도 작아서 Apache Avro 파일 포맷과 함께 많이 사용된다.
(직렬화란 데이타를 파일로 저장하거나 또는 네트워크로 전송하기 위하여 바이너리 스트림 형태로 저장하는 행위이다.)
특히 GRPC 라는 네트워크 프로토콜의 경우 HTTP 2.0 을 기반으로 하면서, 메세지를 이 프로토콜 버퍼를 이용하여 직렬화하기 때문에, 프로토콜 버퍼를 이해해놓으면 GRPC를 습득하는 것이 상대적으로 쉽다.
프로토콜 버퍼는 하나의 파일에 최대 64M까지 지원할 수 있으며, 재미있는 기능중 하나는 JSON 파일을 프로토콜 버퍼 파일 포맷으로 전환이 가능하고, 반대로 프로토콜 버퍼 파일도 JSON으로 전환이 가능하다.
설치 및 구성
프로토콜 버퍼 개발툴킷은 크게 두가지 부분이 있다. 데이타 포맷 파일을 컴파일 해주는 protoc 와 각 프로그래밍 언어에서 프로토콜 버퍼를 사용하게 해주는 라이브러리 SDK가 있다.
프로토콜 버퍼를 사용하기 위해서는 저장하기 위한 데이타형을 proto file 이라는 형태로 정의한다. 프로토콜 버퍼는 하나의 프로그래밍 언어가 아니라 여러 프로그래밍 언어를 지원하기 때문에, 특정 언어에 종속성이 없는 형태로 데이타 타입을 정의하게 되는데, 이 파일을 proto file이라고 한다.
이렇게 정의된 데이타 타입을 프로그래밍 언어에서 사용하려면, 해당 언어에 맞는 형태의 데이타 클래스로 생성을 해야 하는데, protoc 컴파일러로 proto file을 컴파일하면, 각 언어에 맞는 형태의 데이타 클래스 파일을 생성해준다.
결과 화면인데 패킷중 첫번째를 size로 받아와 size 바이트 만큼 받아 왔으면 그다음 id 그리고 데이터를 순차적으로 읽어온다, 여기서 패킷은 TCP 로 전송 될경우 데이터 한 덩어리중 일부가 지연으로 나중에 올경우를 대비해
패킷 중 사이즈가 2바이트라면 2바이트를 먼저 받고 해당 사이즈를 먼저 추출한다음 전체 바이트 만큼 모두 도착 할때까지 수신을 반복한다, 그리고 모두다 데이터를 수신 받았다면 이후부터 패킷의 내용들을 까서 데이터로 받는 처리를 하게 된다
TCP 는 데이터의 순서가 보장 됨으로 나중에 왔다 하더라고 모두 전송된 데이터의 비트들은 정상적으로 정렬되어 있다
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
int pos = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
pos += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
pos += 2;
// TODO
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq:
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
pos += 8;
}
break;
case PacketID.PlayerInfoOk:
{
int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
pos += 4;
int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
pos += 4;
}
//Handle_PlayerInfoOk();
break;
default:
break;
}
Console.WriteLine($"RecvPacketId: {id}, Size {size}");
}
Thread Local Storage(TLS)
TLS : 스레드 빌딩 블록으로 스레드 마다 고유한 메모리 영역에 할당되게 된다
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
코드
ServerCore
Session.cs
namespace ServerCore
{
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
// size = size 포함한 전체 패킷 크기
// [size(2)][packetId(2)][ ... ][size(2)][packetId(2)][ ... ]
public sealed override int OnRecv(ArraySegment<byte> buffer) // 오버라이드 다시 불가
{
int processLen = 0;
while (true)
{
// 최소한 헤더는 파싱할 수 있는지 확인
if (buffer.Count < HeaderSize)
break;
// 패킷이 완전체로 도착했는지 확인
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ushort
if (buffer.Count < dataSize)
break;
// 여기까지 왔으면 패킷 조립 가능. new 사용했다고 힙에다 할당해주는게 아니라 스택 복사
OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
processLen += dataSize;
// [size(2)][packetId(2)][ ... ] 다음 부분으로 위치 변경
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return processLen;
}
public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}
public abstract class Session
{
Socket _socket;
int _disconnected = 0;
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer); // 얼마만큼 데이터를 처리했는지 리턴
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
public void Send(ArraySegment<byte> sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
ArraySegment<byte> buff = _sendQueue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null;
_pendingList.Clear();
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
}
void RegisterRecv()
{
_recvBuffer.Clean(); // 커서가 너무 뒤로 가있는 상태 방지
// 유효한 범위 설정. 다음으로 버퍼를 받을 공간 Set.
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // count=freesize, 이만큼 받을 수 있다.
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Write 커서 이동
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다
int processLen = OnRecv(_recvBuffer.ReadSegment);
if (processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
// Read 커서 이동
if (_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
SendBuffer
namespace ServerCore
{
public class SendBufferHelper
{
// 전역이지만 내 쓰레드에서만 사용할 수 있음.
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; }); // 처음 만들어질때 null만 리턴하도록
public static int ChunkSize { get; set; } = 4096 * 100;
public static ArraySegment<byte> Open(int reserveSize)
{
if (CurrentBuffer.Value == null) // SendBuffer 한번도 사용 안한 상태
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(ChunkSize); // 기존 청크 없앤 후 새롭게 할당
return CurrentBuffer.Value.Open(reserveSize);
}
public static ArraySegment<byte> Close(int usedSize)
{
return CurrentBuffer.Value.Close(usedSize);
}
}
public class SendBuffer
{
// RecvBuffer처럼 Clean이 없는 이유는 내가 사용이 끝난 부분 이전 부분을 다른 세션에서 Session클래스의 _sendQueue에 넣어논 상태일 수 있기 때문에, 즉 누군가 앞부분을 참조 중인 상태일 수 있기 때문에 재위치 시킬 수 없다.
// 패킷에 가변인자 들어가면 크기 예측하는게 까다롭기 때문에, 크게 할당 받아놓고 자르는 방식
// [][][][][][][][][u][] : ChunkSize = 4096 * 100
byte[] _buffer;
int _usedSize = 0;
public int FreeSize { get { return _buffer.Length - _usedSize; } }
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
public ArraySegment<byte> Open(int reserveSize) // 요구할 예약 공간
{
if (reserveSize > FreeSize)
return null;
// 예약공간이기 때문에 usedSize 이동 x
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize); // usedSize 위치 포함해서 reserveSize만큼
}
public ArraySegment<byte> Close(int usedSize) // 예약 사이즈는 3이라도 실제로 2개 사용되었다면 2만큼 범위 리턴
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
}
}
Server
ClientSession.cs
namespace Server
{
class Packet
{
public ushort size;
public ushort packetId;
}
class PlayerInfoReq : Packet
{
public long playerId;
}
class PlayerInfoOk : Packet
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
class ClientSession : PacketSession
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
Thread.Sleep(5000);
Disconnect();
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
int pos = 0;
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
pos += 2;
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
pos += 2;
// TODO
switch ((PacketID)id)
{
case PacketID.PlayerInfoReq: // required
{
long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
pos += 8;
}
break;
case PacketID.PlayerInfoOk:
{
int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
pos += 4;
int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
pos += 4;
}
//Handle_PlayerInfoOk();
break;
default:
break;
}
Console.WriteLine($"RecvPacketId: {id}, Size {size}");
}
// TEMP
public void Handle_PlayerInfoOk(ArraySegment<byte> buffer)
{
}
...
}
DummyClient
ServerSession.cs
namespace DummyClient
{
class Packet
{
public ushort size;
public ushort packetId;
}
class PlayerInfoReq : Packet
{
public long playerId;
}
class PlayerInfoOk : Packet
{
public int hp;
public int attack;
}
public enum PacketID
{
PlayerInfoReq = 1,
PlayerInfoOk = 2,
}
class ServerSession : Session
{
// unsafe : 포인터 조작. 속도가 빠른 장점
static unsafe void ToBytes(byte[] array, int offset, ulong value)
{
fixed (byte* ptr = &array[offset])
*(ulong*)ptr = value;
}
static unsafe void ToBytes<T>(byte[] array, int offset, T value) where T : unmanaged
{
fixed (byte* ptr = &array[offset])
*(T*)ptr = value;
}
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 패킷의 크기는 아래에서 정해짐
PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };
// 보낸다
for (int i = 0; i < 5; i++)
{
ArraySegment<byte> s = SendBufferHelper.Open(4096);
//byte[] size = BitConverter.GetBytes(packet.size);
//byte[] packetId = BitConverter.GetBytes(packet.packetId);
//byte[] playerId = BitConverter.GetBytes(packet.playerId);
ushort size = 0; // int로 만들면 TryWriteBytes의 버전 중 int 버전으로 넘겨줌. ToUInt16
bool success = true;
// destination(Span<byte>)의 공간보다 value의 크기가 크다면 실패
// offset+size부터 offset-size만큼의 범위
size += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.packetId);
size += 2;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.playerId);
size += 8;
success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), size);
ArraySegment<byte> sendBuff = SendBufferHelper.Close(size); // 실질적으로 보내줄 버퍼.
if (success)
Send(sendBuff);
}
}
...
}
현재 보낼 수 있으면 큐에 있는 내용을 보내고 보낼수 없는 상황이라면 큐에 보낼 내용을 담아 놓기만한다
보낼수 있는 상황과 보낼수 없는 상황은
우선 한번 SendAsync 를 할때 동시에 할수는 없고 한번에 하나만 보낼수 있음으로 SendAsync 가 완료 될때까지 다른 스레드가 SendAsync 를 하지 못하게 데이터만 큐에 쌓아 놓게 한뒤 SendAsync 처리가 완료되는 OnSendCompleted 가 호출 되면 SendAsync 처리를 마무리 한다
그런데 OnSendCompleted 가 호출 될때 SendAsync 의 한번의 루틴이 끝나기 전에 데이터가 쌓인게 있다면
다시 한번 RegisterSend 를 호출하여 기존에 쌓여 있는것을 다시 send 해주는데
이렇게 하는 이유는 SocketAsyncEventArg 를 여러개 생성하지 않고 재사용하기 위해 이런 처리를 하는건데
이유는 좀금이라도 send 할때의 비용을 아끼기 위해서이다 (유저가 많아질 수록 이 부하도 점점 쌓이기 때문)
session 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnected = 0;
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
bool _pending = false;
object _lock = new object();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
//receive 할때 데이터를 받는 버퍼를 만들어준다
recvArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다
//초기에 한번 receive 되도록 등록해 준다
RegisterRecv(recvArgs);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
}
public void Send(byte[] sendBuff)
{
lock(_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pending == false)
{
RegisterSend();
}
}
}
void RegisterSend()
{
_pending = true;
byte[] buff = _sendQueue.Dequeue();
_sendArgs.SetBuffer(buff, 0, buff.Length);
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
//다른 소켓에 의해 OnSendCompleted 호출 될 수 있음으로 lock 을 걸어준다
private void OnSendCompleted(object value, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
if(_sendQueue.Count > 0)
{
RegisterSend(); //send를 보내는 처리 과정 사이에 다른 스레드에 의해서 send 가 보내진게 있다면 다시 send 를 보내도록 한다
}
else
{
_pending = false;
}
}
catch (System.Exception ex)
{
Console.WriteLine($"OnSendCompleted Failed {ex}");
}
}
else
{
DisConnect();
}
}
}
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
//pending 이 false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
//OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
if (pending == false)
{
OnRecvCompleted(null, args);
}
}
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//받은 바이트가 0 바이트 이상이고
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
//args.BytesTransferred : 몇바이트를 받았는지 바이트 수
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From client] {recvData}");
RegisterRecv(args);
}
catch (Exception e)
{
Console.WriteLine($"OnRecevCompleted Failed {e}");
}
}
else
{
DisConnect();
}
}
public void DisConnect()
{
//멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
//Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
{
//즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
//다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
return;
}
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
}
}
서버 시작 부분
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
//클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
session.Start(clientSocket); //안에 receive 가 비동기로 처리됨
//클라이언트로 보내는 처리
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
session.Send(sendBuff);
Thread.Sleep(1000);
session.DisConnect();
//session.DisConnect(); //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
}
static void Main(string[] args)
{
//DNS : Domain Name System
// 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
//www.google.com =>
string host = Dns.GetHostName();
//host = "google.com"; //ipHost.AddressList[0] == {172.217.161.238}
IPHostEntry ipHost = Dns.GetHostEntry(host);
//이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨
// ipHost.addressList[0] = IPAddress.Parse(" 경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); //최종 주소
try
{
_listener.Init(endPoint, OnAcceptHandler);
while (true)
{
}
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
}
}
리슨 부분 ( 이전에 있던 Listen 과 유사하다)
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
{
//AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌,
//tcp 로 할 경우 stream, tcp 로 설정해준다
//리슨 하는 자체가 소켓을 하나 만들어야 한다
_listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
_listenSocket.Bind(iPEndPoint); //소켓에 ip 와 포트 할당
//최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
//Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가
//대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.
_listenSocket.Listen(10);
//이건 한번 사용하고 재사용이 가능하다
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//최초 한번은 등록해준다
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
//재사용이 됨으로 null 로 처리한다
args.AcceptSocket = null;
//SocketAsyncEventArgs 가 하나일때
//AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
//하나하고 그다음 하나 step by step
bool pending = _listenSocket.AcceptAsync(args); //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
if (pending == false) //false 면 pending 없이 바로 완료 됐다는 얘기임
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
//accept 되어 새로 생성된 소켓을
_onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
//위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
//RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다
RegisterAccept(args);
}
public Socket Accept()
{
//return _listenSocket.Accept();
//클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
//클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
//return _listenSocket.Accept();
//async 는 비동기로 처리 된다
//return _listenSocket.AcceptAsync()
return null;
}
}
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
while (true)
{
try
{
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
socket.Connect(endPoint);
Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");
//서버로 보낸다
for(int i=0;i< 5; ++i)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i} ");
int bytesSent = socket.Send(sendBuff);
}
//서버에서 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"received : {recvData}");
//서버와 통신을 위해 생성했던 소켓 종료 처리
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
Thread.Sleep(100);
}
}
}
}
클라이언트 더미 코드 또한 동일하다 : 좀 더 다양하게 서버로 접속하게 처리해야 하지만 우선 send 에 만 비동기이면서 멀티스레드로 보낼수 있는 구조로 만드는 것에 초점을 맞춘다
서버코어 부분인데 이 부분에서 listen과 receive 를 하기 위한 초기호및 보내기 데이터 설정 등을 한다
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
//클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
static void OnAcceptHandler(Socket clientSocket)
{
try
{
Session session = new Session();
session.Start(clientSocket); //안에 receive 가 비동기로 처리됨
//클라이언트로 보내는 처리
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
session.Send(sendBuff);
Thread.Sleep(1000);
session.DisConnect();
//session.DisConnect(); //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
}
static void Main(string[] args)
{
//DNS : Domain Name System
// 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
//www.google.com =>
string host = Dns.GetHostName();
//host = "google.com"; //ipHost.AddressList[0] == {172.217.161.238}
IPHostEntry ipHost = Dns.GetHostEntry(host);
//이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨
// ipHost.addressList[0] = IPAddress.Parse(" 경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); //최종 주소
try
{
_listener.Init(endPoint, OnAcceptHandler);
while (true)
{
}
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
}
}
리슨을 담당하는 코드
Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가 대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
{
//AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌,
//tcp 로 할 경우 stream, tcp 로 설정해준다
//리슨 하는 자체가 소켓을 하나 만들어야 한다
_listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
_listenSocket.Bind(iPEndPoint); //소켓에 ip 와 포트 할당
//최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
_listenSocket.Listen(10);
//이건 한번 사용하고 재사용이 가능하다
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//최초 한번은 등록해준다
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
//재사용이 됨으로 null 로 처리한다
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args); //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
if (pending == false) //false 면 pending 없이 바로 완료 됐다는 얘기임
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
//accept 되어 새로 생성된 소켓을
_onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
//위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
//RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다
RegisterAccept(args);
}
public Socket Accept()
{
//return _listenSocket.Accept();
//클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
//클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
//return _listenSocket.Accept();
//async 는 비동기로 처리 된다
//return _listenSocket.AcceptAsync()
return null;
}
}
}
세션코드로 클라로부터 들어온 데이터를 비동기로 받는 로직이다
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnected = 0;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
//receive 할때 데이터를 받는 버퍼를 만들어준다
recvArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다
//초기에 한번 receive 되도록 등록해 준다
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
//pending 이 false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
//OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
if (pending == false)
{
OnRecvCompleted(null, args);
}
}
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//받은 바이트가 0 바이트 이상이고
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
//args.BytesTransferred : 몇바이트를 받았는지 바이트 수
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From client] {recvData}");
RegisterRecv(args);
}
catch (Exception e)
{
Console.WriteLine($"OnRecevCompleted Failed {e}");
}
}
else
{
}
}
public void DisConnect()
{
//멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
//Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
{
//즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
//다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
return;
}
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
}
}
서버와 클라간의 테스트를 위한 더미 클라이언트 코드
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
while (true)
{
try
{
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
socket.Connect(endPoint);
Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");
//서버로 보낸다
for(int i=0;i< 5; ++i)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i} ");
int bytesSent = socket.Send(sendBuff);
}
//서버에서 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"received : {recvData}");
//서버와 통신을 위해 생성했던 소켓 종료 처리
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
Thread.Sleep(100);
}
}
}
}
bool pending = _listenSocket.AcceptAsync(args);
SocketAsyncEventArgs 가 하나일때(즉 n 번 선언해 놓고 Register 를 n 번 한게 아닌 n = 1 일때) AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다, 하나하고 그다음 하나 step by step
클라 접속이 이뤄지더라도 AcceptAsync를 명시적으로 호출해줘야 OnAcceptCompleted가 실행이 된다 즉, AcceptAsync는 일종의 입장 허락의 개념인데 그 전에 접속을 희망한 클라들은 대기열(큐)에 입장 대기를 하고 있게 되고 OnAcceptCompleted를 호출해서 입장 관련 처리를 다 끝낸 다음, 마지막에 RegisterAccept를 다시 호출해서 다음 입장을 받아주기 때문에 동시에 여러 쓰레드가 OnAcceptCompleted를 실행할 수 없기도 하다
Accepting connections asynchronously gives you the ability to send and receive data within a separate execution thread. Before calling the AcceptAsync method, you must call the Listen method to listen for and queue incoming connection requests.
결과 화면 : 더미 클라에서 send 를 연달아 다섯번정도하기 때문에 서버에서 비동기 receive 로 5번이 처리 되는것 것을 볼 수 있다, 콘솔 창에서 보면 알 수 있듯이 receive 가 동시에 멀티 스레드 처럼 5번 처리 되는것이 아니고 한번에 한번만 처리 되는 것이라 볼 수 있는데 현재 Recv 코드는 오직 1번에 1개의 쓰레드만 접근 할 수 있다
다시 말해 최초에 RegisterRecv 를 1개만 걸어 놨기 때문에 Register->Completed .. 과정도 오로지 한번에 1개만 일어난다
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
//클라이언트로부터 받아오는 처리
byte[] recvBuff = new byte[1024];
int recvBytpes = clientSocket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
Console.WriteLine($"[From Client] {recvData}");
//클라이언트로 보내는 처리
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
clientSocket.Send(sendBuff);
clientSocket.Shutdown(SocketShutdown.Both); //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
clientSocket.Close(); //클라와 서버간의 연결 끊기
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
}
static void Main(string[] args)
{
//DNS : Domain Name System
// 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
//www.google.com =>
string host = Dns.GetHostName();
//host = "google.com"; //ipHost.AddressList[0] == {172.217.161.238}
IPHostEntry ipHost = Dns.GetHostEntry(host);
//이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨
// ipHost.addressList[0] = IPAddress.Parse(" 경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); //최종 주소
try
{
_listener.Init(endPoint, OnAcceptHandler);
while (true)
{
}
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
}
}
리스터 코드 : 비동기 accept
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
{
//AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌,
//tcp 로 할 경우 stream, tcp 로 설정해준다
//리슨 하는 자체가 소켓을 하나 만들어야 한다
_listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
_listenSocket.Bind(iPEndPoint); //소켓에 ip 와 포트 할당
//최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
_listenSocket.Listen(10);
//이건 한번 사용하고 재사용이 가능하다
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//최초 한번은 등록해준다
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
//재사용이 됨으로 null 로 처리한다
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args); //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
if (pending == false) //false 면 pending 없이 바로 완료 됐다는 얘기임
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
//accept 되어 새로 생성된 소켓을
_onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
//위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
//RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다
RegisterAccept(args);
}
public Socket Accept()
{
//return _listenSocket.Accept();
//클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
//클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
//return _listenSocket.Accept();
//async 는 비동기로 처리 된다
//return _listenSocket.AcceptAsync()
return null;
}
}
}
더미 클라이언트 코드
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
while (true)
{
try
{
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
socket.Connect(endPoint);
Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");
//서버로 보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
int bytesSent = socket.Send(sendBuff);
//서버에서 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"received : {recvData}");
//서버와 통신을 위해 생성했던 소켓 종료 처리
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
}
Thread.Sleep(100);
}
}
}
}
AcceptAsync : 비동기 방식으로 클라의 접속을 말한다
AcceptAsync 로 처리하면 서버에서 클라의 접속을 받아 Accept() 함수 처럼 accept 하게 될때 해당 라인에서 무한 대기 하지 않고 클라와 통신하기 위한 전용 socket 을 만들어 리턴하고 서버에선 바로 다음 로직을 처리 할 수 있게 된다
using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
namespace ServerCore
{
class Program
{
static void Main(string[] args)
{
//DNS : Domain Name System
// 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
//www.google.com =>
string host = Dns.GetHostName();
//host = "google.com"; //ipHost.AddressList[0] == {172.217.161.238}
IPHostEntry ipHost = Dns.GetHostEntry(host);
//이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨
// ipHost.addressList[0] = IPAddress.Parse(" 경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); //최종 주소
//AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌,
//tcp 로 할 경우 stream, tcp 로 설정해준다
//리슨 하는 자체가 소켓을 하나 만들어야 한다
Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
listenSocket.Bind(endPoint); //소켓에 ip 와 포트 할당
//최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
listenSocket.Listen(10);
while (true)
{
Console.WriteLine("Listening .... ");
//클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
//클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
Socket clientSocket = listenSocket.Accept();
//클라이언트로부터 받아오는 처리
byte[] recvBuff = new byte[1024];
int recvBytpes = clientSocket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
Console.WriteLine($"[From Client] {recvData}");
//클라이언트로 보내는 처리
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
clientSocket.Send(sendBuff);
clientSocket.Shutdown(SocketShutdown.Both); //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
clientSocket.Close(); //클라와 서버간의 연결 끊기
}
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
}
}
클라이언트 코드
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
socket.Connect(endPoint);
Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");
//서버로 보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
int bytesSent = socket.Send(sendBuff);
//서버에서 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"received : {recvData}");
//서버와 통신을 위해 생성했던 소켓 종료 처리
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
}
}
기존엔 함수 형태를 클래스 처럼 (억지로??) 맞추는 형태였기에 구문 문법 자체가 지저분해졌는데 Class 를 사용함으로서 이보다는 좀더 깔끔하게 정의할 수 있다
JavaScript class는 ECMAScript 6을 통해 소개되었으며, 기존 prototype 기반의 상속 보다 명료하게 사용할 수 있습니다. Class 문법은 새로운 객체지향 상속 모델을 제공하는 것은 아닙니다. JavaScript class는 객체를 생성하고 상속을 다루는데 있어 훨씬 더 단순하고 명확한 문법을 제공합니다.
함수 선언과 클래스 선언의 중요한 차이점은 함수 선언의 경우 호이스팅이 일어나지만, 클래스 선언은 그렇지 않다는 것입니다. 클래스를 사용하기 위해서는 클래스를 먼저 선언 해야 하며, 그렇지 않으면, 다음 아래의 코드는 ReferenceError 에러를 던질 것입니다. :
var p =newPolygon();// ReferenceErrorclassPolygon{}
constructor 메소드는 class 로 생성된 객체를 생성하고 초기화하기 위한 특수한 메소드입니다. "constructor" 라는 이름을 가진 특수한 메소드는 클래스 안에 한 개만 존재할 수 있습니다. 만약 클래스에 한 개를 초과하는 constructor 메소드를 포함한다면, SyntaxError 가 발생할 것입니다.
constructor는 부모 클래스의 constructor 를 호출하기 위해 super 키워드를 사용할 수 있습니다.
static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.
정적 메소드나 프로토타입 메소드가 this 값 없이 호출될 때, this 값은 메소드 안에서 undefined가 됩니다. class 문법 안에 있는 코드는 항상 strict mode 로 실행되기 때문에 이동작은 "use strict" 명령어가 없더라도 같습니다.
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined
Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined
If the above is written using traditional function–based syntax, then autoboxing in method calls will happen in non–strict mode based on the initial this value. If the inital value is undefined, this will be set to the global object.
Autoboxing will not happen in strict mode, the this value remains as passed.
function Animal() { }
Animal.prototype.speak = function() {
return this;
}
Animal.eat = function() {
return this;
}
let obj = new Animal();
let speak = obj.speak;
speak(); // global object
let eat = Animal.eat;
eat(); // global object
extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 클래스의 자식 클래스를 생성하기 위해 사용됩니다.
classAnimal{constructor(name){this.name = name;}speak(){
console.log(this.name +' makes a noise.');}}classDogextendsAnimal{speak(){
console.log(this.name +' barks.');}}
subclass에 constructor가 있다면, "this"를 사용하기 전에 가장 먼저 super()를 호출해야 합니다.
또한 es5에서 사용되던 전통적인 함수 기반의 클래스를 통하여 확장할 수도 있습니다.
function Animal (name){this.name = name;}
Animal.prototype.speak =function(){
console.log(this.name +' makes a noise.');}classDogextendsAnimal{speak(){
console.log(this.name +' barks.');}}var d =newDog('Mitzie');
d.speak();
클래스는 생성자가 없는 객체(non-constructible)을 확장할 수 없습니다. 만약 기존의 생성자가 없는 객체을 확장하고 싶다면, 이 메소드를 사용하세요. Object.setPrototypeOf():
var Animal ={speak(){
console.log(this.name +' makes a noise.');}};classDog{constructor(name){this.name = name;}speak(){
console.log(this.name +' barks.');}}
Object.setPrototypeOf(Dog.prototype, Animal);var d =newDog('Mitzie');
d.speak();
“A promise is an object that may produce a single value some time in the future”
프로미스는 자바스크립트 비동기 처리에 사용되는 객체입니다. 여기서 자바스크립트의 비동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성’을 의미합니다. 비동기 처리에 대한 이해가 없으시다면 이전 글 ‘자바스크립트 비동기 처리와 콜백 함수’를 읽어보시길 추천드립니다 :)
Promise가 왜 필요한가요?
프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용합니다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 아래와 같은 API를 사용합니다.
위 API가 실행되면 서버에다가 ‘데이터 하나 보내주세요’ 라는 요청을 보내죠. 그런데 여기서 데이터를 받아오기도 전에 마치 데이터를 다 받아온 것 마냥 화면에 데이터를 표시하려고 하면 오류가 발생하거나 빈 화면이 뜹니다. 이와 같은 문제점을 해결하기 위한 방법 중 하나가 프로미스입니다.
프로미스 코드 - 기초
그럼 프로미스가 어떻게 동작하는지 이해하기 위해 예제 코드를 살펴보겠습니다. 먼저 아래 코드는 간단한 ajax 통신 코드입니다.
functiongetData(callbackFunc){
$.get('url 주소/products/1',function(response){callbackFunc(response);// 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌});}getData(function(tableData){
console.log(tableData);// $.get()의 response 값이 tableData에 전달됨});
위 코드는 제이쿼리의 ajax 통신을 이용하여 지정한 url에서 1번 상품 데이터를 받아오는 코드입니다. 비동기 처리를 위해 프로미스 대신에 콜백 함수를 이용했죠.
위 코드에 프로미스를 적용하면 아래와 같은 코드가 됩니다.
functiongetData(callback){// new Promise() 추가returnnewPromise(function(resolve, reject){
$.get('url 주소/products/1',function(response){// 데이터를 받으면 resolve() 호출resolve(response);});});}// getData()의 실행이 끝나면 호출되는 then()getData().then(function(tableData){// resolve()의 결과 값이 여기로 전달됨
console.log(tableData);// $.get()의 reponse 값이 tableData에 전달됨});
콜백 함수로 처리하던 구조에서 new Promise(), resolve(), then()와 같은 프로미스 API를 사용한 구조로 바뀌었습니다. 여기서 new Promise()는 좀 이해가 가겠는데 resolve(), then()은 뭐 하는 애들일까요? 아래에서 함께 알아보겠습니다.
프로미스의 3가지 상태(states)
프로미스를 사용할 때 알아야 하는 가장 기본적인 개념이 바로 프로미스의 상태(states)입니다. 여기서 말하는 상태란 프로미스의 처리 과정을 의미합니다. new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖습니다.
Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태
Pending(대기)
먼저 아래와 같이 new Promise() 메서드를 호출하면 Pending(대기) 상태가 됩니다.
그리고, 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있습니다.
functiongetData(){returnnewPromise(function(resolve, reject){reject(newError("Request is failed"));});}// reject()의 결과 값 Error를 err에 받음getData().then().catch(function(err){
console.log(err);// Error: Request is failed});
위 코드는 서버에서 제대로 응답을 받아오면 resolve() 메서드를 호출하고, 응답이 없으면 reject() 메서드를 호출하는 예제입니다. 호출된 메서드에 따라 then()이나 catch()로 분기하여 데이터 또는 오류를 출력합니다.
여러 개의 프로미스 연결하기 (Promise Chaining)
프로미스의 또 다른 특징은 여러 개의 프로미스를 연결하여 사용할 수 있다는 점입니다. 앞 예제에서 then() 메서드를 호출하고 나면 새로운 프로미스 객체가 반환됩니다. 따라서, 아래와 같이 코딩이 가능합니다.
functiongetData(){returnnewPromise({// ...});}// then() 으로 여러 개의 프로미스를 연결한 형식getData().then(function(data){// ...}).then(function(){// ...}).then(function(){// ...});
위 코드는 프로미스 객체를 하나 생성하고 setTimeout()을 이용해 2초 후에 resolve()를 호출하는 예제입니다.
resolve()가 호출되면 프로미스가 대기 상태에서 이행 상태로 넘어가기 때문에 첫 번째 .then()의 로직으로 넘어갑니다. 첫 번째 .then()에서는 이행된 결과 값 1을 받아서 10을 더한 후 그다음 .then() 으로 넘겨줍니다. 두 번째 .then()에서도 마찬가지로 바로 이전 프로미스의 결과 값 11을 받아서 20을 더하고 다음 .then()으로 넘겨줍니다. 마지막 .then()에서 최종 결과 값 31을 출력합니다.
실무에서 있을 법한 프로미스 연결 사례
실제 웹 서비스에서 있을 법한 사용자 로그인 인증 로직에 프로미스를 여러 개 연결해보겠습니다.
위 코드는 페이지에 입력된 사용자 정보를 받아와 파싱, 인증 등의 작업을 거치는 코드를 나타내었습니다. 여기서 userInfo는 사용자 정보가 담긴 객체를 의미하고, parseValue, auth, display는 각각 프로미스를 반환해주는 함수라고 가정했습니다. 아래와 같이 말이죠.
var userInfo ={
id:'test@abc.com',
pw:'****'};functionparseValue(){returnnewPromise({// ...});}functionauth(){returnnewPromise({// ...});}functiondisplay(){returnnewPromise({// ...});}
앞에서 프로미스 에러 처리 방법 2가지를 살펴봤습니다. 개개인의 코딩 스타일에 따라서 then()의 두 번째 인자로 처리할 수도 있고 catch()로 처리할 수도 있겠지만 가급적 catch()로 에러를 처리하는 게 더 효율적입니다.
그 이유는 아래의 코드를 보시면 알 수 있습니다.
// then()의 두 번째 인자로는 감지하지 못하는 오류functiongetData(){returnnewPromise(function(resolve, reject){resolve('hi');});}getData().then(function(result){
console.log(result);thrownewError("Error in then()");// Uncaught (in promise) Error: Error in then()},function(err){
console.log('then error : ', err);});
따라서, 더 많은 예외 처리 상황을 위해 프로미스의 끝에 가급적 catch()를 붙이시기 바랍니다.
Rookie mistake #5: using side effects instead of returning
이 코드에 어떤 문제가 있는 것 같나요?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});
좋습니다. 이 코드를 가지고 당신이 알아야 하는 promise의 모든 것에 대해서 이야기 할 수 있습니다.
진지하게, 이 방법은 꽤나 이상해 보일 수 있지만 한번 이해하고 난 다음에는 우리가 여태 이야기한 모든 에러들을 예방할 수 있는 매우 좋은 방법입니다.
이전에 말했는데 promise가 가진 최대의 이점은 우리에게 소중한 return과 throw를 되돌려준다는 것입니다. 하지만 실제로 이게 어떤 형태일까요?
모든 promise는 우리에게 then() 메서드를 제공합니다. (또는 catch() 메서드나요.) then() 메서드 내부를 보겠습니다.
somePromise().then(function () {
// I'm inside a then() function!
});
이 함수의 내부에서 우리는 어떤 일을 할 수가 있을까요? 세가지가 가능합니다.
1. return 다른 promise
2. return 발생한 값 이나 undefined
3, throw 에러
이게 다입니다. 이제 이것들을 이해를 하면 여러분은 promise를 이해한 것입니다. 하나씩 살펴보겠습니다.
1. Return another promise
이는 위에서 살펴본 composing promise의 일반적인 패턴입니다.
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
첫 번째 함수가 getUserAccountById() 함수를 리턴 합니다, 다음 함수로 말이지요. 다음 메서드는 콜백으로 해당 함수의 리턴값을 받습니다. 여기서 return을 하는 것이 매우 중요한데 만약에 return을 하지 않는다면 다음 함수는 아마 undefined를 받게 될 것입니다. 우리는 userAccount가 필요한데 말입니다.
2. Return a synchronous value (or undefined)
undefined를 리턴 하는 일은 종종 발생하는 실수입니다. 하지만 동기적으로 발생한 값을 리턴 하는 것은 동기적인 코드를 promise방식의 코드로 변환하는 굉장한 방법입니다. 예를들어 우리가 사용자 캐시를 in-memory 에 가지고 있다고 하겠습니다. 우리는 아래의 코드처럼 구현할 수 있습니다.
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
굉장하지 않나요? 두 번째 함수는 userAccount가 동기적이나 비동기적으로 가져온 값인지 아닌지는 신경 쓰지 않습니다. 첫 번째 함수는 동기적인 값이나 비 동기적인 값 모두를 마음대로 전달할 수가 있습니다.
불행하게도 값을 리턴하지 않는 함수의 경우에는 자바스크립트는 undefined로 값을 처리해버립니다. 이는 예상치 못한 문제를 일으키게 됩니다.
이러한 이유로 저는 개인적으로는 then() 함수 안에서는 반드시 throw나 return을 통해서 무언가를 다음 함수로 전달을 시킵니다.
여러분도 이렇게 해야 합니다.
3. Throw a synchronous error
throw는 promise를 더욱 굉장하게 만들어 주는 역할을 합니다. 동기적으로 에러가 발생하면 throw를 통해 에러를 전달합니다. 사용자가 로그아웃이 되었다면 에러를 발생시키는 코드입니다.
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
우리의 catch()는 사용자가 로그아웃이 되었다면 동기적으로 에러를 받습니다. 그리고 물론 promise가 reject가 발생하는 경우 비동기적으로 에러를 받습니다. 다시 한번 강조하면 함수는 에러가 동기적이든 비동기적이든 전혀 신경 쓰지 않습니다.
이는 매우 유용한데 왜냐하면 개발 시에 코딩의 에러를 발견하는데 도움을 줍니다. 예를 들어 then() 메서드의 내부에 JSON.parse()를 한다고 가정 하겠습니다. 이 경우에 json에 문제가 있다면 동기적으로 에러를 발생시킬 수 있습니다. callback의 경우에는 에러를 무시하는 일이 발생합니다. 하지만 promise와 함께라면 우리는 간단하게 catch() 내부에서 이모든 일들을 처리할 수 있습니다.
Advanced mistakes
자, 이제 조금 더 깊이 있게 들어가보겠습니다.
이번에 다룰 내용들은 advanced 내용입니다. advanced로 분류가 된 이유는 이미 promise를 이해하고 받아들인 개발자들이 일으키는 실수이기 때문입니다. 하지만 우리는 이것들을 다룰 필요가 있습니다. 만약에 우리가 쉽게 해결할 수 있는 문제였다면 앞으로의 내용들을 beginner 섹션에 다뤘을 것입니다.
Advanced mistake #1: not knowing aboutPromise.resolve()
위의 다양한 예들을 통해 promise는 동기적인 코드를 비 동기적인 코드로 감싸주는 매우 유용한 기능입니다. 구현은 아래와 같이 합니다.
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);
위의 표현을 Promise.resolve()를 통해 훨씬 간결하게 표현하는 것이 가능합니다.
저는 위에서 catch()는 매우 유용하다고 말했습니다. 그래서 아래의 두 개의 작은 코드는 동일합니다.
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
하지만 아래의 두 개의 코드는 동일한 의미는 아닙니다.
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
만약에 위의 두 코드가 왜 다른지 궁금하다면 첫번째 코드에서 첫 함수가 에러를 반환한다면 무슨 일이 벌어질지를 생각해보면 됩니다.
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// I caught your error! :)
});
somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// I didn't catch your error! :(
});
보시는 봐와 같이 then(resolveHandler, rejectHandler)의 형태로 구현을 한다면 에러가 resolveHandler를 통해 반환이 된경우라면 rejectHandler가 에러를 잡아내지 못하게 됩니다.
이러한 이유로 저는 절대 두번째 방식으로는 구현을 하지 않습니다. 항상 에러에 대비해서는 catch()를 사용합니다. 단, Mocha 를 통한 비동기적인 테스트 코드 구현은 제외합니다.
it('should throw an error', function () {
return doSomethingThatThrows().then(function () {
throw new Error('I expected an error!');
}, function (err) {
should.exist(err);
});
});
Promise.all()
Promise.all(iterable) 메서드는 iterable 인자의 모든 프로미스가 이행하거나 iterable 인자가 비어있는 경우 이행하는 프로미스를 반환합니다. iterable 인자 내 임의의 프로미스가 거부하는 경우 첫 번째로 거절한 프로미스의 이유를 사용해 거절합니다.
인자에 프로미스가 없는 경우, 비동기적으로 이행된Promise. 단, 구글 크롬 58은 이미 이행된 프로미스를 반환합니다.
다른 모든 경우, 대기 중인 Promise. 반환하는 프로미스는 나중에 비동기적으로(스택이 비는 시점에) , 모든 프로미스가 이행하거나 임의의 프로미스가 거부할 때 이행/거부합니다. "Promise.all 실패-우선 연산" 예제를 참고하세요. 반환하는 프로미스의 이행 값은 인자로 넘어온 프로미스의 순서와 일치하며 완료 순서에 영향을 받지 않습니다.
이행: 빈 iterable이 전달되면이 메서드는 이미 해결 된 promise를 동기적으로 반환합니다. 전달 된 promise가 모두 충족(fulfilled)되거나 promise가 아닌 경우 Promise.all에서 반환하는 promise는 비동기 적으로 수행됩니다. 모든 경우에 반환된 promise는 인수로 전달 된 반복 가능한 값의 모든 값 (non-promise value)을 포함하는 배열로 충족(fulfilled)됩니다.
거부: 전달 된 promise중 하나라도 거부하면 Promise.all은 다른 promise가 남아있는지에 여부에 관계없이 다른 모든 Promise를 버리고 문제가 되는 값과 함께 거절(reject)한다.
Promise.all 은 배열 내 요소 중 어느 하나라도 거절(reject)하면 즉시 거절합니다. 즉 Promise.all 은 빠르게 실패합니다: 만약 timeout 이후 결정(resolve)하는 4개의 Promise를 가지고 있고, 그 중 하나가 거절(reject)하면 Promise.all 은 즉시 거절합니다.
all 구문중 실패시하는 Promise 함수가 있다면 해당 실패 promise 함수의 인자가 넘어오게 된다
Promise 오브젝트는 new 키워드와 생성자를 사용하여 만듭니다. 이 생성자는 인수로 "executor 함수"라는 함수를 사용합니다. 이 함수는 매개 변수로 두 가지 함수를 가져야 합니다. 비동기 작업이 성공적으로 완료되고 결과를 값으로 반환하면 첫 번째 함수(resolve)가 호출됩니다. 두 번째 (reject)는 작업이 실패 할 때 호출되며 보통 오류 객체를 반환합니다.
const myFirstPromise = new Promise((resolve, reject) => {
// do something asynchronous which eventually calls either:
//
// resolve(someValue); // fulfilled
// or
// reject("failure reason"); // rejected
});
함수가 프로미스를 사용하도록 하려면 단순히 프로미스를 반환하면됩니다.
function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
let myFirstPromise = new Promise((resolve, reject) =>
{
// We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
// In this example, we use setTimeout(...) to simulate async code.
// In reality, you will probably be using something like XHR or an HTML5 API.
setTimeout(function(){
resolve("Success!"); // Yay! Everything went well!
}, 3000);
});
myFirstPromise.then((successMessage) => {
// successMessage is whatever we passed in the resolve(...) function above.
// It doesn't have to be a string, but if it is only a succeed message, it probably will be.
console.log("Yay! " + successMessage);
});
결과 3초 후
"Success!"
Promise.prototype.then()
then() 메서드는 Promise를 리턴하고 두개의 콜백 함수를 인수로 받습니다. 하나는 Promise가 성공(success)했을 때를 위한 콜백 함수이고, 다른 하나는 실패(failure)했을 때를 위한 콜백 함수입니다.
then 메서드는 Promise를 리턴하기 때문에, 이어지는 then 호출들을 손쉽게 chaining 할 수 있습니다. onFulfilled 또는 onRejected 콜백 함수가 리턴하는 값은 자동으로 resolved promise로 wrapping 되기 때문에 다음에 오는 then 이나 catch 메서드로 전달 됩니다.
var p2 =newPromise(function(resolve, reject){resolve(1);});
p2.then(function(value){
console.log(value);// 1return value +1;}).then(function(value){
console.log(value);// 2});
p2.then(function(value){
console.log(value);// 1});
차이는 미묘하지만 매우 유용합니다. 프라미스 거부는 거부 콜백(또는 동일하게 기능하는 catch())을 사용하여 다음 then()으로 건너뜁니다. then(func1, func2)를 사용하는 경우 func1와 func2 중에 하나만 호출되며, 둘이 동시에 호출되지 않습니다. 그러나 then(func1).catch(func2)를 사용하는 경우 둘은 체인에서 개별적인 단계이므로 func1이 거부하면 둘 다 호출됩니다. 다음을 봅시다.
비슷하게 ajax 콜을 요청하거나 DB에서 데이터를 가져올 때와 같이 async 함수를 감싸는 경우는 new Promise()를 사용하면 됩니다.
처음 Promise를 사용할 때는 왜 같은 흐름의 함수가 두 개로 제공될까 이상했는데, 사용하다보니 return Promise.resolve()와 new Promise()는 사용 방법이 약간 다릅니다. return Promise.resolve()는 sync 로직 흐름에서, new Promise()는 sync는 물론, async 로직 흐름에서도 사용할 수 있습니다.
요새 모듈은 사용할 때 Promise를 지원하는 경우가 종종 있습니다. 좀 더 많은 모듈이 Promise를 지원하기를 바래봅니다
var obj ={
a:'hello',
b:function(){alert(this.a);}};
?? 엄청 간단하네요. 사실 객체 리터럴이 바로 싱글턴 패턴의 대표적인 예입니다. 저 객체는 단 하나밖에 존재하지 않죠. 하지만 모든 속성이 다 공개되어 있다는 단점이 있습니다. 비공개로 만드는 게 바로 제대로 된 싱글턴입니다.
var singleton =(function(){var instance;var a ='hello';functioninitiate(){return{
a: a,
b:function(){alert(a);}};}return{
getInstance:function(){if(!instance){
instance =initiate();}return instance;}}})();var first = singleton.getInstance();var second = singleton.getInstance();
console.log(first === second);// true;
코드가 확 길어졌네요. 차근히 살펴보면 쉽습니다. IIFE로 비공개 변수를 가질 수 있게 만들어줍니다. 그리고 그 안에 instance변수와 initiate 함수를 만들어줍니다. initiate 함수 안의 내용이 실제 객체의 내용입니다. 위의 obj 객체와 비교하면 a가 비공개 변수가 되었네요.
IIFE로 즉시 반환되는 부분(return)을 보시죠. getInstance라는 메소드를 가진 객체를 반환하는데, getInstance 함수를 호출하는 순간 내부적으로 initiate 함수가 호출되고, instance에 아까 그 객체의 내용이 저장되고 동시에 반환됩니다. getInstance가 여러 번 호출됐을 경우에는 코드를 보시면 이미 instance 객체가 있는 경우에는 initiate를 거치지 않고 바로 반환하는 것을 알 수 있습니다.
first와 second 변수를 보면 두 번 다 getInstance 함수를 호출했는데요. 결과적으로 두 변수는 같습니다. first 때 initiate된 객체를 second 때도 똑같이 반환받았기 때문이죠. 즉, 아무리 호출해도 기존에 있던 객체는 복사되는 것도 아니고 그냥 그대로 반환됩니다. 싱글턴 패턴은 모듈 패턴을 변형한 디자인 패턴입니다.
싱글턴을 언제 써야할 지 잘 모르겠나요? 처음 네임스페이스를 만들 때 사용하면 됩니다! 예를 들어 게임을 만든다고 치면, 게임 객체를 싱글턴으로 만드는 겁니다. 게임 내의 모든 것을 감싸고 있는 객체를 말이죠. 게임을 실행했을 때 게임은 한 번만 켜져야하기 때문에 싱글턴이 적절합니다.
세 번째는 생성자 패턴입니다. 이것도 이미 배웠죠? 상속을 배울 때 다뤘습니다! 대부분의 객체는 이 패턴으로 만들게 됩니다. 특히 상속이 필요할 때는 제일 많이 쓰이죠. 모듈 패턴과 생성자 패턴을 조합해서 코드를 보기 좋게 만들 수 있습니다.
Gadget.isShiny() 호출했을 때 내부의 this 는 Gadget 생성자를 가리키지만, iphone.isShiny() 를 호출했을 때는 this 는 생성자 함수로 생성된 객체인 iphone 을 가리키게 됩니다.
마지막으로 스태틱한 방법으로도, 스태틱하지 않은 방법으로도 호출될 수 있는 어떤 메서드를 호출 방식에 따라 살짝 다르게 동작하게 하는 예제를 살펴보도록 합니다.
메서드가 어떻게 호출되었는지 판별하기 위해서 instanceof 연산자를 활용해 봅니다.
JAVASCRIPT
// 생성자var Gadget =function(price){this.price = price;};// 스태틱 메서드
Gadget.isShiny =function(){// 다음은 항상 동작한다.var msg ='you bet';if(thisinstanceofGadget){// 다음은 스태틱하지 않은 방식으로 호출되었을 때만 동작하도록 한다.(인스턴스 멤버를 가리킴)
msg +=', it costs $'+this.price +'!!';}return msg;};// 프로토타입에 일반적인 메서드를 추가한다
Gadget.prototype.isShiny =function(){return Gadget.isShiny.call(this);// 이 this 는 생성자 함수로 생성된 인스턴스 객체를 가리키게 된다.};
스태틱 메서드와 인스턴스를 통해 스태틱하지 않은 방법으로 호출해 보면 다음과 같은 결과가 나타납니다.
JAVASCRIPT
// 스태틱 메서드를 호출
console.log(Gadget.isShiny());// you bet 이 기록된다.// 인스턴스를 통해 스태틱하지 않은 방법으로 호출var iphone =newGadget(345.99);
console.log(iphone.isShiny());// you bet, it costs $345.99!! 가 기록된다.
비공개 스태틱 멤버
지금까지는 공개 스태틱 멤버를 살펴보았습니다.
이번에는 비공개 스태틱 멤버를 구현하는 방법을 알아봅니다.
비공개 스태틱 멤버란 다음과 같은 의미를 가지고 있습니다.
동일한 생성자 함수로 생성된 객체들이 공유하는 멤버입니다.
생성자 외부에서는 접근할 수 없습니다.
Gadget 생성자 안에 counter 라는 비공개 스태틱 프로퍼티를 구현하는 예제를 살펴보도록 합니다.
비공개 프로퍼티는 먼저 클로저 함수를 만들고, 비공개 멤버를 이 함수로 감싼 후 이 함수를 즉시 실행한 결과로 새로운 함수를 반환하게 됩니다.
반환되는 함수는 Gadget 변수에 할당되어 새로운 생성자가 될 것입니다.
JAVASCRIPT
var Gadget =function(){// 스태틱 변수/프로퍼티var counter =0;// 생성자의 새로운 구현 버전을 반환한다.returnfunction(){
console.log(counter +=1);};}();// 즉시 실행한다.
새로운 Gadget 생성자는 단순히 비공개 counter 값을 증가시켜 출력합니다.
몇 개의 인스턴스를 만들어 테스트해보면 실제로 모든 인스턴스가 동일한 counter 값을 공유하고 있음을 확인할 수 있습니다.
JAVASCRIPT
var g1 =newGadget();// 1 이 기록된다var g2 =newGadget();// 2 이 기록된다var g3 =newGadget();// 3 이 기록된다
객체당 1씩 counter 를 증가시키고 있기 때문에 이 스태틱 프로퍼티는 Gadget 생성자를 통해 생성된 개별 객체의 유일성을 식별하는 ID 가 될 수 있다.
유일한 식별자는 쓸모가 많으니 특권 메서드로 노출시켜도 좋지 않을까?
앞선 예제에 덧붙여 비공개 스태틱 프로퍼티에 접근할 수 있는 getLastId() 라는 특권 메서드를 추가해보도록 해봅니다.
JAVASCRIPT
// 생성자var Gadget =function(){// 스태틱 변수/프로퍼티var counter =0,
NewGadget;// 이 부분이 생성자를 새롭게 구현한 부분이다.
NewGadget =function(){
counter +=1;};// 특권 메서드
NewGadget.prototype.getLastId =function(){return counter;};// Gadgt 생성자를 덮어쓴다.return NewGadget;}();// 즉시 실행한다.
새로운 버전을 아래에서 테스트 해봅니다.
JAVASCRIPT
var iphone =newGadget();
console.log(iphone.getLastId());// 1 이 기록var ipod =newGadget();
console.log(ipod.getLastId());// 2 이 기록var ipad =newGadget();
console.log(ipad.getLastId());// 3 이 기록
공개/비공개 스태틱 프로퍼티는 상당히 편리합니다.
특정 인스턴스에 한정되지 않는 메서드와 데이터를 담을 수 있고 인스턴스별로 매번 재생성되지도 않습니다.
자바스크립트는 프로토타입 기반 언어라고 불립니다. 자바스크립트 개발을 하면 빠질 수 없는 것이 프로토타입인데요. 프로토타입이 거의 자바스크립트 그 자체이기때문에 이해하는 것이 어렵고 개념도 복잡합니다.
하지만 프로토타입이 무엇인지 깨우친 순간 자바스크립트가 재밌어지고, 숙련도가 올라가는 느낌을 팍팍 받을 수 있습니다. 그럼 지금부터 프로토타입을 이해해봅시다.
Prototype vs Class
클래스(Class)라는 것을 한 번쯤은 들어보셨을겁니다. Java, Python, Ruby등 객체지향언어에서 빠질 수 없는 개념이죠. 그런데 중요한 점은 자바스크립트도 객체지향언어라는 것입니다. 이게 왜 중요하냐구요? 자바스크립트에는 클래스라는 개념이 없거든요. 대신 프로토타입(Prototype)이라는 것이 존재합니다. 자바스크립트가 프로토타입 기반 언어라고 불리는 이유이죠.
클래스가 없으니 기본적으로 상속기능도 없습니다. 그래서 보통 프로토타입을 기반으로 상속을 흉내내도록 구현해 사용합니다.
참고로 최근의 ECMA6 표준에서는 Class 문법이 추가되었습니다. 하지만 문법이 추가되었다는 것이지, 자바스크립트가 클래스 기반으로 바뀌었다는 것은 아닙니다.
어디다 쓰나요?
그럼 프로토타입을 언제 쓰는지 알아봅시다.
넌 이미 알고있다
자바스크립트에 클래스는 없지만 함수(function)와 new를 통해 클래스를 비스무리하게 흉내낼 수 있습니다.
function Person() { this.eyes = 2; this.nose = 1; }
자바스크립트 개발을 하시는 분이라면 아마 써보진 않았어도 최소한 본 적은 있을겁니다. 간단히 설명하자면 Person.prototype이라는 빈 Object가 어딘가에 존재하고, Person 함수로부터 생성된 객체(kim, park)들은 어딘가에 존재하는 Object에 들어있는 값을 모두 갖다쓸 수 있습니다. 즉, eyes와 nose를 어딘가에 있는 빈 공간에 넣어놓고 kim과 park이 공유해서 사용하는 것이죠. 이해되셨나요?
프로토타입을 깊게 파보면 엄청나게 복잡하지만 개발자가 사용하는 부분만 본다면 이게 거의 전부입니다. 하지만 개발자는 사용법만 알고있는게 아니라 언제나 왜? 를 생각해야합니다.
프로토타입이 왜 이렇게 쓰이는지 조금 더 깊게 알아보도록 하겠습니다.
Prototype Link와 Prototype Object
자바스크립트에는 Prototype Link 와 Prototype Object라는 것이 존재합니다. 그리고 이 둘을 통틀어 Prototype이라고 부릅니다. 프로토타입을 좀 안다는 것은 이 둘을 완벽히 이해하고 갖고 놀 수준이 되었다는 뜻입니다.
제가 프로토타입에 대해 공부하면서 중요하다고 생각되는 포인트가 몇 가지 있었습니다. 그 포인트들을 잘 이해하면서 보시기 바랍니다.
Prototype Object
모든 객체(Object)의 조상은 함수(Function)입니다.
function Person() {} // => 함수
var personObject = new Person(); // => 함수로 객체를 생성
personObject 객체는 Person이라는 함수로부터 파생된 객체입니다. 이렇듯 언제나 객체는 함수로부터 시작됩니다. 여러분이 많이 쓰는 일반적인 객체 생성도 예외는 아닙니다.
var obj = {};
얼핏보면 함수랑 전혀 상관없는 코드로 보이지만 위 코드는 사실 다음 코드와 같습니다.
var obj = new Object();
위 코드에서 Object가 자바스크립트에서 기본적으로 제공하는 함수입니다.
Object와 마찬가지로 Function, Array도 모두 함수로 정의되어 있습니다. 이것이 첫 번째 포인트입니다.
그렇다면 이것이 Prototype Object랑 무슨 상관이있느냐? 함수가 정의될 때는 2가지 일이 동시에 이루어집니다.
1.해당 함수에 Constructor(생성자) 자격 부여
Constructor 자격이 부여되면 new를 통해 객체를 만들어 낼 수 있게 됩니다. 이것이 함수만 new 키워드를 사용할 수 있는 이유입니다.
2.해당 함수의 Prototype Object 생성 및 연결
함수를 정의하면 함수만 생성되는 것이 아니라 Prototype Object도 같이 생성이 됩니다.
그리고 생성된 함수는 prototype이라는 속성을 통해 Prototype Object에 접근할 수 있습니다. Prototype Object는 일반적인 객체와 같으며 기본적인 속성으로 constructor와 __proto__를 가지고 있습니다.
constructor는 Prototype Object와 같이 생성되었던 함수를 가리키고 있습니다. __proto__는 Prototype Link입니다. 밑에서 자세히 설명합니다.
Prototype Object는 일반적인 객체이므로 속성을 마음대로 추가/삭제 할 수 있습니다. kim과 park은 Person 함수를 통해 생성되었으니 Person.prototype을 참조할 수 있게 됩니다.
Prototype Link를 보기 전에 Prototype Object를 어느 정도 이해하시고 보기 바랍니다. 함수가 정의될 때 이루어지는 일들을 이해하는 것이 두 번째 포인트, Prototype Object를 이해하는 것이 세 번째 포인트입니다.
Prototype Link
kim에는 eyes라는 속성이 없는데도 kim.eyes를 실행하면 2라는 값을 참조하는 것을 볼 수 있습니다. 위에서 설명했듯이 Prototype Object에 존재하는 eyes 속성을 참조한 것인데요, 이게 어떻게 가능한걸까요??
바로 kim이 가지고 있는 딱 하나의 속성 __proto__가 그것을 가능하게 해주는 열쇠입니다.
prototype 속성은 함수만 가지고 있던 것과는 달리(Person.prototype 기억나시죠?) __proto__속성은 모든 객체가 빠짐없이 가지고 있는 속성입니다.
__proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킵니다. kim객체는 Person함수로부터 생성되었으니 Person 함수의 Prototype Object를 가리키고 있는 것이죠.
__proto__를 까보니 역시 Person 함수의 Prototype Object를 가리키고 있었습니다.
kim객체가 eyes를 직접 가지고 있지 않기 때문에 eyes 속성을 찾을 때 까지 상위 프로토타입을 탐색합니다. 최상위인 Object의 Prototype Object까지 도달했는데도 못찾았을 경우 undefined를 리턴합니다. 이렇게 __proto__속성을 통해 상위 프로토타입과 연결되어있는 형태를 프로토타입 체인(Chain)이라고 합니다.
이런 프로토타입 체인 구조 때문에 모든 객체는 Object의 자식이라고 불리고, Object Prototype Object에 있는 모든 속성을 사용할 수 있습니다. 한 가지 예를 들면 toString함수가 있겠습니다.
var data =10;// window.data == this.data 형식과 동일하다.functionouter(){this.data =20;
data =30;
console.log("1. data1 = "+ data);// 30
console.log("2. this.data = "+this.data);// 30
console.log("3. window.data = "+ window.data);// 30}outer();
일반 중첩함수에서 this
중첩 함수에서의 this 는 window 를 가리킵니다.
JAVASCRIPT
var data =10;functionouter(){// 중첩함수functioninner(){this.data =20;
data =30;
console.log("1. data1 = "+ data);// 30
console.log("2. this.data = "+this.data);// 30
console.log("3. window.data = "+ window.data);// 30}inner();}outer();
이벤트 리스너에서 this
이벤트에서의 this 는 이벤트를 발생한 객체를 가리킵니다.
JAVASCRIPT
리스너실행
var data =10;$(document).ready(function(){// 이벤트 리스너 등록 $("#myButton").click(function(){this.data =20;
data =30;this.style.color ='#000';
console.log("1. data1 = "+ data);// 30
console.log("2. this.data = "+this.data);// 20 (이벤트 핸들러의 객체)
console.log("3. window.data = "+ window.data);// 30});});
메서드에서 this
메서드 내에서의 this 는 메서드를 호출한 객체를 가리킵니다.
JAVASCRIPT
var data =10;functionMyClass(){this.data =20;
data =30;
console.log("1. data1 = "+ data);
console.log("2. this.data = "+this.data);
console.log("3. window.data = "+ window.data);}// 인스턴스 생성var my1 =newMyClass();// this 는 메소드를 호출한 객체를 나타낸다.// 여기선 my1 이 인스턴스// 일반 함수 호출할 경우MyClass();// 30, 30, 30
메서드 내부의 중첩함수에서 this
메서드 내부의 중첩함수에서의 this 는 window 를 가리킵니다.
그리고 메서드 내부의 중첩함수에서 this 를 보존하는 방법은 아래와 같습니다.
JAVASCRIPT
var data =10;functionMyClass(){this.data =50;// 내부 함수내에서 this 를 보존하는 방법var objThis =this;
objThis.data =40;functioninner(){this.data =20;// window.data 와 같음.
data =30;// window.data 와 같음.
console.log("1. data1 = "+ data);// 30;
console.log("2. this.data = "+this.data);// 30 중첩(내부)함수, 즉 컨텍스트에 따라 this 가 달라진다.
console.log("3. window.data = "+ window.data);
console.log("4. objThis = "+ objThis.data);}inner();}// 인스턴스 생성var my1 =newMyClass();
개체의 새로운 인스턴스는 해당 개체에 할당된 프로토타입의 동작을 "상속"받습니다. 새로운 객체를 prototype 에 대입하면 대입된 객체가 부모가 됨
예를 들어 가장 큰 배열 요소의 값을 반환하는 Array 개체에 메서드를 추가하려면 함수를 선언하고 이를 Array.prototype에 추가한 후 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function array_max( )
{
var i, max =this[0]; //이때 this 는 자기자신 배열의 원소들을 가져오기위한 용도로 사용된다
for (i =1; i < this.length; i++)
{
if (max <this[i])
max =this[i];
}
return max;
}
Array.prototype.max = array_max;
var myArray =newArray(7, 1, 3, 11, 25, 9);
document.write(myArray.max());
// 결과 :
// 25
JavaScript는 클래스라는 개념이 없습니다. 그래서 기존의 객체를 복사하여(cloning) 새로운 객체를 생성하는 프로토타입 기반의 언어입니다. 프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어냅니다. 이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있습니다. 프로토타입은 객체를 확장하고 객체 지향적인 프로그래밍을 할 수 있게 해줍니다. 프로토타입은 크게 두 가지로 해석됩니다. 프로토타입 객체를 참조하는 prototype 속성과 객체 멤버인 proto 속성이 참조하는 숨은 링크가 있습니다. 이 둘의 차이점을 이해하기 위해서는 JavaScript 함수와 객체의 내부적인 구조를 이해 해야합니다. 이번 글에서는 JavaScript의 함수와 객체 내부 구조부터 시작하여 프로토타입에 대해 알아보겠습니다.
1. 함수와 객체의 내부 구조
JavaScript에서는 함수를 정의하고, 파싱단계에 들어가면, 내부적으로 수행되는 작업이 있습니다. 함수 멤버로 prototype 속성이 있습니다. 이 속성은 다른 곳에 생성된 함수이름의 프로토타입 객체를 참조합니다. 프로토타입 객체의 멤버인 constructor 속성은 함수를 참조하는 내부구조를 가집니다. 아래의 그림 1과 같이 표현합니다.
tip : 이미지상 오른쪽을로 '프로토타입 객체' 라고 하는데, '프로토타입 객체'라는 두 단어가 각각 다른것을 의미하는 것이 아니고 두단어가 합쳐져 프로토타입객체 라고 칭하고있음
function Person(){}
[소스 1]
속성이 하나도 없는 Person이라는 함수가 정의되고, 파싱단계에 들어가면, Person 함수 Prototype 속성은 프로토타입 객체를 참조합니다. 프로토타입 객체 멤버인 constructor 속성은 Person 함수를 참조하는 구조를 가집니다. 여기서 알아야 하는 부분은 Person 함수의 prototype 속성이 참조하는 프로토타입 객체는 new라는 연산자와 Person 함수를 통해 생성된 모든 객체의 원형이 되는 객체입니다. 생성된 모든 객체가 참조한다는 것을 기억해야 합니다. 아래의 그림 2와 같이 표현합니다.
function Person(){}
var joon = new Person();
var jisoo = new Person();
[소스 2]
JavaScript에서는 기본 데이터 타입인 boolean, number, string, 그리고 특별한 값인 null, undefined 빼고는 모두 객체입니다. 사용자가 정의한 함수도 객체이고, new라는 연산자를 통해 생성된 것도 객체입니다. 객체 안에는 proto(비표준) 속성이 있습니다. 이 속성은 객체가 만들어지기 위해 사용된 원형인 프로토타입 객체를 숨은 링크로 참조하는 역할을 합니다.
2. 프로토타입 객체란?
함수를 정의하면 다른 곳에 생성되는 프로토타입 객체는 자신이 다른 객체의 원형이 되는 객체입니다. 모든 객체는 프로토타입 객체에 접근할 수 있습니다. 프로토타입 객체도 동적으로 런타임에 멤버를 추가할 수 있습니다. 같은 원형을 복사로 생성된 모든 객체는 추가된 멤버를 사용할 수 있습니다.
function Person(){}
var joon = new Person();
var jisoo = new Person();
Person.prototype.getType = function (){
return "인간";
};
console.log(joon.getType()); // 인간
console.log(jisoo.getType()); // 인간
[소스 3]
위 소스 3 6라인은 함수 안의 prototype 속성을 이용하여 멤버를 추가하였습니다. 프로토타입 객체에 getType()이라는 함수를 추가하면 멤버를 추가하기 전에 생성된 객체에서도 추가된 멤버를 사용할 수 있습니다. 같은 프로토타입을 이용하여 생성된 joon과 jisoo 객체는 getType()을 사용할 수 있습니다.
여기서 알아두어야 할 것은 프로토타입 객체에 멤버를 추가, 수정, 삭제할 때는 함수 안의 prototype 속성을 사용해야 합니다. 하지만 프로토타입 멤버를 읽을 때는 함수 안의 prototype 속성 또는 객체 이름으로 접근합니다.
joon.getType = function (){
return "사람";
};
console.log(joon.getType()); // 사람
console.log(jisoo.getType()); // 인간
jisoo.age = 25;
console.log(joon.age); // undefined
console.log(jisoo.age); // 25
[소스 4]
위 소스 4 1라인은 joon 객체를 이용하여 getType() 리턴 값을 사람으로 수정하였습니다. 그리고 joon과 jisoo에서 각각 getType()을 호출하면 joon 객체를 이용하여 호출한 결과는 사람으로 출력되고, jisoo로 호출한 결과는 인간이 출력됩니다. joon 객체를 사용하여 getType()을 호출하면 프로토타입 객체의 getType()을 호출한 것이 아닙니다. joon 객체에 추가된 getType()을 호출한 것입니다. 프로토타입 객체의 멤버를 수정할 경우는 멤버 추가와 같이 함수의 prototype 속성을 이용하여 수정합니다.
Person.prototype.getType = function (){
return "사람";
};
console.log(jisoo.getType()); // 사람
[소스 5]
소스 5를 보게 되면 함수의 prototype 속성을 이용하여 getType() 리턴 값을 사람으로 수정합니다. 그리고 jisoo 객체를 이용하여 호출한 결과 사람이 나옵니다.
결론을 내리면, 프로토타입 객체는 새로운 객체가 생성되기 위한 원형이 되는 객체입니다. 같은 원형으로 생성된 객체가 공통으로 참조하는 공간입니다. 프로토타입 객체의 멤버를 읽는 경우에는 객체 또는 함수의 prototype 속성을 통해 접근할 수 있습니다. 하지만 추가, 수정, 삭제는 함수의 prototype 속성을 통해 접근해야 합니다.
3. 프로토타입이란?
JavaScript에서 기본 데이터 타입을 제외한 모든 것이 객체입니다. 객체가 만들어지기 위해서는 자신을 만드는 데 사용된 원형인 프로토타입 객체를 이용하여 객체를 만듭니다. 이때 만들어진 객체 안에 __proto__ (비표준) 속성이 자신을 만들어낸 원형을 의미하는 프로토타입 객체를 참조하는 숨겨진 링크가 있습니다. 이 숨겨진 링크를 프로토타입이라고 정의합니다.
function Person(){}
var joon = new Person();
[소스 6]
위 그림 6 joon 객체의 멤버인 __proto__ (비표준) 속성이 프로토타입 객체를 가리키는 숨은 링크가 프로토타입이라고 합니다. 프로토타입을 크게 두 가지로 해석된다 했습니다.
함수의 멤버인 prototype 속성은 프로토타입 객체를 참조하는 속성입니다. 그리고 함수와 new 연산자가 만나 생성한 객체의 프로토타입 객체를 지정해주는 역할을 합니다. 객체 안의 __proto__(비표준) 속성은 자신을 만들어낸 원형인 프로토타입 객체를 참조하는 숨겨진 링크로써 프로토타입을 의미합니다.
JavaScript에서는 숨겨진 링크가 있어 프로토타입 객체 멤버에 접근할 수 있습니다. 그래서 이 프로토타입 링크를 사용자가 정의한 객체에 링크가 참조되도록 설정하면 코드의 재사용과 객체 지향적인 프로그래밍을 할 수 있습니다.
4. 코드의 재사용
코드의 재사용 하면 떠오르는 단어는 바로 상속입니다. 클래스라는 개념이 있는 Java에서는 중복된 코드를 상속받아 코드 재활용을 할 수 있습니다. 하지만 JavaScript에서는 클래스가 없는, 프로토타입 기반 언어입니다. 그래서 프로토타입을 이용하여 코드 재사용을 할 수 있습니다.
이 방법에도 크게 두 가지로 분류할 수 있습니다. classical 방식과 prototypal 방식이 있습니다. classical 방식은 new 연산자를 통해 생성한 객체를 사용하여 코드를 재사용 하는 방법입니다. 마치 Java에서 객체를 생성하는 방법과 유사하여 classical 방식이라고 합니다. prototypal 방식은 리터럴 또는 Object.create()를 이용하여 객체를 생성하고 확장해 가는 방식입니다. 두 가지 방법 중 JavaScript에서는 prototypal 방식을 더 선호합니다. 그 이유는 classical 방식보다 간결하게 구현할 수 있기 때문입니다. 밑의 예제 1 ~ 4번까지는 classical 방식의 코드 재사용 방법이고, 5번은 prototypal 방식인 Object.create()를 사용하여 코드의 재사용을 보여줍니다.
(1) 기본 방법
부모에 해당하는 함수를 이용하여 객체를 생성합니다. 자식에 해당하는 함수의 prototype 속성을 부모 함수를 이용하여 생성한 객체를 참조하는 방법입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){}
Korean.prototype = new Person();
var kor1 = new Korean();
console.log(kor1.getName()); // 혁준
var kor2 = new Korean("지수");
console.log(kor2.getName()); // 혁준
[소스 7]
위 소스 7을 보면 부모에 해당하는 함수는 Person입니다. 10라인에서 자식 함수인 Korean 함수 안의 prototype 속성을 부모 함수로 생성된 객체로 바꿨습니다. 이제 Korean 함수와 new 연산자를 이용하여 생성된 kor 객체의 __proto__속성이 부모 함수를 이용하여 생성된 객체를 참조합니다. 이 객체가 Korean 함수를 이용하여 생성된 모든 객체의 프로토타입 객체가 됩니다. kor1에는 name과 getName() 이라는 속성이 없지만, 부모에 해당하는 프로토타입객체에 name이 있습니다. 이 프로토타입객체의 부모에 getName()을 가지고 있어 kor1에서 사용할 수 있습니다. 이 방법에도 단점이 있습니다. 부모 객체의 속성과 부모 객체의 프로토타입 속성을 모두 물려받게 됩니다. 대부분의 경우 객체 자신의 속성은 특정 인스턴스에 한정되어 재사용할 수 없어 필요가 없습니다. 또한, 자식 객체를 생성할 때 인자를 넘겨도 부모 객체를 생성할 때 인자를 넘겨주지 못합니다. 그림 7 소스 하단 두 번째 줄에서 kor2 객체를 생성할 때 Korean 함수의 인자로 지수라고 주었습니다. 객체를 생성한 후 getName()을 호출하면 지수라고 출력될 거 같지만, 부모 생성자에 인자를 넘겨주지 않았기 때문에 name에는 default 값인 혁준이 들어있습니다. 객체를 생성할 때마다 부모의 함수를 호출할 수도 있습니다. 하지만 매우 비효율적입니다. 그래서 다음 방법은 이 방법의 문제점을 해결하는 방법을 알아보겠습니다.
(2) 생성자 빌려 쓰기
이 방법은 기본 방법의 문제점인 자식 함수에서 받은 인자를 부모 함수로 인자를 전달하지 못했던 부분을 해결합니다. 부모 함수의 this에 자식 객체를 바인딩하는 방식입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
Person.apply(this, arguments);
}
var kor1 = new Korean("지수");
console.log(kor1.name); // 지수
[소스 8]
위 소스 8 10라인을 보면 Korean 함수 내부에서 apply 함수를 이용합니다.
부모객체인 Person 함수 영역의 this를 Korean 함수 안의 this로 바인딩합니다. 함수를 호출하여 지정된 개체를 함수의 this 값으로 대체하고 지정된 배열을 함수의 인수로 대체합니다.
객체를 생성하고, name을 출력합니다. 객체를 생성할 때 넘겨준 인자를 출력하는 것을 볼 수 있습니다. 기본 방법에서는 부모객체의 멤버를 참조를 통해 물려받았습니다. 하지만 생성자 빌려 쓰기는 부모객체 멤버를 복사하여 자신의 것으로 만들어 버린다는 차이점이 있습니다. 하지만 이 방법은 부모객체의 this로 된 멤버들만 물려받게 되는 단점이 있습니다. 그래서 부모객체의 프로토타입 객체의 멤버들을 물려받지 못합니다. 위 그림 8 그림을 보시면 kor1 객체에서 부모객체의 프로토타입 객체에 대한 링크가 없다는 것을 볼 수 있습니다.
(3) 생성자 빌려 쓰고 프로토타입 지정해주기
이 방법은 방법 1과 방법 2 문제점들을 보완하면서 Java에서 예상할 수 있는 동작 방식과 유사합니다.
function Person(name) {
this.name = name || "혁준"; }
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
Person.apply(this, arguments);
}
Korean.prototype = new Person();
var kor1 = new Korean("지수");
console.log(kor1.getName()); // 지수
[소스 9]
위 소스 9 9라인에서 부모 함수 this를 자식 함수 this로 바인딩합니다. 11라인에서 자식 함수 prototype 속성을 부모 함수를 사용하여 생성된 객체로 지정했습니다. 부모객체 속성에 대한 참조를 가지는 것이 아닌 복사본을 통해 내 것으로 만듭니다. 동시에 부모객체의 프로토타입 객체에 대한 링크도 참조됩니다. 부모객체의 프로토타입 객체 멤버도 사용할 수 있습니다. 그림 7과 비교했을 때 kor1 객체에 name 멤버가 없는 반면 그림 9에서는 name 멤버를 가지고 있는 것을 확인할 수 있습니다. 그림 8과 비교했을 때는 프로토타입 링크가 부모 함수로 생성한 객체에 대해 참조도 하고 있습니다. 그리고 부모 객체의 프로토타입 객체도 링크로 연결된 것을 볼 수 있습니다. 이 방법에도 문제점이 있습니다. 부모 생성자를 두 번 호출합니다. 생성자 빌려 쓰기 방법과 달리 getName()은 제대로 상속되었지만, name에 대해서는 kor1 객체와 부모 함수를 이용하여 생성한 객체에도 있는 것을 볼 수 있습니다.
(4) 프로토타입공유
이번 방법은 부모 생성자를 한 번도 호출하지 않으면서 프로토타입 객체를 공유하는 방법입니다.
function Person(name) {
this.name = name || "혁준";
}
Person.prototype.getName = function(){
return this.name;
};
function Korean(name){
this.name = name;
}
Korean.prototype = Person.prototype;
var kor1 = new Korean("지수");
console.log(kor1.getName()); // 지수
[소스 10]
위 소스 10 12라인에서 자식 함수의 prototype 속성을 부모 함수의 prototype 속성이 참조하는 객체로 설정했습니다. 자식 함수를 통해 생성된 객체는 부모 함수를 통해 생성된 객체를 거치지 않고 부모 함수의 프로토타입 객체를 부모로 지정하여 객체를 생성합니다. 부모 함수의 내용을 상속받지 못하므로 상속받으려는 부분을 부모 함수의 프로토타입 객체에 작성해야 사용자가 원하는 결과를 얻게 됩니다. 그림 9와 비교했을 때 중간에 부모 함수로 생성한 객체가 없고 부모 함수의 프로토타입 객체로 링크가 참조되는 것을 볼 수 있습니다.
(5) prototypal한 방식의 재사용
이 방법은 Object.create()를 사용하여 객체를 생성과 동시에 프로토타입객체를 지정합니다. 이 함수는 첫 번째 매개변수는 부모객체로 사용할 객체를 넘겨주고, 두 번째 매개변수는 선택적 매개변수로써 반환되는 자식객체의 속성에 추가되는 부분입니다. 이 함수를 사용함으로 써 객체 생성과 동시에 부모객체를 지정하여 코드의 재활용을 간단하게 구현할 수 있습니다.
var person = {
type : "인간",
getType : function(){
return this.type;
},
getName : function(){
return this.name;
}
};
var joon = Object.create(person);
joon.name = "혁준";
console.log(joon.getType()); // 인간
console.log(joon.getName()); // 혁준
[소스 11]
위 소스 1라인에서 부모 객체에 해당하는 person을 객체 리터럴 방식으로 생성했습니다. 그리고 11라인에서 자식 객체 joon은 Object.create() 함수를 이용하여 첫 번째 매개변수로 person을 넘겨받아 joon 객체를 생성하였습니다. 한 줄로 객체를 생성함과 동시에 부모객체의 속성도 모두 물려받았습니다. 위의 1 ~ 4번에 해당하는 classical 방식보다 간단하면서 여러 가지 상황을 생각할 필요도 없습니다. JavaScript에서는 new 연산자와 함수를 통해 생성한 객체를 사용하는 classical 방식보다 prototypal 방식을 더 선호합니다.
자바스크립트의 프로토타입(Prototype) 프로퍼티 란?
모든 함수 객체의 Constructor는 prototype 이란 프로퍼티를 가지고 있다. 이 prototype 프로퍼티는 객체가 생성될 당시 만들어지는 객체 자신의 원형이될 prototype 객체를 가리킨다. 즉 자신을 만든 원형이 아닌 자신을 통해 만들어질 객체들이 원형으로 사용할 객체를 말한다. prototype object는 default로 empty Object 를 가리킨다.
이 말이 매우 어렵게 들릴수도 있다. 하지만 정확히 이해한다면 그리 어려운 말이 아니다. 위에서 분명히 프로토타입은 자기 자신을 생성하게 한 자신의 원형 객체라고 정의했다. 그럼 그 원형객체란 프로토타입은 function A() 함수객체 그 차체일가? 전혀 그렇지 않다.
자바스크립트의 모든 객체는 생성과 동시에 자기자신이 생성될 당시의 정보를 취한 Prototype Object 라는 새로운 객체를 Cloning 하여 만들어낸다. 프로토타입이 객체를 만들어내기위한 원형이라면 이 Prototype Object 는 자기 자신의 분신이며 자신을 원형으로 만들어질 다른 객체가 참조할 프로토타입이 된다. 즉 객체 자신을 이용할 다른 객체들이 프로토타입으로 사용할 객체가 Prototype Object 인 것이다. 즉 위에서 언급한 __proto__라는 prototype 에 대한 link는 상위에서 물려받은 객체의 프로토타입에 대한 정보이며 prototype 프로퍼티는 자신을 원형으로 만들어질 새로운 객체들 즉 하위로 물려줄 연결에 대한 속성이다.
functionfoo(){}varfoo = newfoo();
위 예제코드를 통해 만들어지는 Prototype Link 와 Prototype Property가 가리키는 Prototype Object의 Real Link 에 대한 관계도는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
//#예제 2.
var A =function () { };
A.x=function() {
console.log('hello');
};
A.prototype.x =function () {
console.log('world');
};
var B =new A();
var C =new A();
B.x(); // 'world'
C.x(); // 'world'
예제2 에서의 결과가 world 가 되는 이유도 같은 이유다. A.prototype 은 A의 Prototype Object를 참조하는 녀석이기 때문에 A.prototype.x 를 정의한다는 것은 A의 Prototype Object를 직접 이용하게 되는 것이고 그에 따라서 A의 Prototype Object를 프로토타입으로 이용하여 만들어지는 B,C 가 알고 있는 x 는 function () {console.log('world');} 가 되는 것이다.
객체는 new Object(), Object.create() 또는 literal 표기법 (initializer 표기법)을 사용하여 초기화될 수 있습니다. 객체 초기자(object initializer)는 0개 이상인 객체 속성명 및 관련값 쌍 목록입니다, 중괄호({})로 묶인.
1
2
3
4
5
6
7
8
9
10
11
var o = {};
var o = { a: "foo", b: 42, c: {} };
var a ="foo", b =42, c = {};
var o = { a: a, b: b, c: c };
var o = {
property: function ([parameters]) {},
get property() {},
set property(value) {},
};
설명
객체 초기자는 Object의 초기화를 기술하는 식(expression)입니다. 객체는 속성으로 구성됩니다, 객체를 설명하는 데 사용되는. 객체의 속성값은 primitive 데이터 형 또는 다른 객체를 포함할 수 있습니다.
객체 생성
속성이 없는 빈 객체는 다음과 같이 만들 수 있습니다:
var object ={};
리터럴(literal) 또는 초기자(initializer) 표기법의 이점은, 빠르게 중괄호 내 속성이 있는 객체를 만들 수 있다는 것입니다. 당신은 그저 쉼표로 구분된 키: 값 쌍 목록을 표기합니다.
다음 코드는 키가 "foo", "age" 및 "baz"인 세 속성이 있는 객체를 만듭니다. 이들 키값은 문자열 "bar", 숫자 42 그리고 세 번째 속성은 그 값으로 다른 객체를 갖습니다.
var object ={
foo:"bar",
age:42,
baz:{ myProp:12},}
javascript : 함수와 객체, new
1
2
3
4
5
6
var n =newNumber();
var s =3;
console.log(typeof n); //대문자 Number 는 정수를 담는 객체, typeof 로 확인 되는 Number 타입 문자열은 object
console.log(typeof s); //변수가 숫자로 정의 될때의 타입은 소문자 문자열로 "number"
결과
object
number
함수가 생성자가 되는 시점
1
2
3
4
5
6
function Person() { //Person 함수 이지만
};
var p =new Person(); //new 함수명을 쓰게 되면 이때 Person 은 생성자가된다
console.log(p); //즉 객체를 만든것
//즉 Person 에 인자를 추가 하게 되면 객체 생성시 생성자 인수를 넘길 수 있는 형태가됨
//단 생성자를 통한 초기화가 이뤄짐으로 멤버 변수에 인자 값을 대입하려면
//this.멤버변수 = 매개변수; 의 형태인 this. 이 들어가야한다
일반 함수 앞에 new 를 붙여 생성하면 이 함수가 생성자가 된다
즉 객체를 생성한다는 것이 javascript 에서 custom 형 객체를 생성하는 법인데
javascript 에선 함수 자체에 this 가 들어가 있다는 것 자체가 원래 함수라기 보단 객체라는 점을 내포하고 있다라고 볼 수 있는 부분이기 대문에
함수가 생성자로 취급 되는 대목이 그렇게 생소하게 와닿지는 않는 부분이긴하다
전역객체와 객체지향 프로그래밍
전역 this 는 전역객체 window 와 동일한 객체를 말한다
즉 this.funcThis === window.funcThis 는 true 를 리턴한다
Func() 가 생성자로 호출 될때 funcThis 변수는 o2 에 대한 this 를 담는 것임으로
funcThis === o2 의 결과는 true 가 된다
그러나 아래의 결과는 false 를 보이는데 전역 왜냐하면 this 와 o2에 대한 this===funcThis 는 서로 다른객체임으로
funcThis === this;
false
이런 결과가 나타남
p.s javascript 가 추구하는 방향을 이해하지 못하는것은 아니지만 그러나 항상 javascript 는 생각 이상으로 이것저것 모아놓은 느낌을 들게한다
일부 ref : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer
일부 ref : https://opentutorials.org/module/532/6571
arguments 개체는 명시적으로 만들 수 없습니다.arguments 개체는 함수가 실행을 시작할 때만 사용할 수 있습니다.함수의 arguments 개체는 배열이 아니지만 배열 요소에 액세스하는 방식과 동일하게 각각의 인수에 액세스할 수 있습니다.n 인덱스는 실제로 arguments 개체의 0n 속성 중 하나에 대한 참조입니다.
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
function ArgTest(a, b) {
var s ="";
s +="Expected Arguments: "+ArgTest.length; // 2 : 함수 인자 개수
s +="\n";
s +="Expected Function.arguments: "+ArgTest.arguments.length; //4 : 총 매개변수 개수
s +="\n";
s +="Passed Arguments: "+arguments.length; //4 : 총 매개변수 개수