Search

Python TCP Server, Client 구현 방법

카테고리
Programming
태그
Python
Tech
게시일
2024/02/07
수정일
2024/02/24 09:37
시리즈
1 more property

TCP Client / Server Basic

Server(Code & Result)

아래의 코드는 기본 라이브러리인 socketserver를 이용하여 작성되었습니다.
import socketserver from datetime import datetime class TCPServerHandler(socketserver.BaseRequestHandler): def handle(self): buff_size = 2048 raw_payload = self.request.recv(buff_size) payload = raw_payload.strip() try: if payload != b'': self.request.sendall(f"I GOT YOUR DATA : {payload.decode()}".encode('utf-8')) self.logging(payload) except ValueError as e: print(e) def logging(self, payload): # self.client_address : (BIND_IP, BIND_PORT) print(f"[{str(datetime.now())}] {self.client_address} - {payload.decode()}") if __name__ == "__main__": _server_ip = "127.0.0.1" _server_port = 12321 server = socketserver.TCPServer((_server_ip, _server_port), TCPServerHandler) server.serve_forever()
Python
복사
아래의 결과에서는 bind 된 IP(127.0.0.1)와 서버의 로컬 포트(54296 ~ 54305) 입니다.
[2024-02-07 13:39:20.708781] ('127.0.0.1', 54296) - DATA00 [2024-02-07 13:39:20.709064] ('127.0.0.1', 54297) - DATA01 [2024-02-07 13:39:20.709288] ('127.0.0.1', 54298) - DATA02 [2024-02-07 13:39:20.709487] ('127.0.0.1', 54299) - DATA03 [2024-02-07 13:39:20.709672] ('127.0.0.1', 54300) - DATA04 [2024-02-07 13:39:20.709815] ('127.0.0.1', 54301) - DATA05 [2024-02-07 13:39:20.709948] ('127.0.0.1', 54302) - DATA06 [2024-02-07 13:39:20.710100] ('127.0.0.1', 54303) - DATA07 [2024-02-07 13:39:20.710245] ('127.0.0.1', 54304) - DATA08 [2024-02-07 13:39:20.710389] ('127.0.0.1', 54305) - DATA09
Python
복사

Client(Code & Result)

Client는 Server에 연결 후 데이터 송신/수신 과정이 이루어집니다. 아래의 코드에서는 반복문 내에서 다음과 같은 과정이 일어납니다.
1.
Client는 아이피(127..0.0.1), 포트(12321)를 서버로 설정함
2.
10개의 데이터를 List로 생성하여 저장함
3.
반복문 TCP 통신 로직 실행
a.
connect() 함수를 이용하여 서버에 3 Way Handshake 요청 후 연결
b.
sendall() 함수를 이용하여 서버에 데이터 송신함
c.
recv(1024) 함수를이용하여 최대 1024 버퍼만큼 데이터를 수신함
d.
송수신한 데이터를 출력함
4.
10개의 데이터에 대해 3번 로직이 완료될 때까지 반복함
import socket HOST, PORT = "127.0.0.1", 12321 data = [f"DATA{index:02d}" for index in range(10)] for d in data: # Create a socket (SOCK_STREAM means a TCP socket) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # Connect to server and send data sock.connect((HOST, PORT)) sock.sendall(bytes(d + "\n", "utf-8")) # Receive data from the server and shut down received = str(sock.recv(1024), "utf-8") print("Sent: {}".format(d)) print("Received: {}".format(received))
Python
복사
Client에서 위와 같이 TCP 연결 후 통신하는 코드를 작성하게 되면 다음과 같이 결과 값을 볼 수 있습니다.
Sent: DATA00 Received: I GOT YOUR DATA : DATA00 Sent: DATA01 Received: I GOT YOUR DATA : DATA01 ... Sent: DATA09 Received: I GOT YOUR DATA : DATA09
Python
복사

Description

