소켓이란?
OS는 User(사용자)영역과 Kernel(커널)영역으로 나뉘는데
OS 커널 영역에 구현되어 있는 프로토콜 요소(TCP, IP ..등)에 대한 추상화된 인터페이스를 소켓이라 부른다.
윈도우에서 메모장에 글을 쓰고 저장할 때 물리적으로 디스크에 저장이 되는데 디스크에 저장이 되기 전에 사용자와 커널 영역 사이에서 파일 형태라는 추상화된 정보 단위로 묶어진다.
여기서 말하는 파일 형태는 사용자 영역과 커널영역간의 데이터를 주고 받기 위한 추상화된 인터페이스를 의미한다.
(폴더는 윈도우에서 제공하는 것으로 데이터를 트리 구조로 분류하는 것이며 파일과는 다름)
H/W 단에 있는 디스크(SSD 또는 HDD)에 데이터를 저장하기 위해 시스템 콜을 통해 커널 모드로 전환이 되서 저장이 된 다음에 사용자 모드로 전환이 됨
만약에 데이터를 저장 할 때 파일 형태가 아니라 프로토콜(TCP or IP)라는 것을 사용하면 소켓이라고 부름
소켓 통신 코드로 구현
1. 서버단 소켓 통신
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore {
class Program
{
static void Main(string[] args)
{
// DNS 사용 (IP 주소를 하드코딩하면 추후에 IP주소가 바뀌는 경우 수동으로 바꿔줘야 함)
// www.google.com -> 이렇게 도메인 이름만 고정시키면 IP주소가 바껴도 DNS가 변경되지 않는 한 문제가 없음
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
// DNS에 해당되는 IP가 여러개 있을 수도 있기 때문에 배열형태로 값을 리턴한다.
// 사용자가 많이 접속하는 사이트의 경우 트래픽 제어를 하기 위해서 IP 주소를 여러개 두기도 함
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 7777);
// 1. 소켓준비
Socket listenSocket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 2. 소켓에 서버주소랑 Port 번호 Bind
listenSocket.Bind(ipEndPoint);
// 3. Listen (서버에 클라이언트가 접속할 수 있도록 문을 열어줌)
// 인자값으로 들어가는 backlog 수의 의미는? 최대 대기수(여러명의 클라가 한번에 서버에 접속하는 경우 대기하도록 조절)
listenSocket.Listen(10);
// 4. Accept
while (true)
{
Console.WriteLine("Listening ...");
// 클라이언트가 서버에 접속하려고 하면 Accpet(연결 수락)을 한다.
// 연결이 되면 서버는 클라이언트랑 통신할수 있는 소켓을 반환
Socket socket = listenSocket.Accept();
// 클라이언트로 부터 데이터 수신
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[FROM Client] {recvData}");
// 클라이언트에게 데이터 송신
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to Server");
socket.Send(sendBuff);
// 통신 끊기
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
2. 클라이언트 소켓 통신
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient {
class Program {
static void Main(string[] args) {
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 7777);
// 1. 서버와 통신을 하기 위한 소켓을 생성
Socket socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// 2. 서버와 연결 시도
socket.Connect(ipEndPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
// 서버로 데이터 송신
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World");
int sendBytes = socket.Send(sendBuff);
// 서버로 부터 데이터 수신
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[FROM Server] {recvData}");
// 연결종료
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
서버단 결과)
클라이언트가 서버한테 접속 요청을 하면 서버쪽 대기(Listen) 소켓에서 해당 클라이언트와 통신할 수 있는 소켓을 반환시켜서 그 소켓을 통해 통신을 하는 방식
서버의 경우 클라이언트와 통신이 종료되도 다른 클라이언트와 통신을 하기 위한 준비를 계속 하고 있음
클라이언트단 결과)
통신을 하기 위한 서버의 IP주소와 포트번호로 접속 요청을 보내 서버쪽에서 통신 연결을 허락해주면
클라이언트는 서버와 통신하기 위해 설정된 소켓으로 통신을 한다.
서버는 클라이언트가 통신 요청을 할 때마다 해당 클라이언트와 통신하기 위한 소켓을 계속 만들어야 되고
클라이언트는 해당 서버와 통신할 소켓 하나만 있으면 된다.
NonBlocking 방식을 이용한 소켓 통신
Accept, Receive, Send 계열의 블로킹 함수는 현재 작업이 종료될 때까지 다른 작업으로 넘어 갈 수 없기 때문에 치명적인문제가 발생할 수 있다.
예를 들어 몇만명의 유저가 소켓 통신을 하는 경우 첫번째 유저가 작업을 끝내기 전까지는 다른 유저들은 계속 기다려야 하기 때문에 논블로킹을 계열의 함수를 사용하는 것이 좋다.
논블로킹을 사용한 서버단 코드)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Listener
{
Socket listenSocket;
// Accept가 된 이후 콜백
Action<Socket> onAcceptHandler;
public void Init(IPEndPoint ipEndPoint, Action<Socket> onAcceptHandler) {
// 1.서버단 listen 소켓 생성
listenSocket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(ipEndPoint);
listenSocket.Listen(10);
// 2.delegate를 이용해서 Init 함수가 종료되고 콜백 함수로 실행될 onAcceptHandler 함수 추가
this.onAcceptHandler += onAcceptHandler;
// 3.소켓 비동기 이벤트 객체 생성
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
// delegate로 OnAcceptCompleted 함수 추가
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
// 클라이언트가 서버에 접속하려고 하면 Accpet(연결 수락)을 해주는 함수
RegisterAccept(args);
}
public void RegisterAccept(SocketAsyncEventArgs args) {
// 다른 클라이언트로 부터 Accpet 처리를 할 때 이전에 Accpet 처리를 했던 적이 있다면 소켓데이터가 남아있기 때문에
// Accpet 요청을 하기 전에 데이터를 null로 지워준다.
args.AcceptSocket = null;
// pending이 false -> 서버가 클라이언트에게 Accept 처리를 해준경우
// pending이 true -> Accept 대기 중인 경우
bool pending = listenSocket.AcceptAsync(args);
if (pending == false) {
OnAcceptCompleted(null, args);
}
}
public void OnAcceptCompleted(object sender, SocketAsyncEventArgs args) {
// 소켓 에러가 없는 경우에 한해서 실행
if (args.SocketError == SocketError.Success)
{
// Accept 이후 받아온 소켓을 onAcceptHandler 콜백 함수 인자에 클라이언트와 통신할 소켓 저장
this.onAcceptHandler.Invoke(args.AcceptSocket);
}
else // 소켓 에러가 있으면 에러메세지 반환
{
Console.WriteLine(args.SocketError.ToString());
}
// 다음 클라이언트가 대기하고 있는 경우를 위해서 Accept를 호출
RegisterAccept(args);
}
}
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore {
class Program
{
static Listener listener = new Listener();
static void OnAcceptHandler(Socket clientSocket) {
try
{
// 데이터 수신
byte[] recvBuff = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
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);
}
}
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 7777);
// Init 함수가 종료되면 콜백함수로 OnAcceptHandler 함수 실행
listener.Init(ipEndPoint, OnAcceptHandler);
Console.WriteLine("Listening ...");
while (true)
{
;
}
}
}
}
Init 메서드와 RegisterAccept 메서드를 보면
"SocketAsyncEventArgs"(소켓 비동기 이벤트)에 "completed" 이벤트를 이용해서 콜백 delegate(대리자)함수를 추가하는고 "AcceptAsync()" 호출시 Accept가 대기 중인 경우 true 값을 반환하는데 이후 대기중인 작업이 완료되면 completed 이벤트에 추가된 콜백 delegate가 발생한다.
여기서 대기중인 작업이 완료됬다는 뜻은 Accpet가 완료됬다는 의미임
OnAcceptCompleted 메서드에서는
Accpet() 작업이 완료되고 소켓에 문제가 없을 경우 Init 함수가 끝나고 실행될 콜백 함수의 인자에 클라이언트와 통신할 소켓을 넣어준다.
그리고 다음 클라이언트가 서버와 연결을 시도하는 경우에 대비해서 Accept를 호출한다.
AcceptAsync() 함수의 공식문서를 확인해보면 해석하기가 생각보다 쉽지 않다..
delegate에 대한 참고 : https://m.blog.naver.com/isaac7263/222162479809
'게임개발 > 게임서버' 카테고리의 다른 글
멀티 쓰레드 프로그래밍 (0) | 2024.04.17 |
---|---|
멀티쓰레드 Lock 구현 방식 (2) | 2024.01.15 |
TLS(Thread Local Storage) (0) | 2023.10.28 |
ReadWriteLock (0) | 2023.10.26 |
교착 상태(Deadlock) (0) | 2023.10.17 |