센로그

4. Socket 본문

CS/풀스택서비스네트워킹

4. Socket

seeyoun 2023. 9. 30. 13:33

※ 참고 포스팅

https://grace7040.tistory.com/34

 

Chapter 23. TRANSPORT LAYER : UDP AND TCP

◆ Process-to-Process Delivery 데이터 링크 계층은 node-to-node (hop-to-hop) 네트워크 계층은 host-to-host 전송 계층은? process-to-process 서버-클라이언트 의 관계를 갖고 있음. (서버-클라이언트 프로세스는 서로

grace7040.tistory.com


◆ Socket

소켓은 L3과 L4 사이의 커뮤니케이션 인터페이스이다. API.

어플리케이션은 소켓을 열고, 소켓에 정보를 주어서 상대 어플리케이션까지 가고.. 하면서 통신함.

소켓을 사용한 프로그램들은 bidirectional을 지원한다. (보내면서 받을 수 있음)

소켓 프로그래밍을 통해 한 컴퓨터 내의 프로그램끼리 통신하는 것도 가능하다.

 


◆ 소켓 서버 - 소켓 클라이언트

서버는 주로 리슨하며, 소켓을 통해서 정보를 요청 받음.

클라이언트는 리슨하고있는 서버에게 정보를 요청함.

 


◆ Berkeley Socket API Functions

버클리 소켓 API 함수들 - 많이 활용되는 것들 위주 설명

 

  • socket()
    소켓을 여는 과정. 운영 체제로부터 포트 번호를 요청해서 받고, 시스템 리소스 할당 받음.
    • 반환값 -1 : 에러
  • bind()
    통신을 위해 필요한 정보인 IP주소와 포트 번호를 OS에게 전달하여 소켓-운영 체제(의 4계층)를 연결함.
    클라-서버 구조에서는, 서버에서 사용함.
    • 반환값 -1 : 에러
    • 반환값 0 : 성공
  • listen()  (TCP)
    연결 요청 기다림.
    클라-서버 구조에서는, 서버는 먼저 살아나서 클라가 접속하길 기다려야 함.
    (UDP는 리슨 없이 그냥 클라가 쏘면 받는다)
    • 반환값 -1 : 에러
    • 반환값 0 : 성공
  • connect()  (TCP)
    연결 요청.
    클라-서버 구조에서는, 클라가 연결 요청을 함.
    서버의 IP주소 및 포트 번호를 이용해 연결 요청을 함. 
    • 반환값 -1 : 에러
    • 반환값 0 : 성공
  • accept()  (TCP)
    연결 요청을 수락
    리슨하고 있는 서버가, 클라로부터 받은 연결 요청을 수락한다.

    처음에 만들었던 소켓은 연결을 위한 소켓. 즉 서버 본인만의 정보를 갖고있던 소켓이다.
    이제 연결을 하면, 내 정보와 상대 정보를 쌍으로 가지고 있는 새 소켓을 만든다. 이 과정에서 상대를 받아들이고 메모리 할당하는 등의 작업을 한다.
    • 반환값 -1 : 에러
    • 반환값 0 : 성공
  • send(), recv(), sendto(), recvfrom()
    이 함수들을 이용해 실제로 데이터를 주고 받음. 
  • close()
    소켓을 닫는 과정. 소켓에 할당했던 리소스를 회수한다.
    TCP의 경우, 통신을 위해 했던 연결을 종료한다. 
  • gethostbyname(), gethostbyaddr()
    이름이나 ip주소를 통해 host를 알아내는 함수.
  • getsocketopt()
    특정 소켓의 옵션을 건들고 싶을때 사용

 

※ 기억해야 할 것
결코 연결을 반드시 성공한다는 보장은 없다!!!
잘못된 IP 주소와 포트 번호를 가지고 연결 요청을 하거나, 서버가 더 이상 연결 요청을 받을 수 없어서 연결 해제를 요청할 수도 있다. 연결 실패할 수도 있다는 것. bind 요청도 운영 체제가 무조건 받아주는 건 아님. listen도 실패할 수 있음. 
→ 충분히 연결 실패를 할 수 있다는 사실을 이해하고, 안 됐을 경우 잘 처리해줘야 한다. (예외 처리 및 재시도. 소켓 만드는 과정부터 다시 시도하는 과정)

 


◆ TCP에서, Berkeley Socket API 작동 과정

클라-서버 프로그램은 이 순서에 맞추어 소켓 API를 호출한다.

 

서버 입장에서 보자.

  • socket(): 소켓 호출. 어플리케이션이 소켓을 쓰겠다고 선언.
  • bind(): 운체와 앱(소켓) 연결
  • listen(): 클라의 연결 요청 기다림
  • accept(): 클라가 소켓을 만들고, 이를 통해 connect요청시 수락
  • send(), recv(): 실제로 데이터를 주고 받음
  • close(): 연결 종료

 

UDP의 경우, 서버는 미리 socket 만들고 bind 후 recv 상태로 있고, 클라는 보낼 거 있으면 연결 없이 바로 send

, listen, accept, close 과정이 없다.

 

ex) 엄청나게 간단하게 짠 TCP 에코 서버-클라이언트 코드 예제

