(C#) Session [4]
포스트
취소

(C#) Session [4]

지난 글(Session [3])에 이어서 완성된 Session을 사용해 보도록 한다.

💻 코드

비동기식으로 만들어진 Send, Recv 등을 사용하기 위해 추상메소드로 만들어준다.

1
2
3
4
5
6
7
abstract class Session
{
    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);
}


추상클래스 Session을 사용하기 위해 Main에서 새로 GameSession을 구현한다.

  • 🛠 이러한 Session의 경우 상속받아 사용하는게 더 편리하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GameSession : Session
{
    public override void OnConnected(EndPoint endPoint)
    {
    }

    public override void OnDisConnected(EndPoint endPoint)
    {
    }

    public override void OnRecv(ArraySegment<byte> buffer)
    {
    }

    public override void OnSend(int numOfBytes)
    {
    }
}


만들어낸 추상메소드를 각 기능에 맞는 코어에 넣어준다.

[ Source Code (Click) ]
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
65
66
67
68
69
// OnDisConnected
public void Disconnect()
{
    // 혹시 클라이언트 2명 이상이 동시에 종료됐을 때를 방지
    if (Interlocked.Exchange(ref _disConnected, 1) == 1)
        return;

    // ※ 추상메소드 이곳에 넣기 ※
    OnDisConnected(_socket.RemoteEndPoint);

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

// OnSend
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    lock (_lock)
    {
        if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        {
            try
            {
                _sendArgs.BufferList = null;
                _pendingList.Clear();

                // ※ 추상메소드 이곳에 넣기 ※
                OnSend(_sendArgs.BytesTransferred);

                // 끝내기 전 Send할게 있다면 이곳에서 처리해주기.
                // 혹시 보내는 과정에서 다른 클라이언트가 Send했다면
                if (_sendQueue.Count > 0)
                    RegisterSend();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"OnRecvCompleted Failed : {ex}");
            }
        }
        else
        {
            Disconnect();
        }
    }
}

// OnRecv
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
    // Byte가 0으로 올때도 있기 때문에 0 이상으로 받기
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
    {
        try
        {
            // ※ 추상메소드 이곳에 넣기 ※
            OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
            
            RegisterRecv();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OnRecvCompleted Failed : {ex}");
        }
    }
    else
    {
        Disconnect();
    }
}


당장 구현할 기능이 없으니 코어에서 임시로 사용하던 기능을 GameSession에 넣어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GameSession : Session
{
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"OnConnected : {endPoint}");
    }

    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}");
    }
}


지금 상황에서 OnConnected를 넣기에는 너무 애매하기 때문에 코드를 수정하도록 한다.

OnConnected가 실행될 위치를 생각해보면 Listener에서 클라이언트의 접속이 성공했을 때 실행되는 위치에 넣으면 될것이다.

그리고 이러한 중요한 코드(Start, Connected)는 내부 코어에서 실행할 수 있도록 하는 것이 좋다.

하지만 이러한 SessionListener에서 생성하는 것은 조금 말이 안맞기 때문에

그때그때 다양한 Session을 받아 사용할 수 있도록 CallBack을 적용한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Listener
{
    // Action<Socket> _onAcceptHandler;
    Func<Session> _sessionFactory;

    // public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
    public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
    {
        // _onAcceptHandler += onAcceptHandler;
        _sessionFactory += sessionFactory;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Listener
{
    void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        if (args.SocketError == SocketError.Success)
        {
            // 클라이언트와 통신 시작
            Session session = _sessionFactory.Invoke();

            session.Start(args.AcceptSocket);
            session.OnConnected(args.AcceptSocket.RemoteEndPoint);
        }
        else
            Console.WriteLine(args.SocketError.ToString());

        // 다시 낚시대 던지기
        RegisterAccept(args);
    }
}


Main에서 Listener를 호출하던 코드도 수정해준다.

예시로 사용하기 때문에 람다식을 사용해 GameSession을 생성하여 보내준다.

그리고 기존에 콜백으로 사용하던 OnAcceptHandler는 사용하지 않으므로

기능은 OnConnected에 보내주고 삭제한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override void OnConnected(EndPoint endPoint)
{
    Console.WriteLine($"OnConnected : {endPoint}");

    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to LHH Server !!");
    Send(sendBuff);
    Thread.Sleep(1000);
    Disconnect();
}

static void Main(string[] args)
{
    _listener.Init(endPoint, () => { return new GameSession(); });
}


🔍 조심해야될 부분

클라이언트가 입장하고 서버와 통신이 시작되는 부분에서

session.Start -> session.OnConnected로 넘어가는 과정에 클라이언트가 연결을 끊는다면

session.OnConnected에서는 에러가날 수 밖에 없다.

이러한 부분에서 미리 공부하는 것도 좋지만, 직접 경험하여 몸에 쌓는 것이 더 기억에 남을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Listener
{
    void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        if (args.SocketError == SocketError.Success)
        {
            // 클라이언트와 통신 시작
            Session session = _sessionFactory.Invoke();

            session.Start(args.AcceptSocket);
            session.OnConnected(args.AcceptSocket.RemoteEndPoint);
        }
        else
            Console.WriteLine(args.SocketError.ToString());

        // 다시 낚시대 던지기
        RegisterAccept(args);
    }
}


