-
TCP Socket 알아보기Network 2022. 12. 19. 22:59728x90반응형
- 목차
소개.
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