성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ClientWebSocket의 Ping, Pong 처리</h1> <p> 지난 글에 WebSocket의 기본 구현을 다뤄봤는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13505'>https://www.sysnet.pe.kr/2/0/13505</a> </pre> <br /> 이번에는 Ping/Pong이 어떻게 ClientWebSocket에 구현되었는지 살펴보겠습니다. 이를 위해 그에 앞서 우선 닷넷 WebSocket의 구조를 먼저 살펴볼 필요가 있습니다. 왜냐하면 WebSocket을 상속한 ClientWebSocket은,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.Net.WebSockets.ClientWebSocket ws = new System.Net.WebSockets.ClientWebSocket(); </pre> <br /> 최소한의 API만 노출하고 있는, 사실상 껍데기에 불과하기 때문입니다. 실질적인 작업은, 이후 ConnectAsync 시점에 불리는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > await ws.ConnectAsync(new Uri(url), connectTimeout.Token); </pre> <br /> WebSocketHandle 인스턴스가 담당합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ClientWebSocket 타입 public Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, CancellationToken cancellationToken) { // ...[생략]... return ConnectAsyncCore(uri, invoker, cancellationToken); } private async Task ConnectAsyncCore(Uri uri, HttpMessageInvoker? invoker, CancellationToken cancellationToken) { <span style='color: blue; font-weight: bold'>_innerWebSocket = new WebSocketHandle();</span> await <span style='color: blue; font-weight: bold'>_innerWebSocket.ConnectAsync</span>(uri, invoker, cancellationToken, Options).ConfigureAwait(false); // ...[생략]... } </pre> <br /> 또한 WebSocketHandle 타입은 내부적으로 WebSocket 필드를 하나 가지고 있는데요, 그곳에는 ConnectAsync 시점에 ManagedWebSocket 인스턴스를 생성해 담아두게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // WebSocketHandle 타입 public async Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, CancellationToken cancellationToken, ClientWebSocketOptions options) { // ...[생략]... <span style='color: blue; font-weight: bold'>WebSocket = WebSocket.CreateFromStream</span>(connectedStream, new WebSocketCreationOptions { IsServer = false, SubProtocol = subprotocol, KeepAliveInterval = options.KeepAliveInterval, DangerousDeflateOptions = negotiatedDeflateOptions }); // ...[생략]... } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // WebSocket 타입 public static WebSocket <span style='color: blue; font-weight: bold'>CreateFromStream</span>(Stream stream, WebSocketCreationOptions options) { // ...[생략]... <span style='color: blue; font-weight: bold'>return new ManagedWebSocket(stream, options);</span> } </pre> <br /> 그러니까, WebSocket 프로토콜과 관련된 기능들은 모두 ManagedWebSocket에서 구현하고 있는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 닷넷 ClientWebSocket의 경우, Ping/Pong 처리를 Options.KeepAliveInterval 속성으로 지정할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.Net.WebSockets.ClientWebSocket ws = new System.Net.WebSockets.ClientWebSocket(); <span style='color: blue; font-weight: bold'>ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);</span> // 10초마다 서버로 Pong 전송 </pre> <a name='pong'></a> <br /> 이후, ManagedWebSocket 내부에서는 SendKeepAliveFrameAsync 메서드가 KeepAliveInterval마다 호출이 되는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ManagedWebSocket 타입 private void SendKeepAliveFrameAsync() { // This exists purely to keep the connection alive; don't wait for the result, and ignore any failures. // The call will handle releasing the lock. We send a pong rather than ping, since it's allowed by // the RFC as a unidirectional heartbeat and we're not interested in waiting for a response. ValueTask t = SendFrameAsync(<span style='color: blue; font-weight: bold'>MessageOpcode.Pong</span>, endOfMessage: true, disableCompression: true, ReadOnlyMemory<byte>.Empty, CancellationToken.None); // ...[생략]... } </pre> <br /> 재미있는 건, 이때 Ping이 아닌 Pong을 보낸다는 점입니다. 그 이유는 위의 코드에 남겨진 주석에서 설명하고 있습니다. 어차피 WebSocket의 Ping을 송신해도 서버로부터 응답이 오는 것에 관심이 없으므로, 그냥 클라이언트가 살아 있다는 Pong을 주기적으로 전송하는 역할만 하는 것입니다.<br /> <br /> 저렇게 (KeepAliveInterval로 인해) 자동 송신되는 Pong을 nodejs에서는 다음과 같이 수신할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > sockserver.on('connection', ws => { console.log('New client connected!') ws.send('connection established') <span style='color: blue; font-weight: bold'>ws.on('pong', () => console.log('pong received from client!'))</span> // ...[생략]... }) </pre> <br /> 따라서, nodejs 서버/닷넷 클라이언트를 실행하면 10초마다 닷넷 클라이언트에서 Pong을 주기적으로 송신하고, nodejs는 ws.on('pong', ...) 코드로 인해 10초마다 "pong received..." 로그가 찍히게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 자, 그럼 반대로 nodejs 측에서 ping 또는 pong을 전송하도록 해볼까요?<br /> <br /> 우선, ping부터 다음과 같은 코드로 전송해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > sockserver.on('connection', ws => { interval_id = 0; console.log('New client connected!') ws.send('connection established') <span style='color: blue; font-weight: bold'>ws.on('pong', () => { console.log('pong received from client'); });</span> ws.on('close', () => { clearInterval(interval_id); console.log('Client has disconnected!'); }); ws.on('message', data => { sockserver.clients.forEach(client => { console.log(`distributing message: ${data}`) client.send(`echo ${data}`) }) }) ws.onerror = function () { console.log('websocket error') } <span style='color: blue; font-weight: bold'>interval_id = setInterval(() => { ws.ping(); }, 1000 * 5);</span> }) </pre> <br /> 보는 바와 같이, 5초 주기로 ws.ping을 호출하고 있고, ws.on('pong', ...)으로 (ping을 보낸 측에서 응답할) pong 수신을 처리하고 있습니다. 닷넷 클라이언트 측은, 이에 대해 KeepAliveInterval을 사용할 필요도 없이 오직 해야 할 일은 "ReceiveAsync" 메서드를 호출해 두는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static async Task Main(string[] args) { string url = "ws://localhost:18000/"; var connectTimeout = new CancellationTokenSource(); connectTimeout.CancelAfter(2000); System.Net.WebSockets.ClientWebSocket ws = new System.Net.WebSockets.ClientWebSocket(); await ws.ConnectAsync(new Uri(url), connectTimeout.Token); if (ws.State != System.Net.WebSockets.WebSocketState.Open) { Console.WriteLine($"Failed to connect: {url}"); return; } Console.WriteLine($"Received: {await Read(ws)}"); await ws.SendAsync(Encoding.UTF8.GetBytes("Hello"), WebSocketMessageType.Text, true, CancellationToken.None); Console.WriteLine($"Received: {await Read(ws)}"); // 위의 코드까지는 Send/Receive 쌍을 맞춰 호출 // 아래의 코드는 일방적으로 Receive를 하는 경우로 가정 var result = <span style='color: blue; font-weight: bold'>await ws.ReceiveAsync</span>(new ArraySegment<byte>(new byte[1024]), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); Console.WriteLine("Received close message"); } Console.WriteLine(result.MessageType); } </pre> <br /> 이후 nodejs 서버와 닷넷 클라이언트를 실행하면, nodejs 측의 주기적인 ping 신호에 대해 닷넷 클라이언트는 위의 코드상으로는 딱히 반응은 없지만 nodejs 측의 ws.on('pong', ...) 코드가 실행되는 것을 확인할 수 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 자, 그럼 왜 저렇게 동작하는지 한번 파악해 볼까요? ^^ 위에서 마지막에 호출한 ReceiveAsync 내에서는 nodejs로부터 수신한 Ping 신호를 자동으로 처리하는 코드를 담고 있습니다. 우선, ReceiveAsyncPrivate에서는 다음과 같이 Ping/Pong 신호를 구분해 내고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] private async ValueTask<TResult> ReceiveAsyncPrivate<TResult>(Memory<byte> payloadBuffer, CancellationToken cancellationToken) { // ...[생략]... while (true) // in case we get control frames that should be ignored from the user's perspective { // ...[생략]... await _stream.ReadAsync(Memory<byte>.Empty, cancellationToken).ConfigureAwait(false); // ...[생략]... string? headerErrorMessage = <span style='color: blue; font-weight: bold'>TryParseMessageHeaderFromReceiveBuffer</span>(out header); // ...[생략]... <span style='color: blue; font-weight: bold'>if (header.Opcode == MessageOpcode.Ping || header.Opcode == MessageOpcode.Pong) { await HandleReceivedPingPongAsync(header, cancellationToken).ConfigureAwait(false); continue; }</span> // ...[생략]... } // ...[생략]... } private string? TryParseMessageHeaderFromReceiveBuffer(out MessageHeader resultHeader) { // ...[생략]... Span<byte> receiveBufferSpan = _receiveBuffer.Span; // ...[생략]... <span style='color: blue; font-weight: bold'>header.Opcode</span> = (MessageOpcode)(receiveBufferSpan[_receiveBufferOffset] & 0xF); // ...[생략]... // Do basic validation of the header switch (header.Opcode) { // ...[생략]... <span style='color: blue; font-weight: bold'>case MessageOpcode.Ping: case MessageOpcode.Pong:</span> if (header.PayloadLength > MaxControlPayloadLength || !header.Fin) { // Invalid control messgae resultHeader = default; return SR.net_Websockets_InvalidControlMessage; } break; // ...[생략]... } // ...[생략]... } </pre> <br /> Pong 신호인 경우에는 무시하지만, Ping 신호인 경우에는 응답으로 Pong을 송신하는 절차를 거칩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private async ValueTask HandleReceivedPingPongAsync(MessageHeader header, CancellationToken cancellationToken) { // ...[생략]... // If this was a ping, send back a pong response. if (<span style='color: blue; font-weight: bold'>header.Opcode == MessageOpcode.Ping</span>) { // ...[생략]... await <span style='color: blue; font-weight: bold'>SendFrameAsync( MessageOpcode.Pong,</span>) endOfMessage: true, disableCompression: true, _receiveBuffer.Slice(_receiveBufferOffset, (int)header.PayloadLength), cancellationToken).ConfigureAwait(false); } // ...[생략]... } </pre> <br /> 비록 ManagedWebSocket 자신은 KeepAliveInterval에 대해 Pong 전송만 하는 형식이었지만, 서버의 ping 신호에 대해서는 약속에 따라 Pong으로 응답하고 있는 것입니다. 따라서 만약 nodejs 측의 ping을 pong으로 바꾸면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > sockserver.on('connection', ws => { // ..[생략]... ws.on('pong', () => { console.log('log after pong'); // 이 코드가 실행되지 않음 }); // ..[생략]... interval_id = setInterval(() => { <span style='color: blue; font-weight: bold'>ws.pong();</span> // pong을 송신하므로 닷넷 클라이언트는 무응답! }, 1000 * 5); }) </pre> <br /> ManagedWebSocket의 ReceiveAsyncPrivate 메서드에서는 Pong 수신을 무시하므로 nodejs의 ws.on('pong', ...) 코드도 (ping을 보냈던 것과는 달리) 더 이상 실행되지 않게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 소스코드를 정리해 보면, C#의 ClientWebSocket 자체는 Timeout 기능이 없다는 점입니다. 즉, (ReceiveAsync 내부적으로) Ping/Pong을 수신했다고 해서 어떠한 시간을 늘리거나 하는 등의 코드는 전혀 없습니다. 또한 ClientWebSocket은 그 패킷을 받았다는 것을 인지하기 위한 이벤트 핸들러같은 것도 제공하지 않습니다. 게다가 KeepAliveInterval조차도 단지 서버에 내가 살아 있으니 끊지 말라는 식으로 Pong 응답을 주기적으로 보내는 것에 불과하고!<br /> <br /> 그렇다면 왜 이런 식으로 만든 것일까요?<br /> <br /> 제가 아직 WebSocket에 대해서는 자세하게 몰라서 확신할 수는 없지만, 어쩌면 이것은 Client의 자원 부담이 상대적으로 Server보다는 덜하다는 점에서 나온 특징인 듯합니다. 즉, Client는 굳이 (WebSocket 수준에서의) Timeout 기능이 없어도 문제 될 가능성이 크지 않지만, Server는 필요 없는 연결 자원에 대해 가능한 빠르게 끊는 것이 더 바람직하기 때문입니다.<br /> <br /> 물론 Socket 자체의 Timeout 기능도 있고 WebSocket 역시 Send/Receive에서의 연결 해제 감지가 되므로 Client 입장에서 Ping/Pong을 이용한 timeout 체크가 간절하지 않기도 합니다.<br /> <br /> 반면 서버는, 응용 프로그램 측에서의 프로토콜 결정에 따라 좀 더 공격적으로 연결 여부를 확인하기 위해 필요한 수단이지 않을까 싶습니다. ^^ (혹시 이에 대해 자세한 이력을 아시는 분은 덧글 부탁드립니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 다시 간단하게 정리해 보면, 2가지만 기억하면 됩니다.<br /> <br /> <ol> <li>ClientWebSocket은 KeepAliveInterval을 설정한 경우 서버로 Pong만 전송</li> <li>서버로부터 Ping을 수신하면 Pong 응답, 반면 Pong을 수신하면 응답 없음</li> </ol> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2125
(왼쪽의 숫자를 입력해야 합니다.)