세션 가상클래스로 만들어서 사용
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
/// <summary>
/// 추상class로 만들어서 상속받은 클래스로 사용하기
/// </summary>
///
abstract class Session
{
Socket _socket;
int _disconnected = 0;
List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs(); /// 재사용을 위해 전역변수로 선언
Queue<byte[]> _sendQueue = new Queue<byte[]>();
/// <summary>
/// 엔진과 컨텐츠를 분리하기위해 가상함수 만들엇다!!
/// </summary>
public abstract void OnConnected(EndPoint endpoint);
public abstract void OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
//bool _pending;
object _lock = new object();
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
///recvArgs.UserToken = 1;///아무거나 넘겨줘도 됨 this,숫자,아무거나 가능!! 식별자로 구분하고싶을때 사용
recvArgs.SetBuffer(new byte[1024],0,1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv(recvArgs);
}
/// <summary>
/// send는 recv 만큼 단순하지 않다
/// 뭘 보낼지 모르니까 필요할때 써야한다.
/// 많이 까다롭고 방법도 여러가지가 존재한다!
/// </summary>
/// <param name="sendBuff"></param>
public void Send(byte[] sendBuff)
{
///_socket.Send(sendBuff);
///
///문제 여기서 게속 생성해서 샌드하면 너무 손해다 (new)
lock (_lock)
{
///멀티스레드 이니까 락이 무조건 필요!!(여러명이 Queue에 접근못하게!)
_sendQueue.Enqueue(sendBuff);
if (_pendinglist.Count==0)///_pending == false
{
///처음 send를 호출한 상태(Register가능)
RegisterSend();
}
}
///매번 이렇게 호출하면 너무 비효율적! Queue에 담아서 될때만 호출해주자!
//_sendArgs.SetBuffer(sendBuff,0,sendBuff.Length);
//RegisterSend();
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
///Disconnected가 성공하면 OnDisconnected가 한번 실행될것이다!
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
///안좋은 예
///
///if (_socket == null)
/// return;
///_socket = null;
}
#region 네트워크 통신 (내부사용)
///Send부분
void RegisterSend()
{
///멀티스레드여도 RegisterSend자체가 Lock에서 호출되고 있으니까 lock을 걸필요는 없음
/// 심지어 이부분에서 sendAsync를 여러번 반복하면 부하가 너~~무 심하다!
/// _socket.SendAsync(args) 이것을 아무대나 막쓰는것은 문제가 심하다.
/// 뭉쳐서 보내야 한다!
//_pending = true;
//byte[] buff = _sendQueue.Dequeue();
//_sendArgs.SetBuffer(buff, 0, buff.Length);
/// _sendArgs.BufferList 한번에 리스트로 Async 콜을 줄일수있다.
/// SetBuffer와 같이 사용할순 없다(둘중 하나 선택해야함)
///_pendinglist.Clear();
while (_sendQueue.Count>0)
{
byte[] buff = _sendQueue.Dequeue();
///Add 하는 방식이 특이! ArraySegment 어떤 배열의 일부를 나타내는 구조체!
/// a[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ] c++은 포인터 개념이 있어서 포인터로 시작주소를 옮겨서 건내주면됨
/// 하지만 c#이라면 포인터가 없어서 무조건 첫 주소만 알수있음
/// 그래서 시작주소를 알수있게 보통 (buff, 0, buff.Length) 이런식으로 생겨있다.
//_sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
_pendinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendinglist;
bool pending = _socket.SendAsync(_sendArgs); ///전역변수 사용으로 함수 인자 하나 없애기!(꼬이기 방지)
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
///여기는 callback 방식으로 다른스레드에서 실행될수도 있으니 lock을 걸어줘야한다.
///_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted); <-- 이부분
/// 사실 잘 와닿지 않을수 있음
/// 멀티스레드 프로그램은 계속 크래시를 내보면서 해볼수 밖에 없음 그래야지 감각이 좋아짐
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
///recv와 다르게 보낸개 성공했을때 굳이 뭐 할게 없긴하다
///
///Send는 Receive처럼 시점이 정해져있지 않음
///Send 하기!
/// RegisterSend는 재사용 당연히 불가능!
_sendArgs.BufferList = null; ///필요는 없음 그냥 구분하기 위해서 넣어준거라고 보면됨
_pendinglist.Clear();
OnSend(_sendArgs.BytesTransferred);
///_pending 을 통과못한 큐에 쌓인 데이터 전부 처리해주기
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"On RecvCompleted Failed{e}");
}
}
else
{
Disconnect();
}
}
}
///비동기 방식으로 처리하려면
///두단계 필요!
///1.등록
///2.완료
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending= _socket.ReceiveAsync(args);
if (pending == false) ///완료됬으면 complete 호출!
OnRecvCompleted(null,args);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred >0 && args.SocketError == SocketError.Success)
{
try
{
///이제 성공적으로 recv를 받앗을때 OnRecv가 콜되게 만듬!!
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv(args);
}
catch (Exception e)
{
Console.WriteLine($"On RecvCompleted Failed{e}");
}
}
else
{
/// TODO Disconnect!!
Disconnect();
}
}
#endregion
}
}
메인 프로그램
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// 소켓 프로그래밍 기초
///
/// 메모장 코드를 메인안에 때려박아넣으면 안됨!
///어짜피 커질꺼니까 처음부터 분리하는 습관을 들여야함!
///
///
/// </summary>
namespace ServerCore
{
class GameSession : Session
{
/// <summary>
/// 컨텐츠 에서는 각자 세션을 상속받은 클래스에서 하고싶은것을 하면됨!
/// </summary>
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected :{endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG SERVER!");
Send(sendBuff);
Thread.Sleep(1000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected :{endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
/// 성공적으로 데이터 가져오기 완료
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"From Client[{recvData}]");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes:{numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
///static Session _Session = new Session();
static void Main(string[] args)
{
/// DNS (Domain Name System)
/// 172.1.2.3(실제로 넣으면 문제가됨!) 하드코딩 X!
/// 하지만 도메인 등록이라면 문제가 없다
/// www.rookiss.com -> 123.123.123.12
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
///혹시나 누가 들어오면 OnAcceptHandler에 전달해줘
_listener.Init(endPoint, ()=> { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
func를 이용해서 session클래스 사용
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
/// Accept가 완료되면 어떻게 처리할건지
///Action<Socket> _onAcceptHandler;
///
///GameSession session = new GameSession(); 을 사용하지않기위해
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
///문지기 만들기(핸드폰만들기)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory; /// func라서 리턴값이 있는 람다식을 받을 수 있음
///_listener.Init(endPoint, ()=> { return new GameSession(); }); 이런식으로 사용!
///문지기 교육
_listenSocket.Bind(endPoint);
///영업 시작
///backlog : 최대 대기수
/// 문지기가 안내하기 전까지 대기하는 수
/// 만약 이 수가 넘어가는 사람이 문의하면 바로 fail이 뜬다
_listenSocket.Listen(10);
/// 한번만 만들어주면 계속 재활용 할수있는 어마어마한 장점이 있음
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);///나중에라도 완료되면 여기 이벤트로 연락오게하기!
RegisterAccept(args);///최초로 낚시대를 강물에 던짐!
///여러개를 만들어도 상관없음
///
/* for (int i = 0; i < 10; i++)
{
/// 한번만 만들어주면 계속 재활용 할수있는 어마어마한 장점이 있음
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);///나중에라도 완료되면 여기 이벤트로 연락오게하기!
RegisterAccept(args);///최초로 낚시대를 강물에 던짐!
}*/
}
void RegisterAccept(SocketAsyncEventArgs args)
{
///중요!
///다시 돌아올때 비어있는 args여야한다!!!
args.AcceptSocket = null;
/// 비동기! 동시에 처리안되고 나중에 처리될수있음!!!
///성공하든 아니든 상관없이 바로 리턴을 하고 본다(문제가 될수있음)
///로그인에 실패했는데 리턴때릴수있음
///비동기 계열을 사용하면 대화를 해야함(결과에 대해서)
///
///당장 완료한다는 보장은 없고 요청을 하긴 할꺼다
///말그대로 등록을 한거라고 보면됨
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false)
{
///비동기로 실행했지만 운좋게 완료된 케이스
///낚시대를 강물에 던지자마자 물고기가 잡힌 케이스
OnAcceptCompleted(null,args);
}
else
{
///pending true
/// 나중에 args로 Completed 로 연락이 올것이다.
}
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
///주 스레드와 엮일수 있는 REDZONE이다!!!! 조심!!!!!!!!
///멀티스레드 주의~~~
if (args.SocketError == SocketError.Success)
{
///에러 없이 잘 처리 됫다(실제로 accept를 했다)
/// 유저가 커넥트 연결이 와서 실제로 accept까지 했다면
///TODO
///
/// <summary>
/// 다른이름의 세션이 될수도 있음
/// ex) MMOSession MasterSession 등등...무조건 GameSession일리는 없음!
/// </summary>
Session session = _sessionFactory.Invoke(); ///-> ()=> { return new GameSession();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
//_onAcceptHandler.Invoke(args.AcceptSocket);///완료됬으면 _onAccepHandler 실행!
}
else
{
Console.WriteLine( args.SocketError.ToString());
}
///모두 완료됬으니 다음 아이를 위해서 다시 등록!!
///다음 낚시대 던지기
RegisterAccept(args);
}
}
}