더보기
엄청나게 간단하게 짠 TCP 에코 서버 코드

 

엄청나게 간단하게 짠 TCP 에코 클라이언트 코드

클라에서 HOST 랑 PORT는 연결하려는 서버 꺼 말하는거임. 자기꺼 아님.

 

ex) 실패를 고려한 예외처리 코드를 넣은 TCP 에코 서버-클라이언트 코드 예제

더보기

예외처리한 TCP 에코 서버

 

 

예외처리한 TCP 에코 클라이언트

 


◆ TCP로 1:N 에코서버 개발 (동기식)

앞서 봤던 에코서버는 1:1만 가능한 에코서버였다. 이번에는 1:N을 구현해보자.

 

예제에서는 socketserver라는 파이썬 모듈을 통해서 구현할 것이다. 

그중 socketserver.TCPServer 클래스를 사용해볼 것.

 

class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

: TCP 프로토콜을 지원하는 클래스

아~ 소켓 프로그래밍 순서 다 똑같던데, 굳이 소켓 열고, 바인드하고, 리슨해야 돼? 걍 한번 호출하고 통째로 하면 안돼??

 bind_and_activate 옵션 같은 것들 추가됨

(대부분의 언어들 내에서 버클리 소켓 API를 일일이 다 부르지 않는 방향으로 많은 편리한 모듈들이 이미 만들어져있다.)

 

 

 

< 과정 >

1. request handler 생성

socketserver.BaseRequestHandler을 상속하는 리퀘스트 핸들러 클래스를 만든다.

이 클래스를 통해 클라로부터 데이터를 받고, 다시 보낸다,

2. 서버 클래스에게 내가 만든 request handler 전달

3. 서버 실행. serve_forerver()

4. 소켓 닫기. server_close()

 

< 결과물 >

- 서버 코드

 

... 그런데 이거, 잘 작동할까?

ㄴㄴ. 동시에 지원가능한 클라이언트가 1개밖에 없다는 문제점이 있다.

따라서 서버가 어떤 클라와 통신중일 때, 다른 클라는 서비스중인 클라의 연결 종료시까지 기다려야 한다. ㅠ

=> 멀티스레드 사용해서 해결하자!!

 


 통신 프로그램 개발시 Multi Thread 기술을 사용하는 이유
- 서버와 클라이언트 각각의 경우에 대하여

서버: 기존에 소켓 API만을 사용하여 1:N 통신을 구현하는 경우, 동기식으로 구현된다. 따라서 각 요청은 다음 요청을 시작하기 전에 완료해야 하는데, 클라가 처리하는 데 느린 데이터를 주거나 많은 계산이 필요한 경우에 시간이 너무 오래 걸린다. 따라서 클라가 하나가 아닌 N개가 된다면 제대로 통신할 수 없다. (동시 지원 가능한 클라는 1개)
=> 여러 클라이언트와 동시에 연결하고 각 클라이언트의 요청을 독립적으로 처리해주기 위해 멀티 스레드를 사용한다. 멀티 스레드를 사용해, 각 클라마다 스레드를 할당해주면 비동기적으로 1:N 통신이 가능해진다. 

클라: 메시지 전송 및 수신을 비동기적으로(동시에) 처리하기 위해 멀티 스레드를 사용한다

 


◆ TCP로 1:N 소켓서버 개발 (비동기식)

< 과정 >

1. 소켓서버의 멀티스레드 버전 클래스 생성

socketserver 라이브러리를 활용해, 멀티스레드가 되는 TCP 소켓 서버 클래스를 생성한다. 

