본문 바로가기
컴퓨터과학/네트워크

2. 소켓 프로그래밍

by 니키티스 2021. 10. 6.

해당 글은 한양대학교 이석복 교수님의 과목 "컴퓨터 네트워크"를 공부하고 작성한 글입니다. 해당 강의를 직접 수강하시려면 다음 링크를 참고해주세요.컴퓨터 네트워크 - 한양대학교 | KOCW 공개 강의

 

컴퓨터네트워크

인터넷을 동작시키는 컴퓨터네트워크 프로토폴을 학습한다.

www.kocw.net

 

소켓 프로그래밍

소켓은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점이다.

 

개발자는 운영체제에 이미 구현된 소켓을 통해 애플리케이션 프로세스 간 통신을 할 수 있으므로, 소켓은 통신 API로도 볼 수 있다.

 

소켓은 하위 레벨인 Transport 계층에 의존한다. 소켓을 사용할 때, TCP 방식을 이용하려면 TCP를 위한 소켓, UDP 방식을 이용하려면 UDP를 위한 소켓을 생성해야 한다.

 

어떤 프로토콜을 사용하는지에 따라 소켓의 종류를 나눌 수 있는데,

  • TCP일 경우에는 TCP 소켓
  • UDP일 경우에는 UDP 소켓으로 나눌 수 있다.

소켓을 통해 통신하는 방법은 위와 같다. 여기서는 대표적으로 클라이언트-서버 모델을 사용한다.

 

(1) 소켓을 통한 통신 과정

1) 서버 생성

소켓을 통해 통신하는 과정

처음에는 서버 측부터 시작한다.

집주인(서버)이 집을 안 열었는데 손님(클라이언트)이 찾아올 리가 없다.

서버를 생성하는 것은 먼저 집을 여는 것부터 시작한다.

 

socket()

소켓 프로그램에서는 소켓을 통해 통신한다. 서버가 socket()을 통해 소켓을 생성하고 나서 이를 사용할 수 있게 한다.

 

bind()

우편을 받으려면 먼저 주소가 필요한 법이다. 서버가 패킷을 전달받기 위해서는 주소가 필요한데, 이 역할을 IP 주소와 포트(Port) 번호가 대신해준다.

 

bind()는 소켓을 서버의 IP 주소와 포트 번호와 결합시킨다.

왜냐하면 서버는 IP 주소와 포트 번호가 고정되어 있어야 하기 때문이다.

(-> 클라이언트가 도메인을 통해 서버에 접속하기 위해서)

이렇게 해서 소켓에 주소를 부여한다.

 

listen()

아직까지 생성한 패킷이 어떤 역할을 할지 정해지지 않았다.

서버에서 생성한 소켓은 우편함이나 마찬가지이다.

내 집에 우편이 날아오려면, 우선 누군가가 내게 우편을 써야만 한다.

listen()은 연결 요청을 대기하는 것이다.

다른 우편이 날아올 수 있도록, 소켓이 클라이언트를 기다릴 수 있도록 준비를 해야 한다.

 

연결 요청을 받아들일 준비가 되었다.

 

accept()

accept()는 연결 요청을 수락한다.

새로 연결 요청이 들어오게 되면 accept() 함수가 클라이언트를 서버에 연결시킨다.

accept()를 사용하게 되면 연결 요청이 들어올 때까지 계속 대기한다.

accept() 상태에서 연결 요청이 들어오지 않으면, 프로그램은 계속해서 대기한다.

 

위 그림처럼, accept()를 실행한 이후에는 TCP 클라이언트 측이 connect()를 하기 전까지 서버는 아무것도 하지 않는다.

연결 요청이 들어오게 되면 accept()는 클라이언트를 위한 새로운 소켓을 생성하게 된다.

 

맨 처음에 생성하였던 소켓을 통신에 사용하는 것이 아니라, 새로 들어온 손님(클라이언트) 전용으로 소켓을 만든다.

 