이 과정은 다음과 같이 패킷 흐름을 확인할 수 있습니다. 처음 3-Way-Handshake를 마치고 데이터를 보내는 패킷으로써 [PSH, ACK] 플래그를 설정하여 전송합니다. 그리고 전송을 마치고 바로 [FIN, ACK] 패킷을 이용하여 연결을 종료합니다. 해당 내용을 해석하면 다음과 같습니다.
53982 -> 12321 [SYN] : `Client` "TCP 통신 시작하겠습니다." 12321 -> 53982 [SYN, ACK] : `Server` "확인했습니다. TCP 통신 시작하겠습니다." 53982 -> 12321 [ACK] : `Client` "확인했습니다." 12321 -> 53982 [ACK] : [TCP Window Update] - 흐름제어(슬라이딩 윈도우 기법 참고) 53982 -> 12321 [PSH, ACK] : `Client` "DATA00" 12321 -> 53982 [ACK] : `Server` "확인했습니다." 12321 -> 53982 [PSH, ACK] : `Server` "I GOT YOUR DATA : DATA00" 53982 -> 12321 [ACK] : `Client` "확인했습니다." 53982 -> 12321 [FIN, ACK] : `Client` "통신을 종료합니다." 12321 -> 53982 [ACK] : `Server` "통신 종료 확인했습니다."
Plain Text
복사
위와 같이 Client 코드를 작성하게 되면, Connect 1회당 통신 데이터 송수신 1회 후 통신이 종료됩니다. 따라서 위의 패킷처럼 데이터를 보낼 때마다 새로운 3-Way-Handshake를 수행하며, 서버의 로컬 포트가 달라지는 것을 볼 수 있습니다. 통신 자체에는 문제가 없지만, 왠지 찝찝합니다. 제가 모르는 OS 내의 문제가 발생할 수 있다는 생각에 다른 방법으로 통신이 유지되도록 하는 방법은 없을까 고민해보았습니다.

TCP Client / Server Advanced

Server(Code & Result)

아래의 코드는 위의 서버코드와 약간의 차이가 있습니다. TCPServer를 ThreadingTCPServer로 변경하였고, 핸들러 하나당 연결을 끊지 않고, 무한 반복문을 추가해주었습니다.
import socketserver from datetime import datetime class TCPServerHandler(socketserver.BaseRequestHandler): def handle(self): while True: buff_size = 2048 raw_payload = self.request.recv(buff_size) payload = raw_payload.strip() try: if payload != b'': self.request.sendall(f"I GOT YOUR DATA : {payload.decode()}".encode('utf-8')) self.logging(payload) except ValueError as e: print(e) def logging(self, payload): # self.client_address : (BIND_IP, BIND_PORT) print(f"[{str(datetime.now())}] {self.client_address} - {payload.decode()}") if __name__ == "__main__": _server_ip = "127.0.0.1" _server_port = 12321 server = socketserver.ThreadingTCPServer((_server_ip, _server_port), TCPServerHandler) server.serve_forever()
Python
복사
아래의 결과에서는 bind 된 IP(127.0.0.1)와 서버의 로컬 포트(54411) 입니다. 이번 코드에서는 로컬 포트가 하나로 유지되는 것을 확인할 수 있습니다.
[2024-02-07 13:46:20.616536] ('127.0.0.1', 54411) - DATA00 [2024-02-07 13:46:20.616635] ('127.0.0.1', 54411) - DATA01 [2024-02-07 13:46:20.616701] ('127.0.0.1', 54411) - DATA02 [2024-02-07 13:46:20.616774] ('127.0.0.1', 54411) - DATA03 [2024-02-07 13:46:20.616830] ('127.0.0.1', 54411) - DATA04 [2024-02-07 13:46:20.616900] ('127.0.0.1', 54411) - DATA05 [2024-02-07 13:46:20.616964] ('127.0.0.1', 54411) - DATA06 [2024-02-07 13:46:20.617019] ('127.0.0.1', 54411) - DATA07 [2024-02-07 13:46:20.617075] ('127.0.0.1', 54411) - DATA08 [2024-02-07 13:46:20.617124] ('127.0.0.1', 54411) - DATA09
Python
복사

Client(Code & Result)

아래의 코드는 기존의 코드와 약간의 차이가 있습니다. socket을 선언하고, 1회 연결을 수행합니다. 이후 데이터를 보내는 것은 10회 반복합니다. 그 과정을 요약하면 다음과 같습니다.
1.
Client는 아이피(127..0.0.1), 포트(12321)를 서버로 설정함
2.
10개의 데이터를 List로 생성하여 저장함
3.
connect() 함수를 이용하여 서버에 3 Way Handshake 요청 후 연결
4.
반복문 TCP 통신 로직 실행
a.
sendall() 함수를 이용하여 서버에 데이터 송신함
b.
recv(1024) 함수를이용하여 최대 1024 버퍼만큼 데이터를 수신함
c.
송수신한 데이터를 출력함
5.
10개의 데이터에 대해 4번 로직이 완료될 때까지 반복함
import socket HOST, PORT = "127.0.0.1", 12321 data = [f"DATA{index:02d}" for index in range(10)] # Create a socket (SOCK_STREAM means a TCP socket) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) for d in data: # Connect to server and send data sock.sendall(bytes(d + "\n", "utf-8")) # Receive data from the server and shut down received = str(sock.recv(1024), "utf-8") print("Sent: {}".format(d)) print("Received: {}".format(received))
Python
복사
위의 Client 결과값은 앞서 설명드린 결과 값과 다르지 않습니다.

