소켓 프로그래밍 - 소켓으로 통신하는 방법

Byeonggwan Kang·2021년 9월 13일
0
post-thumbnail

저번 글에 이어서 이번엔 UDP와 TCP 소켓의 사용법, 즉 통신하는 방법에 대해 알아봅시다.

TCP SOCKET

우선 TCP 소켓이 어떻게 상대와 연락하는지부터 알아봅시다. TCP는 연결을 유지해야 하고 데이터 전송을 보장하기 위해 소켓마다 내부에 데이터를 보관한다고 배웠습니다. 따라서 호스트는 각기 다른 TCP 연결마다 별개의 TCP 소켓을 유지해야만 합니다.

소켓이 통신하는 과정에서 몇 가지 중요한 과정들이 있습니다. 제 나름대로 순서에 따라 정리해보겠습니다.

서버에서는

  • 바인딩(Binding) : 해당 소켓이 어떤 주소와 포트를 사용할지 알려줍니다.
// sock : 바인딩에 사용할 소켓
// address : 바인딩에 사용할 주소
// address_len : address의 길이
// 바인딩에 성공시 0, 실패시 -1을 리턴
int bind(SOCKET sock, const sockaddr* address, int address_len);
  • 리스닝(Listening) : 해당 소켓이 TCP 연결을 기다리는 리스닝 모드가 됩니다.
// sock : 리스닝 모드가 될 소켓
// backlog : 연결할 소켓들이 기다리는 대기열의 크기. 기본값은 SOMAXCONN
// 리스닝 모드가 됐다면 0, 실패했다면 -1을 리턴
int listen(SOCKET sock, int backlog);
  • 어셉트(accept) : 리스닝 소켓에 오는 TCP 연결을 받습니다. 리스닝에 성공하면 새 소켓을 만들어 그 호스트와의 통신은 리스닝 소켓이 아닌 이 소켓에서 이루어집니다.
// sock : 리스닝 소켓
// addr : 연결된 호스트의 주소가 여기에 저장됩니다.
// addrlen : addr의 길이가 여기에 저장됩니다.
// 연결에 성공하면 그 호스트와 연결된 새로운 소켓을 리턴
SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);

accept는 호출한 쓰레드에서 TCP 연결을 원하는 클라이언트가 올 때까지 블로킹(blocking), 즉 기다립니다.

클라이언트에서는

  • 커넥트(connect) : 어셉트를 기다리는 호스트와 TCP 연결을 합니다.
// sock : 상대 호스트와 TCP 연결을 유지할 소켓
// addr : 연결하려는 상대 호스트의 주소
// addrlen : addr의 길이
// 성공시 0, 실패시 -1 리턴
int connect(SOCKET sock, const sockaddr* addr, int addrlen);

양 측에서 데이터를 주고 받을 때에는

  • 주고 받기: send와 recv를 사용해서 호스트끼리 데이터를 주고 받습니다. 전송버퍼에 공간이 없거나 받을 데이터가 없다면 호출한 스레드는 블로킹돼서 기다리게 됩니다.

블로킹은 프로그램에 있어서 여러모로 골칫거리가 됩니다. 이를 블로킹하지 않게 하는 방법(논블로킹)은 다음 글에서 소개합니다.

// sock : 사용할 소켓
// buf : 보내거나 받을 데이터
// len : 보내거나 받을 데이터의 길이
// flags : 잘 모르겠지만 게임 코드에선 보통 0으로 둔다고 한다
// 성공적으로 받거나 보냈으면 데이터의 길이를, 또는 실패하면 -1을 리턴
int send(SOCKET sock, const char* buf, int len, int flags);
int recv(SOCKET sock, char* buf, int len, int flags);

생각보다 간단하죠?

TCP의 연결 지향적인 부분은 분명 도움이 될 때가 많습니다. 하지만 코드를 짤 때에는 양 측 호스트가 연결이 끊어진 상태인지 연결되어 있는 상태인지 파악하면서 짜기 쉽지 않았습니다. 여러 클라이언트의 접속을 받는 서버 구현에서는 특히 더 어려웠죠.

가령 예를 들어서 리스닝 소켓의 accept나 recv를 호출하는 도중에 상대방의 소켓이 사라졌다고 칩시다. 이 사실을 모르고 대처하지 않은 최악의 경우에는 해당 쓰레드가 영원히 기다릴 수 있습니다.

따라서 후에 소개할 논블로킹과 멀티스레딩을 이용한 소켓 프로그래밍에서는 미리 큰 그림을 구상하는 게 좋다고 생각합니다.



UDP SOCKET

UDP에서는 TCP에서 연결을 위한 부분을 지우면 된다고 이해하면 되겠습니다.

UDP 소켓은 바인딩 한 후 바로 데이터를 주고 받을 수 있습니다. 만약 바인딩을 하지 않았더라도 동적 포트에서 남은 포트를 자동으로 바인딩 해 줍니다.

  • 주고 받기 : TCP의 send, recv와 사용하는 방법은 같습니다. 디폴트로 사용할 시 블로킹 된다는 점도 잊지 마세요.
// sock : 사용할 소켓
// buf : 보내거나 받을 데이터
// len : 보내거나 받을 데이터의 길이
// flags : 잘 모르겠지만 게임 코드에선 보통 0으로 둔다고 한다
// to/from : 보내거나 받을 상대 호스트 주소
// ~len : 그 길이
// 성공적으로 받거나 보냈으면 데이터의 길이를, 실패하면 -1을 리턴
int sendto(SOCKET sock, const char* buf, int len, int flags, const sockaddr* to, int tolen);
int recvfrom(SOCKET sock, char* buf, int len, int flags, const sockaddr* from, int fromlen);



마치며

소켓 프로그래밍은 프로세스 단에서 통신하는 방법 중에서 가장 기본이라고 생각합니다. 그런 만큼 이미 여러 예제를 짜본 상태입니다. 하지만 많은 경험과 노력 없이는 좋은 코드를 짜기 어려울 것 같습니다. 조만간 예제 뿐만이 아닌 가시적인 성과를 낼 수 있는 프로젝트를 진행해봐야겠습니다.

0개의 댓글