socketserver.TCPServer 클래스 베이스 클래스로, ocketserver.ThreadingMixIn클래스를 믹스인 클래스로 상속받음으로써 구현할 수 있다.

 

Mix-In class란?

상속시 다른 클래스에 섞여서 도움을 주는 클래스.

베이스 클래스를 상속할건데, 뭔가 기능이 좀 부족하다!? 이럴 때 사용함.어떤 클래스든 쓰면 유용할 Utility같은 클래스를 주로 믹스인 클래스로 사용한다.

 

ThreadedTCPServer가 socketserver.TCPServer 클래스를 상속할 때, socketserver.ThreadingMixIn클래스를 살짝 섞었다는 뜻.

 

위 코드에서 두 클래스가 충돌을 일으키면, 베이스 클래스가 아닌 믹스인 클래스의 것으로 사용한다. 베이스 클래스의 싱글스레드 전용 함수가 믹스인 클래스의 멀티 스레드용 함수에 있었다면, 믹스인 클래스 껄로 덮어씌운다는 것. (왼쪽 꺼로 오른쪽을 덮어씌움)

 

 

2. socketserver의 Request Handler을 살짝 수정

핸들러는 기본적으로 핸들러에 있는 내용들이 하나하나 스레드로 뜰 것이므로, 굳이 멀티스레드를 구현할 필요는 없다. 

대신, 이 지금 동작하고 있는 스레드가 어느 스레드인지 보여주기 위한 코드를 추가한다.

 

threading 라이브러리의 current_thread()를 사용한다.

 

 

 

3. 메인 스레드 설정 및 실행

메인 부분도 스레드로 만들 것임. 메인과 핸들러 둘다 독립적으로 돌아가는 스레드로 설정할 것.

 

  • server_thread = threading.Thread(target = server.server_forever) 부분에서, 서버 스레드를 만들었다.
    threading.Thread()는 생성자임. 스레드를 하나 만들겠다는 것.
    파라미터의 경우, 이 스레드를 실행(run)할 때 server.serve_forever()을 실행하겠다! 라는 의미임

 

 

  • Thread.daemon = true이면, 메인이 죽으면 메인에서 파생된 스레드들도 같이 죽는 것. 
    디폴트는 false인데, 그러면 메인이 죽어도 파생 스레드들은 각자 살아서 돌아감.

 

  • server_thread.start()를 하면 스레드가 실행됨.

 

  • 그 다음에 나오는 while문은 서버 운영자가 서버를 멈추고 싶을 때에 관한 내용임.
    우선 클라가 연결하기 전에 baseThreadNumber 변수에다 현재 돌고있는 스레드 수를 받아놓음. (보통 메인 1개)
    클라가 연결을 하면, 그에 따라 리퀘스트 핸들러를 스레드 단위로 띄우기 시작할 것임.

    서버 운영자가 메세지로 quit를 입력하면 서버가 종료되도록 하되,
    만약 현재 돌고있는 스레드가 메인 이외에 있는 경우(기존 스레드 개수와 다른 경우에는 생성되어 작동중인 스레드들이 있다는 것), "아직 스레드 남아있네? 종료 못해. 걔 끝날 때까지 기다려." 라고 한 것임.

 

 

 

< 결과물 >

- 서버 코드

더이상 연결 없는 경우에만 서버 quit 할 수 있도록 처리했다.

더이상 연결이 없는 경우에 while문을 빠져나와서 server.shutdown()이라는 코드를 사용해 아름답게 서버를 끌 수 있게 되었다.

 


◆ TCP로 N:M 채팅 프로그램 개발

앞에서 봤던 비동기식 1:N 통신 프로그램을 수정해서 사용할 것이다.

 

변경사항은 다음과 같다.

 

서버 변경사항

  • 모든 client의 socket 연결 정보를 저장해 둠 (등록 및 삭제)  
  • 특정 client가 전송한 메시지를 모든 client들에 전달함

 

 

클라 변경사항

  • Thread를 사용하여, 전송과 수신을 비동기적으로 분리함. (전송과 수신이 동시에 이루어질 수 있도록 함) 
    => 수신부를 Thread화 하여, 서버로부터의 전송에 항상 준비함

 

코드를 보자.