Description

이 과정은 다음과 같이 패킷 흐름을 확인할 수 있습니다. 처음 3-Way-Handshake를 마치고 데이터를 보내는 패킷으로써 [PSH, ACK] 플래그를 설정하여 전송합니다. 연결을 끊지 않고 데이터를 모두 전송할 때까지 반복합니다. 그리고 반복문을 마치면 바로 [FIN, ACK] 패킷을 이용하여 연결을 종료합니다. 해당 내용을 해석하면 다음과 같습니다.
54411 -> 12321 [SYN] : `Client` "TCP 통신 시작하겠습니다." 12321 -> 54411 [SYN, ACK] : `Server` "확인했습니다. TCP 통신 시작하겠습니다." 54411 -> 12321 [ACK] : `Client` "확인했습니다." 12321 -> 54411 [ACK] : [TCP Window Update] - 흐름제어(슬라이딩 윈도우 기법 참고) 54411 -> 12321 [PSH, ACK] : `Client` "DATA00" 12321 -> 54411 [ACK] : `Server` "확인했습니다." 12321 -> 54411 [PSH, ACK] : `Server` "I GOT YOUR DATA : DATA00" 54411 -> 12321 [ACK] : `Client` "확인했습니다." 54411 -> 12321 [PSH, ACK] : `Client` "DATA01" 12321 -> 54411 [ACK] : `Server` "확인했습니다." 12321 -> 54411 [PSH, ACK] : `Server` "I GOT YOUR DATA : DATA01" 54411 -> 12321 [ACK] : `Client` "확인했습니다." ... 54411 -> 12321 [PSH, ACK] : `Client` "DATA09" 12321 -> 54411 [ACK] : `Server` "확인했습니다." 12321 -> 54411 [PSH, ACK] : `Server` "I GOT YOUR DATA : DATA09" 54411 -> 12321 [ACK] : `Client` "확인했습니다." 54411 -> 12321 [FIN, ACK] : `Client` "통신을 종료합니다." 12321 -> 54411 [ACK] : `Server` "통신 종료 확인했습니다."
Plain Text
복사

Exceptions

[Error 54] Connection reset by peer

만약 두 번 째 Advanced 코드를 실행하고 Client 코드 실행 후 얼마 지나지 않아 아래와 같은 에러가 발생할 수 있습니다. 이는 생성된 서버의 Thread 에서 더는 수신할 데이터가 없어서 Timeout 과 비슷한 에러가 발생한 것입니다. 때문에 recv 로직 부분에서 ConnectionResetError 예외처리를 해주면 됩니다. 해당 에러가 발생했다고 하더라도 새로운 연결을 수행하는데 문제는 없습니다.
Exception occurred during processing of request from ('127.0.0.1', 54485) Traceback (most recent call last): File "/Users/kkamikoon/miniforge3/envs/DUMMY/lib/python3.11/socketserver.py", line 691, in process_request_thread self.finish_request(request, client_address) File "/Users/kkamikoon/miniforge3/envs/DUMMY/lib/python3.11/socketserver.py", line 361, in finish_request self.RequestHandlerClass(request, client_address, self) File "/Users/kkamikoon/miniforge3/envs/DUMMY/lib/python3.11/socketserver.py", line 755, in __init__ self.handle() File "/Users/kkamikoon/Documents/DUMMY/tcp_test/server.py", line 9, in handle raw_payload = self.request.recv(buff_size) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ConnectionResetError: [Errno 54] Connection reset by peer ----------------------------------------
Python
복사
PID와 Thread의 Native ID를 참고하면 아래와 같습니다.
{14:23}~/Documents/DUMMY/tcp_test ➭ python3 server.py [ Main ] PID : 67841, THREAD : 12700037 [Handle] PID : 67841, THREAD : 12700063 [2024-02-07 14:23:50.123772] ('127.0.0.1', 55039) - DATA00 [Handle] PID : 67841, THREAD : 12700063 [2024-02-07 14:23:50.123842] ('127.0.0.1', 55039) - DATA01 [Handle] PID : 67841, THREAD : 12700081 [2024-02-07 14:23:50.401368] ('127.0.0.1', 55040) - DATA00 [Handle] PID : 67841, THREAD : 12700081 [2024-02-07 14:23:50.401433] ('127.0.0.1', 55040) - DATA01 [Handle] PID : 67841, THREAD : 12700100 [2024-02-07 14:23:50.636755] ('127.0.0.1', 55042) - DATA00 [Handle] PID : 67841, THREAD : 12700100 [2024-02-07 14:23:50.636802] ('127.0.0.1', 55042) - DATA01
Python
복사

References