C#/네트워크 관련

스스로 네트워크 프로그래밍 #5 - ReceiveBuffer

tmd1 2025. 2. 24. 22:14

 

RecvBuffer의 필요성

: TCP의 특성으로 데이터를 스트림 형태(데이터가 이어짐)로 전송하는데, 이러한 특성 때문에 보낸 데이터가 무조건 보낸 만큼 도착함이 보장되지 않음. 즉, 데이터 '일부'만 도착 가능(100bytes를 보냈다고 해도 무조건 100 bytes가 오지 않음)

 

때문에 데이터가 '일부'만 도착하게 되는 경우 바로 처리가 불가능함. 소켓 네트워크에서는 대부분의 데이터를 패킷 형태(헤더(패킷의 대한 정보), 실 데이터)로 보내기 때문에 일부만 도착한다면 그 데이터를 제대로 읽어낼 수 없음.

 

따라서, 일부만 도착한 경우 바로 처리가 불가능하니 버퍼에다가 보관만하고 나중에 나머지가 도착한 경우 처리하는 로직이 필요함.

 

즉, RecvBuffer가 필요!

 

class RecvBuffer
{
	ArraySegment<byte> _buffer;
    int _writePos; // 데이터를 받아올 첫 부분을 가리킴
    int _readPos; // 데이터를 읽어올 첫 부분을 가리킴
    
    public RecvBuffer(int bufferSize)
    {
    	_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
    }
    
    // 버퍼 여유분
    public int FreeSize { get { return _buffer.Count - _writePos; } }
    // 실 데이터가 입력되어있는 크기
    public int DataSize { get { return _writePos - _ readPos; } }
    
    // 데이터를 입력할 수 있는 여유분 가져옴
    public ArraySegment<byte> WriteSegment { get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); } }
    // 데이터를 읽을 수 있는 부분을 가져옴
    public ArraySegment<byte> ReadSegment{ get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); } }
    
    public void Clean()
    {
    	if(DataSize == 0)
        	_writePos = _readPos = 0;
        else
        {
        	Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, DataSize);
            _writePos = DataSize;
            _readPos = 0;
        }
    }
    
    // 데이터 처리 후 Pos 위치 변경
    public bool OnRead(int numOfBytes)
    {
    	if(numOfBytes > DataSize)
        	return false;
            
        _readPos += numOfBytes;
        return true;
    }
    
    public bool OnWrite(int numOfBytes)
    {
    	if(numOfBytes > FreeSize)
        	return false;
            
        _writePos += numOfBytes;
        return true;
    }
}

 

 

 

RecvBuffer class를 만들어주었으므로 Session 또한 이를 적용하여 수정하도록한다.

+ clientSession, serverSession을 통합하도록 한다. server와 client 모두 send, recv 기능을 사용하니까!

 

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
{
    public abstract class Session
    {
        Object _lock = new Object();
        Socket _socket;
        SocketAysncEventArgs _sendArgs;
        SocketAysncEventArgs _recvArgs;
        int _disconnect;
        
        Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
        
        RecvBuffer _recvBuffer;

        public abstract void OnConnected(EndPoint endPoint){}
        public abstract void OnDisconnectd(EndPoint endPoint){}
		public abstract void OnRecv(){}
        public abstract void OnSend(){}
        
        public void Disconnect()
        {
            // _disconnect를 1로 바꾼다.
            // 만약 이미 1 였다면(이미 disconnect한 경우) return;
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
                return;

			OnDisconnected();
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        public void Start(Socket socket)
        {

            _socket = socket;
            _sendArgs = new SocketAysncEvnetArgs();
            _recvArgs = new SocketAsyncEventArgs();

            _recvBuffer = new RecvBuffer(65535);

            RegisterRecev(_recvArgs);
        }
        
        public void Send(ArraySegment<byte> segment)
    	{
    		lock(_lock)
        	{
            	_sendQueue.Enqueue(segment);
        
        		// send가 진행중이 아니라면 실행
        		if(_pending == false)
    				RegisterSend(_sendArgs);
    		}
    	}

		void RegisterSend(SocketAsyncEventArgs args)
    	{
    		
            while(_sendQueue.Count > 0)
            {
            	_pendingList.Add(_sendQueue.Dequeue());
            }
            
            args.SetBufferList(_pendingList);
            
    		bool pending = _socket.SendAsync(args);
        	if(pending == false){
        		OnSendCompleted(null, args);
        	}
    	}

		void OnSendCompleted(Object obj, SocketAsyncEventArgs args)
		{
    		lock(_lock)
    		{
        		if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        		{
            		_pendingList.Clear();
                    args.SetBufferList = null;

            		if(_sendQueue.Count > 0)
                		RegisterSend(args);
        		}
        		else
        		{
            		Console.WriteLine(args.SocketError.ToString());
            		Disconnect();
        		}
    		}
		}

        void RegisterRecev(SocketAsyncEventArgs args)
        {
            ArraySegment<byte> segment = _recvBuffer.WriteSegment;
            args.SetBuffer(segment.Array, segment.Offset, segment.Count);
            
            bool pending = _clientSocket.ReceiveAsync(args);
            if (pending == false)
                OnRecvCompleted(null, args);
        }

        void OnRecvCompleted(object obj, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                if(OnWrite(args.BytesTransferred) == false)
                {
                	Console.WriteLine("Error");
                    Disconnect();
                }
                
                // 컨텐츠단에서 처리하고 처리한 길이 반환
                int processLen = OnRecv(_recvBuffer.ReadSegment);
                if(processLen < 0 || processLen > DataSize)
                {
                	Console.WriteLine("Error");
                    Disconnect();
                }

				if(OnRead(processLen) == false)
                {
                	Console.WriteLine("Error");
                    Disconnect();
                }
                RegisterRecev(args);
            }
            else
            {

                Console.WriteLine(args.SocketError.ToString());
                Disconnect();
            }

            
        }

        
    }
}

 

 

왜 send에는 lock 처리가 있고 recv에는 lock 처리가 없는가?

: 호출과 관련한 부분에서 확인할 수 있다. Send의 경우 Send() 함수 자체가 여러 스레드에서 동시 호출 될 수 있다. 따라서 lock 처리를 해야함은 자명하다. 그러나 Recv의 경우 socket 연결 자체가 성공되었을 때 단 한번 Start() 함수를 통해 RegisterRecv() 함수가 실행되고 위 함수는 여러 스레드에서 동시에 호출 될 수 없다! 따라서 lock 처리가 필요하지않다.