소켓 프로그래밍에 대해 처음으로 구현을 했다. 순서가 정해져 있다보니 과정에 이해가 쉬웠다.
- 서버: 생성 -> 결합 -> 대기 -> 승인 -> 통신 -> 종료
- 클라이언트: 생성 -> 연결 요청 -> 통신 -> 종료
지난 시간에 구현한 소켓 프로그래밍의 경우 코드가 모두 메인 함수 내에서 작동을 하며 지저분하고, 관리하기에 어려운 구조로 되어있다.
객체지향의 장점을 살리고자 이를 개선하는 작업을 우선 하고자 한다. 우선 새로운 Class, Listner라는 Class를 추가한다.
솔루션 탐색기 -> 마우스 우측 -> 추가 -> 새항목
서버와 클라가 연결될 때 사용할 소켓 생성, 결합, 대기, 승인까지의 작업을 Listen에서 처리를 한다.
(메시지를 주고 받는 부분은 제외)
Block 함수로 사용되던 부분을 Non-Block 형식으로 변경한다.
➕ 부가설명
지나니간에 사용했던Accept(), Send(), Receive()
함수들은 모두 블로킹 함수라한다. 이러한 유형의 함수들은 해당 동작이 처리 되지 않으면 다음으로 넘어가질 못한다.
Accept()는 어떻게든 사용한다 할 수 있지만, Send와 Receive 같은 경우는 많은 유저와 계속 소통을 해야할텐데 넘어가지 못하면 굉장히 피곤한 상황이 생길 수 있다.
그렇기에 이 부분이 처리되지 않더라도, 다른 행동을 하다가 올 수 있도록 비동기 형식, 콜백 형식을 사용할 것이다.
Init() 이라는 함수를 통해 처음 시작할 때 사용 되던 생성, 결합, 대기를 하도록 구현한다. (승인은 뒤에서 따로 처리 할 예정)
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog = 최대 대기수(매개변수) 초과 시 fail 뜸
_listenSocket.Listen(10);
}
AcceptAsync()
라는 비동기 계열의 함수를 사용하게 되면 성공을 하던, 실패를 하던 바로 bool 값으로 return을 해준다.
하지만 실패를 했을 경우는 따로 알려주도록 구현을 해주어야하기에 조금 더 구현 난이도가 상승한다.
우선 SocketAsyncEventArgs
이라는 매개변수를 사용한 방식으로 진행 할 것이다. 이 친구는 소켓 비동기 처리를 할 때 여러 방면에서 많은 도움이 되는 친구이다.
void RegisterAccept(SocketAsyncEventArgs args)
{
_listenSocket.AcceptAsync(args);
}
Accept 과정이 성공을 하면 내부적으로 승인이되며 통신 가능한 상태가 된다. 하지만 실패 했을 경우를 위한 처리가 필요하다.
bool pending = _listenSocket.AcceptAsync(args)
이 과정에서 fail이 뜬다면 두 가지 경우가 있다.
위 두가지 상황에서도 일단 다음 단계로 넘겨서 실제로 Error 상황인지 체크하며 어떤 일을 할지 구분해준다.
void RegisterAccept(SocketAsyncEventArgs args)
{
bool pending = _listenSocket.AcceptAsync(args);
if(pending == false)
{
// 다음 단계로~
OnAcceptComplete(null, args);
}
}
OnAcceptComplete는 이벤트로 등록하여 실행 시켜줄 것이기에 위에서 성공하던, 실패하던 이 구역으로 들어오게된다.
에러가 없다면 메인에서 메시지 송수신하는 함수를 동작 시킨다. (이 콜백함수는 Init시 매개변수로 받아와 등록됨)
// Object sender는 해당 형식을 맞추기 위함
void OnAcceptComplete(Object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
// todo 유저가 들어오면 뭘 해야할지
// 콜백 방식으로 다음 행동 동작(뒤에서 설명)
// client 소켓으로 뱉어준 부분을 이걸로 대신 처리
_onAcceptHandler.Invoke(args.AcceptSocket);
}
else
Console.WriteLine(args.SocketError.ToString()); ;
// 뭐든 일이 끝났으니 다음 턴을 위해 다시 넣어주기
RegisterAccept(args);
}
에러가 난다면, 출력을 해주고 위에 과정이 뭐가 됐던 끝이나면 RegisterAccept(args)
을 다시 실행 시켜준다. 이때에 만약 args에 값이 들어있는 채로 함수를 실행시키면 문제가 생기기에 null 처리를 해준다.
SocketAsyncEventArgs
이 친구는 최초에 한 번은 실행해야 하며, 그후에는 위에서 처리한 것 같이. 다시 넣어주고, null 처리하며 재사용한다.
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog = 최대 대기수(매개변수) 초과 시 fail 뜸
_listenSocket.Listen(10);
**************** 포인트 ********************
// 최초 한 번은 args를 생성하고 이벤트 등록
// 그 후로는 계속 재사용 가능
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
// Completed 이벤트 핸들러(콜백) 형식으로 구동
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args);
}
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete)
이 부분에서 SocketAsyncEventArgs 타입에 맞는 형식을 등록하기 위해 위에서 사용하지 않는 Object Sender를 넣어준 것이다.
Action을 통해 송수신 업무를 처리 할 것이다.
우선 Server에서 Action을 선언한다. Action<Socket> _onAcceptHandler;
Init에서 매개변수로 받아서, 이를 넣어준다. 여기서 받게되는 Action 값이 메인에서 메시지를 송수신 하던 영역이다.
Send, Receive를 하던 코드를 OnAcceptEventHandler() 함수를 만들어 넣어주고, 이를 Init할 때 매개변수로 담아주어 action 할 수 있게 해준다.
위와 같은 처리가 모두 끝난후. Client 영역에서 접속하는 코드에 while 처리를 하여 여러번 시도하도록 해서 잘 접속하는 지 테스트를 해보면 정상 작동한다.