Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 

파이썬 - Linux 환경 + TCP 서버 소켓을 사용하는 프로세스 종료 후 재실행하는 경우 "OSError: [Errno 98] Address already in use" 오류 발생

간단한 예제를 하나 만들어 볼까요?

import socketserver

server = None


class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        pieces = [b'']
        total = 0
        while b'\n' not in pieces[-1] and total < 10_000:
            pieces.append(self.request.recv(2000))
            total += len(pieces[-1])
        self.data = b''.join(pieces)
        print(f"Received from {self.client_address[0]}:")
        # just send back the same data, but upper-cased
        self.request.sendall(bytes("HTTP/1.1 200 OK\r\n\r\n", "utf-8"))
        self.request.sendall(self.data.upper())


def start_app():
    print('my app started')

    global server
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyTCPHandler)
    try:
        server.serve_forever()
    except:
        print('server closing...')
        server.server_close()
        print('server closed')


if __name__ == '__main__':
    start_app()

실행 후, (웹 브라우저 등을 이용해) HTTP 요청을 보내 정상 동작하는 것을 확인하고 Ctrl+C로 프로세스를 종료합니다. (혹은 kill -9 [pid] 명령어로 종료하거나!)

// 만약 한 번이라도 요청을 보내지 않으면 아래의 현상이 발생하지 않습니다.

$ python main.py
my app started
Received from 127.0.0.1:
^Cserver closing...
server closed

이후, 곧바로 위의 예제를 다시 실행하면 이런 오류가 발생합니다.

$ python main.py
my app started
Traceback (most recent call last):
  File "main.py", line 63, in <module>
    start_app()
  File "main.py", line 55, in start_app
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyTCPHandler)
  File "/home/testusr/miniconda3/envs/py38build/lib/python3.8/socketserver.py", line 452, in __init__
    self.server_bind()
  File "/home/testusr/miniconda3/envs/py38build/lib/python3.8/socketserver.py", line 466, in server_bind
    self.socket.bind(self.server_address)
OSError: [Errno 98] Address already in use

문서를 보니까, shutdown 함수가 있는데,

socketserver — A framework for network servers
; https://docs.python.org/3/library/socketserver.html#socketserver.BaseServer.shutdown

혹시 이걸 부르면 어떨까 싶어 테스트했지만,

def start_app():
    print('my app started')

    global server
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyTCPHandler)
    try:
        server.serve_forever()
    except:
        server.shutdown()

마찬가지입니다. ^^; 이때 netstat로 확인을 해보면 서버 소켓은 없지만 accept로 받았던 클라이언트 소켓이 TIME_WAIT 상태로 남아 있는 것을 확인할 수 있습니다.

// sudo apt install net-tools -y

$ netstat -ano | grep 8080
tcp        0      0 127.0.0.1:8080          127.0.0.1:54042         TIME_WAIT   timewait (57.05/0/0)

보통 TIME_WAIT 이후 2MSL(Maximum Segment Lifetime) 시간이 지나야 완전하게 소켓이 정리되는데요, (제가 테스트 중인) 리눅스 환경의 경우 60초로 설정돼 있습니다.

$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60

혹은 netstat 명령어의 출력에 보면 "timewait (57.05/0/0)"라고 나오는데, 바로 저 "57.05"가 TIME_WAIT이 정리될 때까지의 남은 시간(초)을 의미하므로 그걸로도 추측할 수 있습니다.




어쨌든, 이런 경우 전역 설정을 바꾸는 것은 조금 부담스러운데요,

python TCPServer address already in use but I close the server and I use `allow_reuse_address`
; https://stackoverflow.com/questions/15260558/python-tcpserver-address-already-in-use-but-i-close-the-server-and-i-use-allow

그렇군요, 리눅스도 socket reuse 옵션을 통해 곧바로 재사용할 수 있게 설정할 수 있습니다.

import socketserver

socketserver.TCPServer.allow_reuse_address = True

# ...[생략]...

if __name__ == '__main__':
    start_app()

이후, 종료/재실행을 해보면 정상적으로 서버 소켓이 바인딩되는 것을 확인할 수 있습니다.




그런데, 여기서 흥미로운 점이 하나 있는데요, 저 현상이 C#으로는 재현이 안 된다는 점입니다. 일례로, 다음과 같이 작성한 후,

internal class Program
{
    static void Main(string[] args)
    {
        using (Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);
            serverSocket.Bind(endPoint);

            serverSocket.Listen(10);

            Console.WriteLine("Server listening on port 8080...");

            while (true)
            {
                using (Socket clnt = serverSocket.Accept())
                {
                    Console.WriteLine("Client connected.");
                    clnt.Receive(new byte[4096]);

                    clnt.Send(System.Text.Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\n\r\nTEST IS GOOD"));
                }
            }
        }
    }
}

파이썬과 동일하게 테스트를 해보면 분명히 TIME_WAIT 상태의 클라이언트가 남아 있지만,

$ netstat -ano | grep 8080
tcp        0      0 127.0.0.1:8080          127.0.0.1:35742         TIME_WAIT   timewait (45.52/0/0)

재실행하면 serverSocket.Bind 코드가 잘 실행됩니다. (즉, 파이썬과는 달리 "Address already in use" 오류가 발생하지 않습니다.)

더욱 흥미로운 점이 있는데요, 저렇게 C#으로 작성한 서버 소켓을 실행/Accept/종료한 다음 이어서 파이썬을 실행시키면 "Address already in use" 오류가 발생한다는 점입니다.

// 정상 동작
C# Server 바인딩/Accept/종료 => C# Server 바인딩

// 오류 발생 (OSError: [Errno 98] Address already in use)
C# Server 바인딩/Accept/종료 => 파이썬 Server 바인딩 (allow_reuse_address = False)

