성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 제가 큰 실수를 했군요. ^^; 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# - Reflection을 이용한 ClientWebSocket의 Ping 호출</h1> <p> 지난 글에 Ping/Pong이 ClientWebSocket에 의해 어떻게 처리되는지 살펴봤습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ClientWebSocket의 Ping, Pong 처리 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13518'>https://www.sysnet.pe.kr/2/0/13518</a> </pre> <br /> 언급했듯이, (서버 측 nodejs는 Ping을 전송하고 Pong을 받는데) C#의 ClientWebSocket은 무조건 Pong을 전송하는 식입니다. 그렇다면 혹시 Ping을 전송하는 방법은 없을까요?<br /> <br /> 이미 설명한 것처럼, 모든 처리는 ManagedWebSocket 내부에서 자동으로 수행되는데다 ClientWebSocket이 호출하는 Send 메서드는 내부적으로 Binary와 Text 유형만 보낼 수 있도록 고정했기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public override ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, WebSocketMessageFlags messageFlags, CancellationToken cancellationToken) { // ...[생략]... <span style='color: blue; font-weight: bold'>MessageOpcode opcode;</span> if (_lastSendWasFragment) { if (_lastSendHadDisableCompression != disableCompression) { throw new ArgumentException(SR.net_WebSockets_Argument_MessageFlagsHasDifferentCompressionOptions, nameof(messageFlags)); } <span style='color: blue; font-weight: bold'>opcode = MessageOpcode.Continuation;</span> } else { <span style='color: blue; font-weight: bold'>opcode = messageType == WebSocketMessageType.Binary ? MessageOpcode.Binary : MessageOpcode.Text;</span> } ValueTask t = SendFrameAsync(<span style='color: blue; font-weight: bold'>opcode</span>, endOfMessage, disableCompression, buffer, cancellationToken); // ...[생략]... return t; } </pre> <br /> 딱히 제어를 할 수 있는 여지가 없습니다. 정 원한다면, 가령 테스트를 위해서 필요하다면 Reflection을 이용해 처리하는 수밖에 없습니다. <br /> <br /> 그다지 어렵지 않으니 간단하게 코드로 만들어 볼까요? ^^ 우선 SocketHandle과 ManagedWebSocket을 차례로 구하는 것부터 시작해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private static object? GetSocketHandle(ClientWebSocket ws) { Type type = ws.GetType(); FieldInfo? fi = type.GetField("<span style='color: blue; font-weight: bold'>_innerWebSocket</span>", BindingFlags.NonPublic | BindingFlags.Instance); if (fi == null) { return null; } return fi.GetValue(ws); } private static WebSocket? GetManagedSocket(ClientWebSocket ws) { var webSocketHandle = GetSocketHandle(ws); if (webSocketHandle == null) { return null; } Type type = webSocketHandle.GetType(); return type.GetProperty("<span style='color: blue; font-weight: bold'>WebSocket</span>", BindingFlags.Public | BindingFlags.Instance)?.GetValue(webSocketHandle) as WebSocket; } </pre> <br /> 이후 남은 작업은, ManagedWebSocket이 구현한 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 (header.Opcode == MessageOpcode.Ping) { // ...[생략]... <span style='color: blue; font-weight: bold'>await SendFrameAsync( MessageOpcode.Pong,) endOfMessage: true, disableCompression: true, _receiveBuffer.Slice(_receiveBufferOffset, (int)header.PayloadLength), cancellationToken).ConfigureAwait(false);</span> } // ...[생략]... } </pre> <br /> 우리도 저 SendFrameAsync 메서드를 이용해 MessageOpcode.Ping (0x9)에 해당하는 값을 전송하도록 맞춰주기만 하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private static ValueTask SendPingFrameAsync(WebSocket managedSocket) { Type type = managedSocket.GetType(); MethodInfo? targetMi = null; foreach (MethodInfo mi in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) { if (mi.Name == "<span style='color: blue; font-weight: bold'>SendFrameAsync</span>" && mi.GetParameters().Length == 5) { targetMi = mi; break; } } if (targetMi == null) { throw new NullReferenceException("SendFrameAsync not found"); } /* private enum MessageOpcode : byte { Continuation = 0x0, Text = 0x1, Binary = 0x2, Close = 0x8, <span style='color: blue; font-weight: bold'>Ping = 0x9,</span> Pong = 0xA } */ object? objValue = <span style='color: blue; font-weight: bold'>targetMi.Invoke(managedSocket, new object?[] { (byte)0x9, true, true, ReadOnlyMemory<byte>.Empty, CancellationToken.None });</span> if (objValue == null) { throw new NullReferenceException("SendFrameAsync returned null"); } return (ValueTask)objValue; } private static void SendKeepAliveFrame(WebSocket managedSocket) { ValueTask t = SendPingFrameAsync(managedSocket); if (t.IsCompletedSuccessfully) { t.GetAwaiter().GetResult(); } else { // "Observe" any exception, ignoring it to prevent the unobserved exception event from being raised. t.AsTask().ContinueWith(static p => { _ = p.Exception; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } } </pre> <br /> 끝이군요. ^^ 이제 위에서 구현한 SendKeepAliveFrame 메서드를 ClientWebSocket에 적용해 다음과 같이 일부러 Ping을 전송하는 코드를 만들 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string url = "ws://localhost:18000/"; var connectTimeout = new CancellationTokenSource(); connectTimeout.CancelAfter(2000); System.Net.WebSockets.ClientWebSocket ws = new System.Net.WebSockets.ClientWebSocket(); ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(0); // Pong 전송 코드 수행을 막기 위해 일부러 0으로 설정 await ws.ConnectAsync(new Uri(url), connectTimeout.Token); if (ws.State != System.Net.WebSockets.WebSocketState.Open) { Console.WriteLine($"Failed to connect: {url}"); return; } <span style='color: blue; font-weight: bold'>_ = Task.Run(() => { WebSocket? managedSocket = GetManagedSocket(ws); if (managedSocket == null) { return; } while (true) { SendKeepAliveFrame(managedSocket); Thread.Sleep(5000); } });</span> </pre> <br /> 끝입니다. 잘 동작하는지 확인을 위해 nodejs 서버 코드에 로그를 남겨 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > var { WebSocketServer } = require('ws') const sockserver = new WebSocketServer({ port: 18000 }) 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('ping', () => { console.log('ping received from client'); });</span> // ...[생략]... }) </pre> <br /> 정상적으로 "ping received ..." 메시지가 출력되는 것을 볼 수 있습니다. 당연히 nodejs는 저렇게 ping을 수신하면 응답으로 pong을 다시 클라이언트로 전송하게 될 것입니다. 아쉽게도 수신 여부를 ClientWebSocket으로 알 수는 없지만, 비주얼 스튜디오의 닷넷 소스코드 디버깅 기능을 이용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 비주얼 스튜디오 2022를 이용한 (소스 코드가 없는) 닷넷 모듈 디버깅 - "외부 원본(External Sources)" ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13109'>https://www.sysnet.pe.kr/2/0/13109</a> </pre> <br /> ReceiveAsyncPrivate 메서드 내부의 MessageOpcode.Pong 조건절에 BP를 설정해 확인해 볼 수는 있습니다.<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 = TryParseMessageHeaderFromReceiveBuffer(out header); // 아래의 코드에 Breakpoint 설정 <span style='color: blue; font-weight: bold'>if (header.Opcode == MessageOpcode.Ping || header.Opcode == MessageOpcode.Pong)</span> { await HandleReceivedPingPongAsync(header, cancellationToken).ConfigureAwait(false); continue; } // ...[생략]... } // ...[생략]... } </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2130&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1166
(왼쪽의 숫자를 입력해야 합니다.)