새로 만들어진 소켓은 손님(클라이언트)과 데이터를 주고받는 용도로 사용된다.

 

그럼 기존의 소켓은 어떻게 사용될까? 쓸모가 없어졌으므로 소켓을 닫아버려도 된다. 아니면 기존과 마찬가지로, accept()를 통해 새로운 클라이언트의 연결을 기다릴 수 있다.

 

대부분 클라이언트가 하나 들어오고 서버를 닫는 일은 자주 없다.

기존 소켓을 통해 새로운 클라이언트를 기다림과 동시에 연결된 클라이언트와 메시지를 주고받는다.

 

(다만 accept()는 해당 프로세스, 또는 스레드를 아예 block 해버리므로, 새로 생성된 소켓은 또 다른 프로세스, 또 다른 스레드에서 처리되도록 하기도 한다.)

 

(2) 클라이언트 접속

클라이언트는 서버와 거의 동일하다.

다만 클라이언트는 서버와 달리 IP 주소, 포트 번호가 고정될 필요가 전혀 없다.

그래서 bind()가 필요 없다.

 

또한, 서버와 달리 클라이언트는 연결을 요청하는 입장이다. 다른 클라이언트로부터 연결을 기다릴 필요가 없다.

따라서 연결을 기다리기 위해 listen(), accept()를 쓰는 대신, 서버에 연결하기 위해 connect()를 사용한다.

 

socket()으로 클라이언트 소켓을 생성하고 나면 connect()로 서버에 연결 요청을 한다.

이때 서버와 클라이언트는 three-way handshaking이라는 기법을 통해 연결하게 된다.

 

(3) 데이터 입출력

이제 서버와 클라이언트가 연결되고 나면 자유롭게 데이터를 쓰고 읽으면 된다.


write()로 상대측에게 데이터를 전달하고, read() 함수로 상대로부터 들어온 데이터를 읽는다.

 

이를 원하는 대로 가공하여 웹, 실시간 게임, 동영상 플랫폼 등을 구현한다.

 

(4) 소켓 닫기

통신이 끝나고 나면 이제 close()를 통해 소켓을 닫는다.

 

그림처럼 클라이언트가 close()를 호출하게 되면 서버가 read()를 통해 해당 메시지를 읽게 되고, 서버 측도 close()로 닫으면 통신이 종료된다.

 

반대로 서버가 먼저 close()를 호출할 수도 있다.

close()를 호출하는 순서는 서버 먼저 닫아도 되고, 클라이언트 먼저 닫아도 된다.

 

 

2) 함수: 서버

  • int socket(int domain, int type, int protocol)
    • 소켓을 생성하여 소켓 ID(File description; sockfd)를 반환함
    • type : TCP인지 UDP인지 결정
  • int bind(int sockfd, struct sockaddr* myaddr, int addr)
    • 소켓을 IP와 포트 번호에 바인드 한다.
  • int listen(int sockfd, int backlog);
    • 소켓을 listen 용도로 사용하도록 한다.
    • backlog: 동시에 request가 들어올 시 몇 개까지 큐에 넣어 처리해줄 건지를 지정함
  • int accept(int sockfd, struct sockaddr* cliaddr, int* addrlen);
    • 새로운 연결을 허용한다.
    • 수행되면 block이 되고 cliend에서 connect 될 때 block이 해제된다.
    • 이때 서버도 클라이언트의 서버 주소와 포트 번호를 알게 된다.

 

3) 함수: 클라이언트

클라이언트는 서버와 달리 socket()과 connect()를 사용한다. socket()은 서버란에 적어놨으므로 생략한다.

  • int connect(int sockfd, struct sockaddr* servaddr, int addrlen);
    • sockfd: socket()에서 획득한 소켓 ID(File description)
    • 클라이언트도 서버에 접속해야 하므로 서버 주소와 포트 번호가 필요하다. 두 번째 파라미터 struct sockaddr* servaddr은 서버의 IP 주소와 포트(Port) 번호를 가리킨다.

댓글