1. 서버 코드

  • 기본적으로, 앞서와 같이 RequestHandler을 스레드 단위로 동작하도록 구현한다.
    RequestHandler는 누군가로부터 연결 요청 받았을 때 실행될 것임.

 

  • 글로벌 변수로 정의한 group queue에다가 연결 요청한 클라의 소켓 정보를 append한다.

 

  • quit을 받았다면 해당 클라는 그만 통신하겠다는 얘기이므로 큐에서 제거함.

 

  • 마지막으로, 모든 살아있는 클라들(큐에 있는 클라들)에게 받은 메시지를 sendall하는 기능을 추가했다.

 

  • 이 부분은 변경사항 없다.

 

 

 

2. 클라 코드

  • recvHandler : 원래는 전송/수신이 RequestHandler 안에 같이 있었는데, 둘을 분리했다. 
    그리고 수신 기능만 넣은 recvHandler을 만들어 스레드 단위로 동작하도록 한다.
    (quit 수신시 스레드 종료. 서버쪽에서 quit의 경우 브로드캐스트 안하도록 짰음 ㅇㅇ)

 

  • 메인에서 사용자 입력 들어오면 보내도록 하고, quit이 입력으로 들어오면 메인을 종료하도록 함.

 


◆ UDP로 1:N 에코서버 개발 (동기식)

앞서 봤던 TCP 1:N 에코서버(동기식)에서 조금만 바꾸면 UDP로 바꿀 수 있다.

오히려 엄청 줄었다. UDP에는 listen, connect 같은 과정이 필요 없기 때문임.

 

1. 서버 코드

굉장히 간단하다.

우선 소켓 열 때, socketserver.UDPServer를 사용서 연다.

(TCP때는 socketserver.TCPServer로 열어서, 리슨하고 있었을 텐데 여기선 안한다.)

그리고 똑같이 리퀘스트 핸들러를 통해서 받은 메세지를 그대로 에코해준다.

 

 

2. 클라 코드

얘도 굉장히 간단하다. 

소켓 만들 때 socket.SOCK_DGRAM으로 연다. (Datagram은 UDP에서의 데이터 전송 단위이다.)

이후, TCP와 다르게 connect 없이 메시지를 보내고, close없이 종료한다.

 

(TCP 멀티스레드 버전 갖고와서 수정한거라 recv부분이 스레딩 된 것으로 보인다. 딱히 필요는 없어 보임)

 


◆ UDP로 N:M 채팅 프로그램 개발

 

< TCP 대비 추가적인 작업 >

클라들이 "나 지금 니 통신에 등록할게!" 라는 의미의 특수 명령어를 사용하도록 한다.

예제에서는 #REG, #DEREG를 각각 등록, 해제하는 명령어로 사용한다. (클라가 직접 입력)

그리고 등록된 클라로부터 메세지를 받으면, 등록된 모든 클라들에게 해당 메세지를 브로드캐스팅한다.

 

< 해야하는 이유 >

TCP 때는 통신 전에 서버-클라를 연결하기 때문에, 서버가 active한 소켓 연결 정보를 살펴보면 어느 유저가 연결했는지 알 수 있었다. 그래서 연결된 유저들에게 받은 메세지를 브로드캐스팅 해줄 수 있었다.

 

그러나 UDP는 연결을 하지 않기 때문에, 현재 나랑 통신하고 있는 유저들의 정보를 알 길이 없다.

 

따라서 서버에서 직접 클라들의 정보를 저장하고 관리하는 기능이 필요하다.

 

 

 

이제 코드를 보자.

1. 서버 코드

서버는 클라로부터 #REG 메세지를 받으면 해당 클라의 소켓 정보를 자신의 등록 클라 리스트(group_queue)에 추가한다.

#DEREG 메세지를 받으면 클라 리스트에서 해당 클라를 삭제한다.

 

기존에 TCP 채팅 서비스 했을 때처럼 받은 메시지를 등록된 클라들한테 브로드캐스트하는 코드이다.

 

이때, REG안된 애가 메시지를 보내온 경우(UDP니까 아무나 그냥 보낼 수 있음)에 걍 무시하는 코드가 추가되었다.

'CS > 풀스택서비스네트워킹' 카테고리의 다른 글

6. HTTP/1.1  (0) 2023.10.16
5. ZeroMQ  (1) 2023.10.15
3. OSI Arghitecture L4  (0) 2023.09.30
2. OSI Architecture (L1~L3)  (0) 2023.09.07
1. OSI Architecture Overall  (0) 2023.09.05
Comments