이렇게 구현된 Session이 어느정도 틀이 완성됐다.

[ Source Code (Click) ]
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
namespace ServerCore
{
    abstract class Session
    {
        Socket _socket;
        int _disConnected = 0;  // 소켓이 연속으로 빠져나가는걸 방지

        object _lock = new object();

        Queue<byte[]> _sendQueue= new Queue<byte[]>();
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

        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);

        public void Start(Socket socket)
        {
            _socket = socket;


            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);

            // args 버퍼 생성
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            // Recv 등록
            RegisterRecv();
        }

        // 보내기
        public void Send(byte[] sendBuff)
        {
            // 계속 바이트를 만들어 주기에는 부담이 너무 크다.
            // 그러므로 큐로 보내줄 바이트들을 받아준 다음
            // 보낼준비가 되면 그때 한번에 보낸다.
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }

        // 연결 종료
        public void Disconnect()
        {
            // 혹시 클라이언트 2명 이상이 동시에 종료됐을 때를 방지
            if (Interlocked.Exchange(ref _disConnected, 1) == 1)
                return;

            OnDisConnected(_socket.RemoteEndPoint);
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        #region 네트워크 통신

        void RegisterSend()
        {
            // 알려진바는 없지만 이렇게 구현해야됨.
            // 패킷을 한번에 모아서 보내주기
            while (_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
            }

            _sendArgs.BufferList = _pendingList;

            // 모아놓은 패킷을 보낸다.
            // 만약 바로 보내진다면 pending은 false 나옴.
            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
        }

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        OnSend(_sendArgs.BytesTransferred);

                        // 끝내기 전 Send할게 있다면 이곳에서 처리해주기.
                        // 혹시 보내는 과정에서 다른 클라이언트가 Send했다면
                        if (_sendQueue.Count > 0)
                            RegisterSend();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"OnRecvCompleted Failed : {ex}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }
        }

        // Recv 등록
        void RegisterRecv()
        {
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false)
                OnRecvCompleted(null, _recvArgs);
        }

        // Recv 성공
        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            // Byte가 0으로 올때도 있기 때문에 0 이상으로 받기
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
                    
                    RegisterRecv();
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"OnRecvCompleted Failed : {ex}");
                }
            }
            else
            {
                Disconnect();
            }
        }

        #endregion
    }

    class Listener
    {
        Socket _listeneSocket;
        Func<Session> _sessionFactory;

        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 문지기 ( SocketType.Stream, ProtocolType.Tcp 는 거의 묶음으로 사용한다. )
            _listeneSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory += sessionFactory;

            // 문지기 교육
            _listeneSocket.Bind(endPoint);

            // 영업 시작
            // backlog = 최대 대기수
            _listeneSocket.Listen(10);

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();

            // 유저가 접속하면 OnAcceptCompleted()를 실행
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
            RegisterAccept(args);
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            // args 소켓이 초기화하지 않으면 pending은 계속 false가 나옴.
            args.AcceptSocket = null;

            // (낚시대를 던지자 마자 물고기 입질이 잡힌거)
            // 유저를 기다리는데 바로 유저가 들어왔다면 false 반환
            bool pending = _listeneSocket.AcceptAsync(args);
            if (pending == false)
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                // 받기, 보내기 시작
                Session session = _sessionFactory.Invoke();

                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            // 다시 낚시대 던지기
            RegisterAccept(args);
        }
    }

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to LHH 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 void Main(string[] args)
        {
            // DNS (Domain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddress = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddress, 5000);

            _listener.Init(endPoint, () => { return new GameSession(); });
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
            }
        }
    }
}


💡 참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.