성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
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# - Socket의 TIME_WAIT 상태를 없애는 방법</h1> <p> 예전에 소켓의 상태를 재현하는 방법에 대해서 소개했는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 코드로 재현하는 소켓 상태(FIN_WAIT1, FIN_WAIT2, TIME_WAIT, CLOSE_WAIT, LAST_WAIT) ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1334'>http://www.sysnet.pe.kr/2/0/1334</a> </pre> <br /> 이 중에서 접속을 끊으려는 측, 즉 closesocket을 먼저 호출한 측에서 TIME_WAIT이 발생한다고 했습니다. TIME_WAIT의 특성상, 2MSL 시간이 지난 후 해제된다는 면도 그렇지만, 소켓 자원의 구분을 "로컬측:원격측"의 쌍으로 구분하기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 윈도우 서버 환경에서, 최대 생성 가능한 소켓(socket) 연결 수는 얼마일까? ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/964'>http://www.sysnet.pe.kr/2/0/964</a> </pre> <br /> TIME_WAIT의 누적으로 인한 문제가 발생하는 경우는 많지 않습니다. 그렇긴 해도, 기왕이면 서버 측에서 TIME_WAIT 상태의 소켓이 누적되는 것보다는 (부하에 대한) 부담이 없는 클라이언트 측에서 발생하는 것이 낫기 때문에 가능한 closesocket은 클라이언트에서 먼저 호출하도록 프로토콜을 협의하는 것이 좋습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> TIME_WAIT을 발생시키는 간단한 예제 코드를 한 번 볼까요? 우선 다음은 서버 측을 담당하는 코드이고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Net; using System.Net.Sockets; using System.Threading; class Program { static Socket _serverSocket; static void Main(string[] args) { Thread t = new Thread(serverSockFunc); t.Start(); Console.ReadLine(); } private static void serverSockFunc(object obj) { try { using (_serverSocket = CreateServerSocket()) { _serverSocket.Listen(5); while (_serverSocket != null) { Console.WriteLine("accept..."); using (Socket clntSocket = _serverSocket.Accept()) { try { string inputText = clntSocket.ReadString(); Console.WriteLine(inputText); } catch (Exception e) { Console.WriteLine(e.ToString()); } clntSocket.Close(); } } } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } private static Socket CreateServerSocket() { Socket socket = null; socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.Bind(ipEp); return socket; } } </pre> <br /> 아래는 별도의 EXE에서 실행할 클라이언트 측의 소스 코드입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Net; using System.Net.Sockets; using System.Threading; class Program { static void Main(string[] args) { Thread t2 = new Thread(clntSockFunc); t2.Start(); Console.ReadLine(); } private static void clntSockFunc(object obj) { while (true) { Thread.Sleep(1000); try { using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.Connect(ipEp); try { socket.WriteString("test"); socket.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } } catch (Exception e) { Console.WriteLine(e.ToString()); } } } private static Socket CreateClientSocket() { return new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } } </pre> <br /> 위의 2개 EXE를 실행하고 netstat로 확인하면 금방 다음과 같이 TIME_WAIT 소켓들이 (프로세스 id == 0번으로) 남아 있는 것을 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C:\Windows\System32> <span style='color: blue; font-weight: bold'>netstat -ano | findstr 57102</span> TCP 127.0.0.1:57102 0.0.0.0:0 LISTENING 20008 TCP 127.0.0.1:57102 127.0.0.1:4061 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4062 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4064 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4065 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4066 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4067 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4068 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4069 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4070 TIME_WAIT 0 TCP 127.0.0.1:57102 127.0.0.1:4071 TIME_WAIT 0 </pre> <a name='linger_opt'></a> <br /> 모두 클라이언트 측에서 WriteString 후, Close를 바로 했기 때문에 남아 있는 것으로, 이 문제(?)를 해결하려면 클라이언트 측의 소켓에서 Linger 옵션을 설정하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > TCP 소켓 옵션 - SO_LINGER ; <a target='tab' href='https://blog.naver.com/bringmelove1/119145244'>https://blog.naver.com/bringmelove1/119145244</a> </pre> <br /> C/C++의 l_onoff = 1, l_linger = 0 옵션 설정은 C#에서 다음과 같이 설정할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: <span style='color: blue; font-weight: bold'>socket.LingerState = new LingerOption(true, 0);</span> // Abortive Shutdown </pre> <br /> 이후 netstat로 보면 이렇게 ^^ 조용합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C:\Windows\System32> <span style='color: blue; font-weight: bold'>netstat -ano | findstr 57102</span> TCP 127.0.0.1:57102 0.0.0.0:0 LISTENING 24752 </pre> <br /> 그런데 문제가 있습니다. SO_LINGER 옵션은 close 호출 시 RST 패킷 전송을 하므로 클라이언트 측에서 다음과 같이 Send 후 곧바로 Close를 호출하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.LingerState = new <span style='color: blue; font-weight: bold'>LingerOption(true, 0);</span> socket.Connect(ipEp); <span style='color: blue; font-weight: bold'>socket.WriteString("test"); socket.Close();</span> } </pre> <br /> 서버 측에서 (높은 확률로) Read 시점에 오류가 발생할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > accept... install accept... System.Net.Sockets.SocketException (0x80004005): An existing connection was forcibly closed by the remote host at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags) at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 size, SocketFlags socketFlags) at SocketExtension.ReadInt32(Socket socket) in c:\temp\ConsoleApp2\ConsoleApp1\SocketExtension.cs:line 34 at SocketExtension.ReadString(Socket socket) in c:\temp\ConsoleApp2\ConsoleApp1\SocketExtension.cs:line 18 at ConsoleApp1.Program.serverSockFunc(Object obj) in c:\temp\ConsoleApp2\ConsoleApp1\Program.cs:line 35 accept... System.Net.Sockets.SocketException (0x80004005): An existing connection was forcibly closed by the remote host at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags) at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 size, SocketFlags socketFlags) at SocketExtension.ReadInt32(Socket socket) in c:\temp\ConsoleApp2\ConsoleApp1\SocketExtension.cs:line 34 at SocketExtension.ReadString(Socket socket) in c:\temp\ConsoleApp2\ConsoleApp1\SocketExtension.cs:line 18 at ConsoleApp1.Program.serverSockFunc(Object obj) in c:\temp\ConsoleApp2\ConsoleApp1\Program.cs:line 35 accept... </pre> <br /> 이유인즉, Write 후 Close를 너무 빠르게 호출하기 때문에 상대 측에서 Read 완료 전에 연결이 끊어지는 것입니다. 따라서, 다소의 가정을 통해 다음과 같이 Thread.Sleep을 주면 그런 문제를 없앨 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.LingerState = new LingerOption(true, 0); socket.Connect(ipEp); socket.WriteString("test"); Thread.Sleep(1); // 경우에 따라 1조차도 서버 측의 Read 과정에서 예외가 발생할 수 있음. // 따라서, 매우 임의적인 값에 불과. (localhost 환경이라면 거의 문제가 없겠지만!) socket.Close(); } </pre> <br /> Thread.Sleep이 마음에 들지 않지만, 일단은 TIME_WAIT을 없애는 방법 중의 하나입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 반면 linger timeout을 1(초)로 설정하면 어떻게 될까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > socket.LingerState = new LingerOption(true, 1); </pre> <br /> SO_LINGER 옵션에 시간 설정이 되면, FIN 패킷의 전송과 함께 그에 대한 ACK를 받을 때까지 Close를 호출한 스레드는 blocking된다고 합니다. 만약 마지막 FIN에 대한 ACK 신호를 지정 시간 내에 받게 되면 기존의 closesocket과 동일한 절차대로 진행(graceful shutdown)이 되지만, 그렇지 않은 경우(1초 후)에는 LingerOption(true, 0)인 상황과 동일하게 RST 패킷 전송과 함께 송수신 버퍼의 모든 내용을 폐기합니다.<br /> <br /> 따라서, 최솟값 1초이므로 (localhost 내의 통신이라면 거의 대부분 안전하게 1초 내에는 ACK를 받게 되므로) 거의 항상 기존의 4-way handshake 방식대로 종료 절차를 밟아 TIME_WAIT 현상이 동일하게 발생합니다.<br /> <br /> 재미있는 점이 하나 있다면, LingerOption(true, 1)의 경우 FIN에 대한 ACK 신호를 지정 시간 내에 받았다고 해도, 현재 송/수신 버퍼에 내용이 있다면 RST 처리한다는 점입니다. 따라서, 다음과 같이 의도적으로 서버 측에서 Write 과정을 한 번 더 하게 되면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 서버 측 using (Socket clntSocket = _serverSocket.Accept()) { string inputText = clntSocket.ReadString(); <span style='color: blue; font-weight: bold'>clntSocket.WriteString("1"); // 의도적으로 1바이트 전송</span> clntSocket.Close(); } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 클라이언트 측 using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.LingerState = <span style='color: blue; font-weight: bold'>new LingerOption(true, 1);</span> socket.Connect(ipEp); socket.WriteString("test"); <span style='color: blue; font-weight: bold'>socket.Close();</span> } </pre> <br /> 클라이언트 측의 Close 시점에 FIN을 보내고 ACK를 1초 이내에 받게 되지만, Receive 버퍼에 내용이 있으므로 RST 처리로 넘어가 TIME_WAIT이 이런 경우에도 남지 않게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 다행히 Thread.Sleep(1) 코드 없이 TIME_WAIT을 제거하는 방법이 있습니다. 바로 Disconnect 메서드를 사용하는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 클라이언트 측 using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.LingerState = new <span style='color: blue; font-weight: bold'>LingerOption(true, 0)</span>; socket.Connect(ipEp); socket.WriteString("test"); <span style='color: blue; font-weight: bold'>socket.Disconnect(false);</span> socket.Close(); } </pre> <br /> 단지 Disconnect 메서드의 사용이 다소 꺼려지는 점이 있다면, 이에 대한 문서같은 것들에서 왜 저것이 TIME_WAIT 과정을 거치지 않도록 만들어 주는지에 대한 정확한 자료를 찾을 수 없다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Socket.Disconnect(Boolean) Method ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.disconnect'>https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.disconnect</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> To ensure that all data is sent and received before the socket is closed, you should call Shutdown before calling the Disconnect method.<br /> <br /> If you need to call Disconnect without first calling Shutdown, you can set the DontLingerSocket option to false and specify a nonzero time-out interval to ensure that data queued for outgoing transmission is sent. Disconnect then blocks until the data is sent or until the specified time-out expires. If you set DontLinger to false and specify a zero time-out interval, Close releases the connection and automatically discards outgoing queued data.<br /> </div><br /> <br /> 게다가 위의 내용에 따르면, disconnect가 송/수신 데이터에 대한 책임을 지지 않는다는 것으로 LingerOption(true, 0) 설정과 함께 WriteString 후 곧바로 Close를 호출한 상황에서와 동일하게 서버 측의 예외가 발생할 듯싶은데, 테스트 결과에 따르면 분명히 서버 측은 Disconnect 이전의 데이터를 정상적으로 모두 수신을 합니다. 암튼, Disconnect의 이런 면 때문에 저 코드를 신뢰하고 사용할 수는 없을 것 같습니다.<br /> <br /> 또 다른 대안이 하나 있다면! 굳이 Disconnect를 사용하지 않더라도 클라이언트와 서버의 통신 프로토콜을 클라이언트 측의 마지막 과정이 Read가 되도록 조정하면 그냥 자연스럽게 RST를 보내게 되므로 TIME_WAIT이 남지 않는 상태가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 서버 측 using (Socket clntSocket = _serverSocket.Accept()) { string inputText = clntSocket.ReadString(); <span style='color: blue; font-weight: bold'>clntSocket.WriteString("OK"); // 결과 반환</span> clntSocket.Close(); } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 클라이언트 측 using (var socket = CreateClientSocket()) { IPEndPoint ipEp = new IPEndPoint(IPAddress.Loopback, 57102); socket.LingerState = <span style='color: blue; font-weight: bold'>new LingerOption(true, 0);</span> socket.Connect(ipEp); socket.WriteString("test"); string result = <span style='color: blue; font-weight: bold'>socket.ReadString(); // 마지막 동작이 Read</span> socket.Close(); } </pre> <br /> 아마도 현실적으로는, (서버와 클라이언트를 모두 자신이 통제할 수 있다는 조건 하에) 저런 식으로 처리하는 것이 TIME_WAIT을 없애는 가장 매끄러운 방식이 아닐까 생각합니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1485&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 시간이 있다면 위의 코드들을 상황 별로 네트워크 패킷 캡처를 통해 확인하고 싶은 내용들이 있습니다.<br /> <br /> 가령, 위의 마지막 예제는 시간 상으로 보면 서버 측에서 먼저 Close를 하게 되어 있지만 실제로 테스트해 보면 TIME_WAIT이 서버 측에 남지 않습니다. 아마도 예상으로는, 서버는 Close 시점에 FIN을 보내고 클라이언트 측으로부터 ACK를 받아 FIN_WAIT2 상태에 빠지겠지만 클라이언트 측의 마지막 Close에서 FIN이 아닌 RST를 전송하는 것으로 인해 FIN_WAIT2에서 TIME_WAIT으로 넘어가지 못하고 곧바로 제거되는 것 같습니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1163
(왼쪽의 숫자를 입력해야 합니다.)