반대의 경우에도 (allow_reuse_address = False가 적용된) 파이썬 소켓의 영향으로 C# 서버 소켓이 바인딩되지 않는데요,

// 오류 발생 (OSError: [Errno 98] Address already in use)
파이썬 Server 바인딩/Accept/종료 => 파이썬 Server 바인딩

// 오류 발생 (System.Net.Sockets.SocketException (98): Address already in use)
파이썬 Server 바인딩/Accept/종료 => C# Server 바인딩

도대체 파이썬의 serversocket은 어떤 동작을 추가로 하는 걸까요? ^^;




참고로, C#의 경우 ReuseAddress 옵션은 기본값이 false로 나옵니다.

using (Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
    object? objValue = serverSocket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress);
    Console.WriteLine($"ReuseAddress: {objValue}"); // ReuseAddress: 0

    IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);
    serverSocket.Bind(endPoint);

    // ...[생략]...

    while (true)
    {
        using (Socket clnt = serverSocket.Accept())
        {
            objValue = clnt.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress);
            Console.WriteLine($"ReuseAddress: {objValue}"); // ReuseAddress: 0

            // ...[생략]...
        }
    }
}

다시 말해 파이썬의 경우에는 저 값을 true로 설정하지 않으면 서버 소켓을 재사용할 수 없었지만, C#은 기본값이 false여도 서버 소켓을 재사용할 수 있었습니다. (물론, 파이썬 다음에 실행하면 C# 서버 소켓도 바인딩할 수 없었지만.)

혹시, 저런 현상이 왜 파이썬에서 나타나는지 아시는 분은 덧글 부탁드립니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/10/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  [142]  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1534정성태11/13/201327823.NET Framework: 392. .NET 스레드 콜 스택 덤프 (6) - MDbg를 이용한 방법 [2]파일 다운로드1
1533정성태11/12/201334847기타: 39. Internet Explorer 11에서 유튜브 동영상의 1080p 옵션이 보이지 않는 경우 [5]
1532정성태11/5/201335863Phone: 8. 안드로이드용 Xamarin 개발 시 겪을 만한 시행 착오 정리 [6]
1531정성태11/5/201327145VS.NET IDE: 82. Visual Studio에서 Attach 메서드를 이용해 디버깅을 시작한 경우 Breakpoint가 안 잡힌다면?
1530정성태11/5/201328660기타: 38. 오픈소스로 풀린 하드 디스크 관리 도구 - WindowSMART
1529정성태11/5/201324086오류 유형: 192. SQL 서버 - The transaction log for database '...' is full due to 'LOG_BACKUP'.
1528정성태11/5/201329861디버깅 기술: 58. windbg 분석 사례 - WPF 응용 프로그램의 UI가 반응하지 않는 문제 [5]
1527정성태11/4/201327762VC++: 72. error MIDL2311 - mktyplib compatability mode 컴파일 오류
1526정성태11/3/201324201디버깅 기술: 57. C# - double 값에 대한 windbg 확인
1525정성태11/2/201330886.NET Framework: 391. C# - EXE/DLL로부터 추출한 이미지/아이콘의 배경색 투명 처리 [8]
1524정성태11/2/201331735기타: 37. 프로그램에 보여지는 리소스(예: 아이콘) 추출하는 방법 [1]
1523정성태11/2/201328194VS.NET IDE: 81. Visual Studio 확장 도구 AttachToW3WP - w3wp.exe에 대한 디버거 연결을 자동화하는 도구 [2]
1522정성태11/1/201324628VS.NET IDE: 80. IIS 8.0/8.5 - Global.asax.cs처럼 초기에 실행되는 코드에 Breakpoint를 잡는 방법
1521정성태11/1/201330547VS.NET IDE: 79. IIS 7.5 - Global.asax.cs처럼 초기에 실행되는 코드에 Breakpoint를 잡는 방법
1520정성태10/31/201325050오류 유형: 191. Visual Studio 2010 - 웹 애플리케이션 생성 시 "The project type is not supported by this installation." 오류 발생 해결
1519정성태10/31/201350474기타: 36. SYSTEM 또는 TrustedInstaller 소유로 되어 있는 폴더/파일을 삭제하는 방법 [5]
1518정성태10/30/201328146VS.NET IDE: 78. Visual Studio 확장으로 XmlCodeGenerator 제작하는 방법
1517정성태10/28/201327722디버깅 기술: 56. 덤프 파일에 핸들/스레드 정보를 포함하는 방법 [1]
1516정성태10/28/201333117.NET Framework: 390. FolderBrowserDialog보다 더 쓸만한 대화창이 필요하다면? [1]
1515정성태10/24/201335622VS.NET IDE: 77. Visual Studio 확장(VSIX) 만드는 방법 [5]
1514정성태10/24/201369096개발 환경 구성: 202. Internet Explorer 11을 7, 8, 9, 10 버전으로 인식시키는 방법 [9]파일 다운로드1
1513정성태10/23/201325412개발 환경 구성: 201. Azure Blob Storage의 DNS 경로를 사용자 DNS로 바꾸는 방법 [1]
1512정성태10/18/201328638개발 환경 구성: 200. IIS AppPool의 실행 계정을 변경하는 방법
1511정성태10/12/201326665.NET Framework: 389. The 3n + 1 problem의 C#/Java 버전 풀이 [2]
1510정성태10/8/201327670오류 유형: 190. 윈도우 서버 2012 R2 설치 후 인텔 NIC으로 인한 WMI 오류 발생
1509정성태10/8/201332984오류 유형: 189. Windows Server 8.1/2012 R2 - IME 비정상 종료 현상 [1]
... 136  137  138  139  140  141  [142]  143  144  145  146  147  148  149  150  ...