이전 포스팅을 참고해주세요.
https://dongsik-blog.tistory.com/96
이전 포스팅에서는 Server, Client스크립트를 만들어 서로 Socket 통신을 실시했습니다.
이번에는 Server코드에서 Socket을 초기화하고 작동하는 코드를 다른 클래스로 분리하고,
Accept()를 비동기처리 해보겠습니다.
1. Server스크립트에서 Listener 분리하기
Server에서 Listener클래스를 분리했습니다. 코드는 아래와 같습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace MainServer
{
class Listener
{
Socket _listenSocket;
// socket을 초기화하는 메서드
public void init(IPEndPoint endPoint)
{
// create socket
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// set socket
_listenSocket.Bind(endPoint);
// start
_listenSocket.Listen(10);
}
// client의 socket 정보를 담는 메서드
public Socket Accept()
{
// accept는 bloking계열의 메서드
// bloking : 스레드 대기
// non-bloking : 대기없이 메서드 진행하고, 이후 Callback함수로 new thread에서 메서드로 동작 진행
return _listenSocket.Accept();
}
}
}
using MainServer;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Main_Server
{
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// dns
string hostName = Dns.GetHostName();
IPHostEntry entry = Dns.GetHostEntry(hostName);
IPAddress ipAddr = entry.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 8081);
try
{
_listener.init(endPoint);
while (true)
{
Console.WriteLine("Listening...");
// client의 connection 요청 허가
Socket clientSocket = _listener.Accept();
// ========== client socket이 생겼으니 서로 대화(?)가능 ==========
// recive
byte[] buffer = new byte[1024];
int recvBytes = clientSocket.Receive(buffer);
string recvData = Encoding.UTF8.GetString(buffer, 0, recvBytes);
Console.WriteLine($"recv data : {recvData}");
// send
byte[] buffer2 = Encoding.UTF8.GetBytes("Welcome to dongsik server!");
clientSocket.Send(buffer2);
// get out!
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
2. 비동기 처리
Listener클래스의 _listenSocket.Accept();는 bloking함수입니다.
bloking함수는 client의 connet가 올때까지 쓰레드를 정지하는 문제가 발생합니다.
예를들면.. Accept();에서 Client의 Socket정보를 가져온 서버는 Client의 연결 요청이 올때까지 쓰레드는 하염없이 Client의 연락을 오매불망 기다립니다(정지 상태)
이러한 문제를 해결하기 위해서 비동기처리를 할 것입니다. 비동기 처리를 한다면 위와같은 문제가 생기더라도 쓰레드가 정지되지 않고 일단 실행이 완료되고, 이후에 Client가 연결 요청을 하면 새로운 쓰레드를 실행시켜 Client의 요구를 처리해줍니다.
이 부분은 이해가 힘들어서 이틀동안 강의를 10번을 반복했습니다. 만약 이해가 안된다면 Delegate, Action<>, Invoke를 다시 공부해보면 도움이 될 것이에요👀
Listener.cs
우선 코드 전문을 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace MainServer
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
// socket을 초기화하는 메서드
public void init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
// create socket
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler = onAcceptHandler;
// set socket
_listenSocket.Bind(endPoint);
// start
_listenSocket.Listen(10);
// 다회용
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); // 완료되면 OnAcceptCompled()를 호출
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// args가 재사용될 때 이전에 사용하던 클라이언트의 socket정보가 들어오면 안되니
// null로 초기화
args.AcceptSocket = null;
// pending : 계류
bool pending =_listenSocket.AcceptAsync(args); // 처리 결과에 상관 없이 우선 return 후 call back으로 재호출
if(pending == false) { OnAcceptCompleted(null, args); }
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
_onAcceptHandler.Invoke(args.AcceptSocket);
}
else
{
Console.WriteLine(args.SocketError.ToString());
}
RegisterAccept(args);
}
}
}
위에 작성했던 Listener.cs 코드와 달라진 부분이 있습니다.
Accept가 AcceptAsync가 되면서 비동기 관련 처리를 위한 메서드가 생겨났다는 점입니다.
Register, OnAcceptComplted는 각각 예약, 완료 정도로 해석하면 됩니다.
Socket이 초기화(Init)가 되면 Resigter를 호출하여 대기 등록을 합니다. 만약 pending상대가 아니라면 바로 OnAceeptCompled로 가서 cliend socket정보를 서버에게 넘겨주고 다음 동작을 실행 할 것입니다.
pending상태라면? 우선 Resigter메서드가 종료되고 대기하다가 args가 Compted상태가 된다면? 연결이 가능한 상태라는 의미이기 때문에 OnAceeptCompled를 실행시킵니다.
흐름이 보이나요?
초기화 -> register 등록 -> 계류하다가 차례가 되면 OnComplete에서 client의 socket정보를 서버에 넘겨줌 -> 할 일 끝났으니 Register 등록 -> 계류하다가 차례가 되면 OnComplete에서 client의 socket정보를 서버에 넘겨줌 -> 할 일 끝났으니 Register 등록 -> 계류하다가 차례가 되면 OnComplete에서 client의 socket정보를 서버에 넘겨줌 -> 할 일 끝났으니 Register 등록 -> ... 이 됩니다.
그럼 이제 _onAcceptHandler에서 Invoke로 호출하는 메서드로 이동하겠습니다.
Server.cs
using MainServer;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Main_Server
{
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
// recive
byte[] buffer = new byte[1024];
int recvBytes = clientSocket.Receive(buffer);
string recvData = Encoding.UTF8.GetString(buffer, 0, recvBytes);
Console.WriteLine($"recv data : {recvData}");
// send
byte[] buffer2 = Encoding.UTF8.GetBytes("Welcome to dongsik server!");
clientSocket.Send(buffer2);
// get out!
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
catch
{
}
}
static void Main(string[] args)
{
// dns
string hostName = Dns.GetHostName();
IPHostEntry entry = Dns.GetHostEntry(hostName);
IPAddress ipAddr = entry.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 8081);
_listener.init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
}
Listener에서 호출한 OnAcceptHandler가 작동합니다.
이제 클라이언트에서 지속적으로 호출하도록 코드를 변경 후 실행을 해보겠습니다.
Client.cs
using System.Net.Sockets;
using System.Net;
using System.Text;
namespace Client
{
class Program
{
static void Main(string[] args)
{
// dns
string hostName = Dns.GetHostName();
IPHostEntry entry = Dns.GetHostEntry(hostName);
IPAddress ipAddr = entry.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 8081);
while(true)
{
// socket
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// connection
socket.Connect(endPoint);
Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");
// send
byte[] buffer = Encoding.UTF8.GetBytes("Can I visite your server?");
int bufferSize = socket.Send(buffer);
// recive
byte[] buffer2 = new byte[1024];
int bufferSize2 = socket.Receive(buffer2);
string recvData = Encoding.UTF8.GetString(buffer2, 0, bufferSize2);
Console.WriteLine($"from server : {recvData}");
// out
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
// 0.1초마다 server에 요청 보내는거 확인
Thread.Sleep(1000);
}
}
}
}
결과 확인
자자잔,, 잘 됩니다.