ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCP Socket 알아보기
    Network 2022. 12. 19. 22:59
    728x90
    반응형

    - 목차

     

    소개.

    TCP 소켓에 대해서 가볍게 알아보는 시간을 가지려고 합니다.
    소켓은 TCP 소켓과 같은 네트워크 통신 뿐만 아니라
    IPC (Inter Process Communication) 에서도 Socket 방식의 통신 기법이 있는데요.
    두 소켓 통신의 차이점은 "네트워크 스택을 사용하는가 사용하지 않는가?" 입니다.
    TCP 소켓은 IP 와 PORT 정보가 필요한데 반해, IPC Socket 통신은 그렇지 않죠.
     
    Socket 통신은 여러 System Call 을 필요로합니다.
    NIC(Network Interface Card), Operating System 와 소통하고 도움을 받아야하기 때문에 System Call 이 사용됩니다.
    사용되는 네트워크 System Call 의 종류로는

    - socket
    - bind
    - listen
    - accept
    등이 있습니다.
     
    이러한 전반적인 내용들에 대해서 간단히 설명하고,
    관련 예시 코드 또한 추가해보려고 합니다.
     

    Socket Stream.

    Socket Stream 네트워크 트래픽을 저장할 Stream 입니다.
    Socket Stream 은 단순히 Buffer 나 List 같은 자료구조 형식의 데이터 저장 영역입니다.
    NIC(Network Interface Card) 를 통해서 전달되는 네트워크 트래픽들이 Socket Stream 에 차곡차곡 쌓이게 됩니다.
     
    Socket Stream 은 Network 관련 System Call 중에서 socket System Call 에 의해서 생성됩니다.
     

    import socket
    
    def main():
    	sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

     
    socket System Call 을 전달받은 OS 는 Socket Stream 을 생성해줍니다.
    OS 관점에서 Socket Stream 은 하나의 File 로 볼 수 있고,
    그렇기 때문에 socket SystemCall 의 결과로 File Descriptor (sockfd) 를 리턴해줍니다.
     
    파일 I/O 관련 프로그래밍을 할 때, File 을 open 하여 write, append 하여 내용을 작성하듯이,
    Socket 또한 NIC 를 통한 Network IO 가 수행된다고 생각하시면 됩니다.
     

    System Call.

    네트워크와 관련된 여러 System Call 이 존재합니다.
    네트워크 System Call 들의 역할은 아래와 같습니다.
    "Socket Stream 을 생성해줘."
    "Socket Stream 을 특정 Port 랑 연결해줘."
    "Client 과 Connection 을 맺을 Connection Pool 만들어줘."
     
    네트워크 System Call 에 대해서 하나씩 알아보도록 하겠습니다.
     

    socket.

    socket System Call 은 OS 에게 Socket Stream 의 생성을 요청합니다.
    OS 는 System Call 을 요청한 프로세스를 위해서 Socket Stream 을 생성해줍니다.
     

    bind.

    bind System Call 을 통해서 Socket Stream 과 Network Interface Card 를 연결합니다.
    Socket Stream 과 NIC 를 연결하기 위해서는 Port 라는 매개체가 필요한데요.
    Port 는 네트워크 트래픽이 최종적으로 전달되는 도착지 정보입니다.
    네트워크 트래픽이 무수히 복잡한 인터넷 연결망을 지나 서버로 도착했다면,
    지정된 Port 로 이동하여 긴 여정을 끝마칩니다.
     

    import socket
    
    def main():
      # Create a TCP socket
      sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
      # Bind the socket to a specific address and port
      sockfd.bind(('localhost', 8080))

     
    socket System Call 호출로 얻게된 Socket 의 File Descriptor 를 통해서 Socket Stream 과 NIC 를 연결할 수 있구요.
    여기서 고유한 port 번호를 통해서 연결하게 됩니다.
     
    내부적으로는 OS 가 Port Table 이라는 자료구조를 통해서 Port 와 Socket Stream 을 관리하구요.
    NIC 가 네트워크 트래픽을 수신하게 되면,
    Interrupt 를 통해 우선적으로 네트워크 트래픽을 처리하게 됩니다.
    이 과정에서 OS 는 Port 에 매칭되는 Socket Stream 에 해당 네트워크 트래픽을 write 하게 되죠.
     

    Port Table.

    Port Table 은 netstat 와 lsof 커맨드를 통해서 확인할 수 있습니다.

    netstat -ant | grep 12345
    tcp4       0      0  127.0.0.1.12345        *.*                    LISTEN

     

    lsof -i:12345
    COMMAND    PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
    python3.9 4083 xxxxxxxxxx    5u  IPv4 0xxxxxxxxxxxxxxxxx      0t0  TCP localhost:italk (LISTEN)

     

    listen.

    listen System Call 이 호출됨으로써 Socket Stream 은 Listen 상태가 됩니다.
    Listen 상태는 본격적으로 클라이언트와 커넥션을 맺을 수 있음을 뜻합니다.
    Listen System Call 은 Socket Stream 생성 이후에 얻게된 File Descriptor 를 활용해서 호출할 수 있는데요.
    backlog 라는 파라미터를 통해서 최대 Pending Connection 을 맺을 수 있는 클라이언트의 수를 제한할 수도 있습니다.
    일반적으로 제한없는 많은 수의 요청을 처리할 수 있기만,
    backlog 라는 Pending Connection 의 수를 지정하는 버퍼가 추가적으로 존재합니다.
     

    import socket
    
    def main():
      # Create a TCP socket
      sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
      # Set the listen size to 20
      sockfd.setsockopt(socket.SOL_SOCKET, socket.SO_BACKLOG, 20)
    
      # Bind the socket to a specific address and port
      sockfd.bind(('localhost', 8080))
    
      # Listen for incoming connections
      sockfd.listen(20)

     

    backlog.

    backlog 에 대해서 좀더 살펴보겠습니다.
    Socket System Call 을 통해서 Socket Stream 을 생성할 때에 backlog 또한 함께 생성됩니다.
    backlog 는 클라이언트의 Pending Connection 요청들이 저장되는 영역입니다.
    OS 가 관리하는 In-Memory 자료구조이구요.
    Accept 되지 않은 Pending Connection 들이 저장됩니다.

    위 예시에선 backlog 의 사이즈를 20으로 설정하였습니다.
    이는 동시다발적인 20개의 커넥션 요청을 처리할 수 있음을 뜻합니다.
    물론 한꺼번에 20개의 커넥션을 생성할 순 없지만,
    backlog 이라는 버퍼를 통해서 20개의 커넥션을 생성할 수 있습니다.
    그러나 backlog limit 을 초과하는 커넥션 요청에 한해서는 Reject 될 수 있습니다.

    참고로 backlog 는 queue 기반의 자료구조 형식을 따른답니다.
    이렇게하여 클라이언트의 커넥션 요청의 순서대로 accept 과정을 처리합니다.



    accept.

    accept System Call 은 클라이언트와 통신을 위한 새로운 Socket 를 생성합니다.
    listen System Call 단계에서 클라이언트와 연결을 위한 준비를 마쳤습니다.
    Listen 상태가 된 Socket Stream 은 클라이언트의 연결 요청을 기다리는데요.
    클라이언트의 요청은 Queue 자료구조에 쌓이게 됩니다.
     
    accept System Call 은 대기중인 클라이언트의 연결 요청을 Queue 에서 하나씩 꺼내와
    Client 와의 통신을 위한 새로운 Socket Stream 을 생성합니다.
    그리고 해당 File Descriptor 를 반환하죠.
     
    이 단계에서 연결 과정은 마무리됩니다.

    def main():
        """Creates a TCP server that can handle multiple client connections."""
    
        # Create a TCP socket
        sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
        # Bind the socket to a specific address and port
        sockfd.bind(('localhost', 12345))
    
        # Listen for incoming connections
        sockfd.listen(2)
    
        # Accept incoming connections
        while True:
            connfd, addr = sockfd.accept()

     

     

    recv.

    accept system call 을 통해서 Client 와의 커넥션이 생성됩니다. ( 이를 accepted connection 이라고 함. )
    이 커넥션은 socket-bind-listen system call 들로부터 생성되는 커넥션과 다릅니다.
    accept system call 로부터 생성된 커넥션은 비로소 Client - Server 통신을 가능하게 합니다.

    recv 는 Client 로부터 요청받은 데이터들을 읽어들입니다.
    accept 과정에서 소켓 스트림이 생성되고,
    recv 는 생성된 소켓 스트림의 File Descriptor 를 활용합니다.
    Client 는 send system call 을 통해서 데이터를 전달하구요. ( accepted socket 에 데이터가 저장됨.)
    Server 는 recv 를 통해 데이터를 읽어들입니다. ( accepted socket 의 데이터를 읽어들임. )
    TCP 통신은 이러한 과정의 무한 반복입니다.
    아래는 tcp socket server 와 client 의 통신을 위한 예시입니다.
     
    <server.py>

    import socket
    import threading
    
    
    def handle_client(connfd):
        """Handles a client connection."""
    
        # Receive data from the client
        data = connfd.recv(1024)
    
        print(data)
    
        # Send data back to the client
        connfd.send(data)
    
        # Close the connection
        connfd.close()
    
    
    def main():
        """Creates a TCP server that can handle multiple client connections."""
    
        # Create a TCP socket
        sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
        # Bind the socket to a specific address and port
        sockfd.bind(('localhost', 12345))
    
        # Listen for incoming connections
        sockfd.listen(2)
    
        # Accept incoming connections
        while True:
            connfd, addr = sockfd.accept()
            print(addr)
            # Handle the client connection in a separate thread
            t = threading.Thread(target=handle_client, args=(connfd,))
            t.start()
    
    
    if __name__ == '__main__':
        main()

     
    <client.py>

    import socket
    
    # Create a socket object
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Define the server address (IP and port)
    server_address = ('127.0.0.1', 12345)
    
    # Connect to the server
    client_socket.connect(server_address)
    
    # Send data to the server
    message = "Hello, server!"
    client_socket.sendall(message.encode('utf-8'))
    
    
    # Close the client socket
    client_socket.close()

     
    <실행 결과>

    Connected to pydev debugger (build 231.9161.41)
    ('127.0.0.1', 50155)
    b'Hello, server!'
    ('127.0.0.1', 50159)
    b'Hello, server!'
    ('127.0.0.1', 50171)
    b'Hello, server!'
    ('127.0.0.1', 50175)
    b'Hello, server!'
    ('127.0.0.1', 50182)
    b'Hello, server!'


    위의 예시는 클라이언트 - 서버 간의 일회성 통신입니다.
    이를 short-lived 라고도 부릅니다.
    대표적으로 HTTP 가 위와 같은 short-lived 구조의 TCP 통신을 수행합니다.

    반면 long-lived 구조의 TCP 통신은 별도의 쓰레드에서 recv 과정을 수행합니다.
    그래서 클라이언트와 서버는 accepted socket 를 close 하지 않고 계속 사용할 수 있습니다.

    import socket
    
    def start_server():
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_address = ('localhost', 8080)
    
        server_socket.bind(server_address)
        server_socket.listen(5)  # Listen for incoming connections with a backlog of 5
    
        print(f"Server listening on {server_address}")
    
        while True:
            print("Waiting for a connection...")
            client_socket, client_address = server_socket.accept()
            print(f"Connection established with {client_address}")
    
            handle_client(client_socket)
    
    def handle_client(client_socket):
        try:
            while True:
                data = client_socket.recv(1024)
                if not data:
                    break  # No more data, client has closed the connection
                print(f"Received from {client_socket.getpeername()}: {data.decode()}")
                # Process the data...
    
                # Optionally, you can respond to the client
                response = "Message received!"
                client_socket.sendall(response.encode())
        finally:
            print(f"Closing connection with {client_socket.getpeername()}")
            client_socket.close()
    
    if __name__ == "__main__":
        start_server()
    






    반응형

    'Network' 카테고리의 다른 글

    http multipart request  (0) 2023.03.11
Designed by Tistory.