==================================================================
모든 출처는
- 유니티 개발자를 위한 C#으로 온라인 게임 서버 만들기 - 저자 이석현, 출판사 한빛미디어
그리고 URL :
==================================================================
C#으로 게임 서버 만들기 - 2. 접속 처리 및 버퍼 풀링 기법
1-4-1. 클라이언트 접속 이후의 처리
CNetworkService 세부 구현 내용
Accept처리가 완료되었을 때 on_new_client델리게이트를 호출해주는 부분까지가 CListener 클래스의
역할이었습니다. 이제 CNetworkService 클래스에 대해 좀 더 자세히 들어가보도록 하겠습니다.
앞의 CNetworkService 클래스에는 네트워크 기반이 되는 코드가 들어갑니다.
대표적인 기능은 다음과 같습니다
CListener를 생성하여 클라이언트의 접속 처리
SocketAsyncEventArgs 객체를 풀링하여 재사용 가능하도록 구현
메시지 송수신 버퍼를 풀링하는 매니저 클래스 구현
listener 생성 부분입니다.
1 2 3 4 5 6 | public void listen(string host, int port, int backlog) { this.client_listener = new CListener(); this.client_listener.callback_on_newclient += on_new_client; this.client_listener.start(host, port, backlog); } | cs |
listener 생성 부분입니다.
서버의 host, port, backlog 값을 전달 받아 listener를 생성한 뒤 start 메소드로 접속을 기다립니다.
SocketAsyncEventArgs 풀링 구현
소켓별로 두 개의 SocketAsyncEventArgs가 필요합니다.
하나는 전송용, 다른 하나는 수신용입니다.
SocketAsyncEventArgs마다 버퍼를 필요로 하는데 결국 하나의 소켓에 전송용 버퍼 한개,
수신용 버퍼 한개 총 두 개의 버퍼가 필요하게 됩니다.
먼저 SocketAsyncEventArgs를 어떻게 풀링하여 사용하는지 알아보겠습니다.
1 2 3 4 5 6 7 | public class CNetworkService { // 메시지 수신용 풀 SocketAsyncEventArgsPool receive_event_args_pool; // 메시지 SocketAsyncEventArgsPool send_event_args_pool; } | cs |
전송용, 수신용 풀 객체를 각각 선언합니다.
풀링에 사용되는 클래스는 SocketAsyncEventArgsPool입니다.
이 코드는 msdn에 있는 샘플 코드 그대로입니다.
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // Represents a collection of reusable SocketAsyncEventArgs objects. class SocketAsyncEventArgsPool { Stack<SocketAsyncEventArgs> m_pool; // Initializes the object pool to the specified size // // The "capacity" parameter is the maximum number of // SocketAsyncEventArgs objects the pool can hold public SocketAsyncEventArgsPool(int capacity) { m_pool = new Stack<SocketAsyncEventArgs>(capacity); } // Add a SocketAsyncEventArg instance to the pool // // The "item" parameter is the SocketAsyncEventArgs instance // to add to the pool public void Push(SocketAsyncEventArgs item) { if (item == null) { throw new ArgumentNullException("Items added to a SocketAsyncEventArgsPool cannot be null"); } lock (m_pool) { m_pool.Push(item); } } // Removes a SocketAsyncEventArgs instance from the pool // and returns the object removed from the pool public SocketAsyncEventArgs Pop() { lock (m_pool) { return m_pool.Pop(); } } // The number of SocketAsyncEventArgs instnaces in the pool public int Count { get { return m_pool.Count; } } } | cs |
코드는 굉장히 간단합니다.
객체를 담을 수 있는 Stack을 생성해 Pop / Push 를 통해 꺼내거나 반환하는 작업을 수행합니다.
1 2 | this.receive_event_args_pool = new SocketAsyncEventArgsPool(this.max_connections); this.send_event_args_pool = new SocketAsyncEventArgsPool(this.max_connections); | cs |
최대 동접 수치만큼 생성합니다.
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 26 | for (int i = 0; i < this.max_connections; i++) { CUserToken token = new CUserToken(); // receive pool { // Pre-allocate a set of reusable SocketAsyncEventArgs arg = new SocketAsyncEventArgs(); arg.Completed += new EventHandler<SocketAsyncEventArgs>(receive_completed); arg.UserToken = token; // add SocketAsyncEventArg to the pool this.receive_event_args_pool.Push(arg); } // send pool { // Pre-allocate a set of reusable SocketAsyncEventArgs arg = new SocketAsyncEventArgs(); arg.Completed += new EventHandler<SocketAsyncEventArgs>(send_completed); arg.UserToken = token; // add SocketAsyncEventArg to the pool this.send_event_args_pool.Push(arg); } } | cs |
설정해놓은 최대 동접 수 만큼 SocketAsyncEventArgs객체를 미리 생성하여 풀에 넣어 놓는 코드입니다.
1-4-2. 송, 수신 버퍼 풀링 기법
BufferManager
다음으로 버퍼 관리에 대해 구현해보겠습니다.
SocketAsyncEventArgs마다 버퍼가 하나씩 필요합니다.
이 버퍼라는 것은 바이트 배열로 이루어진 메모리 덩어리입니다.
1 2 | BufferManager buffer_manager; this.buffer_manager = new BufferManager(this.max_connections * this.buffer_size * this.pre_alloc_count, this.buffer_size); | cs |
버퍼 매니저를 선언하고 생성하는 코드입니다.
버퍼의 전체 크기는 아래 공식으로 계산됩니다.
버퍼의 전체 크기 = 최대 동접 수치 * 버퍼 하나의 크기 * (전송용, 수신용)
전송용 한개, 수신용 한개 총 두개가 필요하기 때문에 pre_alloc_count = 2로 설정해 두었습니다.
버퍼 매니저의 코드를 살펴보겠습니다.
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | /// <summary> /// This class creates a single large buffer which can be divided up and assigned to SocketAsyncEventArgs objects for use /// With each socket I/O operation. This enables buffers to be easily reused and gaurds against fragmenting heap memory. /// /// The operations exposed on the BufferManager class are not thread safe. /// </summary> internal class BufferManager { int m_numBytes; // the total number of bytes controlled by the buffer pool byte[] m_buffer; // the underlying byte array maintained by the buffer Manager Stack<int> m_freeIndexPool; int m_currentIndex; int m_bufferSize; public BufferManager(int totalBytes, int bufferSize) { m_numBytes = totalBytes; m_currentIndex = 0; m_bufferSize = bufferSize; m_freeIndexPool = new Stack<int>(); } /// <summary> /// Allocates buffer space used by the buffer pool /// </summary> public void InitBuffer() { // create one big large buffer and divide that out to each SocketAsyncEventArg object m_buffer = new byte[m_numBytes]; } /// <summary> /// Assigns a buffer from the buffer pool to the specified SocketAsyncEventArgs object /// </summary> /// <returns>true if the buffer was successfully set, else false</returns> public bool SetBuffer(SocketAsyncEventArgs args) { if (m_freeIndexPool.Count > 0) { args.SetBuffer(m_buffer, m_freeIndexPool.Pop(), m_bufferSize); } else { if ((m_numBytes - m_bufferSize) < m_currentIndex) { return false; } args.SetBuffer(m_buffer, m_currentIndex, m_bufferSize); m_currentIndex += m_bufferSize; } return true; } /// <summary> /// Removes the buffer from a SocketAsyncEventArg object. This frees the buffer back to the /// buffer pool /// </summary> /// <param name="args"></param> public void FreeBuffer(SocketAsyncEventArgs args) { m_freeIndexPool.Push(args.Offset); args.SetBuffer(null, 0, 0); } } | cs |
이 코드도 msdn에 있는 샘플입니다.
InitBuffer 메소드에서 하나의 거대한 바이트 배열을 생성합니다.
그리고 SetBuffer(SocketAsyncEventArgs args) 메소드에서 SocketAsyncEventArgs 객체에
버퍼를 설정해줍니다.
하나의 버퍼를 설정한 다음에는 index 값을 증가시켜 다음 버퍼 위치를 가리킬 수 있도록 처리합니다.
넓은 땅에 금을 그어 이건 내꺼, 저건 니꺼 하는 식으로 나눈다고 생각하시면 될 것 같습니다.
마지막에는 사용하지 않는 버퍼를 반환시키기 위한 FreeBuffer 메소드입니다.
이 메소드는 아마 쓰이지 않게 될 것 같습니다.
왜냐하면 프로그램 시작시 최대 동접 수치만큼 버퍼를 할당한 뒤 중간에 해제하지 않고 계속 물고 있기 때문입니다. SocketAsyncEventArgs만 풀링하여 재사용할 수 있도록 처리해 놓으면 이 객체에 할당된 버퍼도 같이 따라가게 되기 때문입니다.
이전에 SocketAsyncEventArgs를 생성하여 풀링처리 햇던 부분을 다시 보겠습니다.
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 26 27 28 29 30 31 32 33 34 | for (int i = 0; i < this.max_connections; i++) { CUserToken token = new CUserToken(); // receive pool { // Pre-allocate a set of reusable SocketAsyncEventArgs arg = new SocketAsyncEventArgs(); arg.Completed += new EventHandler<SocketAsyncEventArgs>(receive_completed); arg.UserToken = token; // 추가된 부분 // assign a byte buffer from the buffer pool to the SocketAsyncEventArg object this.buffer_manager.SetBuffer(arg); // add SocketAsyncEventArg to the pool this.receive_event_args_pool.Push(arg); } // send pool { // Pre-allocate a set of reusable SocketAsyncEventArgs arg = new SocketAsyncEventArgs(); arg.Completed += new EventHandler<SocketAsyncEventArgs>(send_completed); arg.UserToken = token; // 추가된 부분 // assign a byte buffer from the buffer pool to the SocketAsyncEventArg object this.buffer_manager.SetBuffer(arg); // add SocketAsyncEventArg to the pool this.send_event_args_pool.Push(arg); } } | cs |
this.buffer_manager.SetBuffer(arg); 라는 코드가 보이시나요?
이 부분이 SocketAsyncEventArgs 객체에 버퍼를 설정하는 코드입니다.
SetBuffer 내부를 보면 범위를 잡아서 arg.SetBuffer를 호출해주게끔 되어있죠.
m_currentIndex += m_bufferSize; 이런식으로 버퍼 크기만큼 인덱스 값을 늘려서
서로 겹치지 않게 해주고 있는겁니다.
1-4-3. 유저의 접속 처리하기
SocketAsyncEventArgs와 버퍼 풀링까지 준비되었다면 접속된 클라이언트를 처리할 준비가 완료된 것입니다.
단순한 에코 서버라면 이런 준비 과정이 필요없을 수 있지만 수 천의 동접을 처리하는 게임서버라면 조금 번거롭더라도 이런 작업들을 잘 만들어 두어야 합니다.
또한 만들어 놓았다고 끝난 것이 아니라 그때부터 시작일지도 모릅니다.
처음 설계한대로 이쁘게 코딩 해놨어도 실제 테스트해보면 부족한 부분이 보일 때가 있기 때문이죠.
디버깅 하면서 뭔가 개운하지 못한 느낌이 든다면 다시 설계를 할 각오도 해야합니다.
클라이언트의 접속이 이루어진 후 호출되는 콜백 메소드인 on_new_client를 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void on_new_client(Socket client_socket, object token) { // 풀에서 하나 꺼내와 사용한다. SocketAsyncEventArgs receive_args = this.receive_event_args_pool.Pop(); SocketAsyncEventArgs send_args = this.send_event_args_pool.Pop(); // SocketAsyncEventArgs를 생성할 때 만들어 두었던 CUserToken을 꺼내와서 // 콜백 매소드의 파라미터로 넘김 if (this.session_created_callback != null) { CUserToken user_token = receive_args.UserToken as CUserToken; this.session_created_callback(user_token); } // 이제 클라이언트로부터 데이터를 수신할 준비를 begin_receive(client_socket, receive_args, send_args); } | cs |
accept 처리가 완료된 후 생성된 새로운 소켓을 파라미터로 넘겨주게 했습니다.
앞으로 이 소켓으로 클라이언트와 메시지를 주고받으면 됩니다.
풀링해 두었던 SocketAsyncEventArgs 객체도 드디어 사용할 때가 왔습니다.
1 2 | SocketAsyncEventArgs receive_args = this.receive_event_args_pool.Pop(); SocketAsyncEventArgs send_args = this.send_event_args_pool.Pop(); | cs |
1 2 | CUserToken user_token = receive_args.UserToken as CUserToken; this.session_created_callback(user_token); | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void begin_receive(Socket socket, SocketAsyncEventArgs receive_args, SocketAsyncEventArgs send_args) { // receive_args, send_args 아무곳에서나 꺼내와도 됨. 둘 다 동일한 CUserToken을 물고 있음 CUserToken token = receive_args.UserToken as CUserToken; token.set_event_args(receive_args, send_args); // 생성된 클라이언트 소켓을 보관해 놓고 통신할 때 사용함 token.socket = socket; // 데이터를 받을 수 있도록 소켓 메소드를 호출해준다. // 비동기로 수신할 경우 워커 스레드에서 대기중으로 있다가 Completed에 설정해놓은 메소드가 호출된다. // 동기로 완료될 경우에는 직접 완료 메소드를 호출해줘야 한다. bool pending = socket.ReceiveAsync(receive_args); if (!pending) { process_receive(receive_args); } } | cs |
비동기 소켓 메소드는 사용법이 다 비슷합니다. xxxAsync 메소드 호출 뒤 리턴 값을 확인하여
1 2 3 4 5 6 7 8 9 10 | void receive_completed(object sender, SocketAsyncEventArgs e) { if (e.LastOperation == SocketAsyncOperation.Receive) { process_receive(e); return; } throw new ArgumentException("The last operation completed on the socket was not a receive."); } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // This method is invoked when an asynchronous receive operation completes. // If the remote host closed the connection, then the socket is closed. private void process_receive(SocketAsyncEventArgs e) { // check if the remote host closed the connection CUserToken token = e.UserToken as CUserToken; if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success) { // 이후의 작업은 CUserToekn에 맡긴다. token.on_receive(e.Buffer, e.Offset, e.BytesTransferred); // 다음 메시지 수신을 위해서 다시 ReceiveAsync 메소드를 호출한다. bool pending = token.socket.ReceiveAsync(e); if (!pending) { process_receive(e); } } else { Console.WriteLine(string.Format("error {0}, transferred {1}", e.SocketError, e.BytesTransferred)); close_clientsocket(token); } } | cs |
1 2 3 4 5 6 | // 다음 메시지 수신을 위해서 다시 ReceiveAsync 메소드를 호출한다. bool pending = token.socket.ReceiveAsync(e); if (!pending) { process_receive(e); } | cs |
'- Programming > - C#' 카테고리의 다른 글
★ 6. c# 네트워크 개발 p5 (0) | 2017.02.10 |
---|---|
★ 5. c# 네트워크 개발 p4 (0) | 2017.02.09 |
★ 4. c# 네트워크 개발 p3 (0) | 2017.02.09 |
★ 3. c# 박싱과 언박싱 (0) | 2017.02.08 |
★ 1. c# 네트워크 개발 p1 (2) | 2017.02.06 |