본문 바로가기

Network/Network Programming

[C++/Network] UDP 소켓과 TCP 소켓

[이전 글]

[C++/Network] Berkeley Socket (버클리 소켓)

 

소켓을 만들어 바인딩하는 과정까지 진행했습니다. 이번에는 UDP 소켓과 TCP 소켓을 만들어보도록 하겠습니다.

 


 

UDP 소켓

 

UDP 소켓은 연결을 유지하지 않고도 데이터를 보낼 수 있습니다. 소켓은 호스트마다 하나의 소켓만 있으면 데이터를 주고받을 수 있지만, 신뢰성을 보장할 수 없습니다.

 

UDP 소켓은 만든 즉시 데이터를 보낼 수 있습니다. 바인딩 되어있지 않다면, 네트워크 모듈이 자동으로 포트를 찾아 바인딩해줍니다. 데이터를 보낼 때 사용하는 함수는 sendto 입니다.

// sock : 데이터그램을 보낼 소켓입니다. 바인딩되지 않았다면 라이브러리가 자동으로 바인딩해줍니다.
// 이 때 바인딩한 주소와 포트는 발신자 주소가 됩니다.
// buf : 보낼 데이터의 시작 주소를 가리키는 포인터입니다.
// len : 데이터의 길이입니다.
// flags : 데이터 전송을 제어하는 비트 플래그입니다. 웬만해선 0으로 둡니다.
// to : 수신자의 목적지입니다. 소켓을 만들 때 지정한 주소 패밀리와 일치해야합니다.
// tolen : sockaddr의 길이를 지정합니다. IPv4의 경우 size0f(sockaddr_in)을 이용하면 됩니다.
int sendto(SOCKET sock, const char* buf, int len, int flags, const sockaddr* to, int tolen);
// 작동에 성공하면 송신 대기열에 넣은 데이터의 길이를, 그렇지 않다면 -1을 반환합니다.

 

반대로, 데이터를 받으려면 recvfrom 함수를 이용합니다.

 

// sock : 데이터를 받는 소켓입니다.
// buf : 수신한 데이터그램을 복사해 넣을 버퍼입니다.
// len : buf 인자가 담을 수 있는 최대 바이트 길이를 지정합니다.
// flag : 데이터 수신을 제어하는 비트 플래그입니다.
// from : 데이터를 받았을 때 발신자의 주소와 포트를 채워줄 곳을 가리킵니다.
// fromlen : from의 길이를 반환할 포인터입니다.
int recvfrom(SOCKET sock, char* buf, int len, int flags, sockaddr* from, int* fromlen);
// 작동에 성공하면 buf에 복사한 바이트의 길이를 반환하며, 그렇지 않으면 -1을 반환합니다.

 


 

TCP 소켓

 

TCP 소켓은 신뢰성을 보장하고, 연결이 유지되어야 송수신이 가능합니다. 추가로 각 호스트마다 별개의 소켓을 만들어 유지시켜줘야합니다.

 

TCP에서는 클라이언트와 서버를 연결하기 위해서 3-way handshaking 작업을 거쳐야합니다. 이는 클라이언트가 연결 요청 패킷을 보내면, 서버가 응답 패킷 및 연결 요청 패킷을 클라이언트에 보냅니다. 이후 클라이언트는 이 패킷을 받아서 연결이 되었는지 확인하는 과정입니다.

 

출처 : 위키백과

 

서버가 첫 단계(= 클라이언트의 연결 요청 패킷을 받는 것)를 위해, 먼저 소켓을 먼저 만들고, 바인딩을 거쳐서 리스닝하는 작업을 해줍니다.

// sock : 리스닝할 소켓입니다.
// backlog : 들어오는 연결을 대기열에 둘 최대 숫자를 지정합니다.
// 만약 설정한 값보다 커진다면(= 대기열이 가득차면), 그 이후의 연결은 끊습니다.
int listen(SOCKET sock, int backlog);
// 실행 성공시 0을, 실패시 -1을 반환합니다.
// 리스닝 소켓은 새로 들어오는 연결 요청을 받기 위한 소켓을 만들어주는 역할입니다.
// 리스닝 소켓으로는 원격 호스트와 통신할 수 없습니다.

 

리스닝을 한 이후, accept를 호출해 handshaking을 계속 진행합니다.

// sock : 리스닝 모드의 소켓으로, 이 소켓으로 들어오는 요청을 받습니다.
// addr : accept 함수가 연결을 요청하는 주소를 담은 포인터입니다.
// addrlen : addr 내용이 채워질 때 그 길이가 addrlen에 채워집니다.
SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);
// 실행에 성공하면 리스닝 소켓과 같은 포트에 바인딩된 새로운 소켓을 만들어 반환합니다.
// 이 소켓은 호스트의 주소와 포트를 담고있어 호스트와 통신하는 용도로 사용할 수 있습니다.
// 전송되는 패킷에 누락이 발생하면 재전송 용도로도 사용됩니다.
// 호스트와 통신을 하기 위해서는 리스닝 소켓이 아닌, 여기서 반환된 소켓을 이용해야합니다.
// 만약 아직 받아줄 연결이 존재하지 않은 상태라면, 호출 스레드를 멈춰 새 연결이 들어오거나
// 시간이 초과될 때까지 기다립니다.

 

서버측에서 listen 상태에서 accept를 통해 접속을 기다렸습니다. 이와는 다르게, 클라이언트는 소켓을 생성해서 connect만 호출하면 서버와 handshaking을 시도합니다.

// sock : 연결에 사용할 소켓입니다.
// addr : 연결하고자 하는 호스트의 주소를 담는 포인터입니다.
// addrlen : addr 인자의 길이입니다.
int connect(SOCKET sock, const sockaddr* addr, int addrlen);
// 연결에 성공하면 0을, 실패하면 -1을 반환합니다.
// connect를 호출하면 서버에 SYN 패킷을 전송해 handshaking을 시도합니다.
// 서버에서는 해당 포트로 바인딩한 리스닝 소켓이 있다면
// 서버는 accept를 호출해 handshaking을 처리합니다.

 

소켓 생성이 다 완료되었으니, 데이터를 전송해보겠습니다. TCP 소켓은 호스트의 주소 정보를 가지고 있기 때문에 데이터 전송 시마다 주소 정보를 일일이 넘겨주지 않아도 됩니다. TCP 소켓으로 데이터를 전송할 때는 send 함수를 사용합니다.

// sock : 데이터를 보내는 데 사용할 소켓입니다.
// buf : 스트림에 기록할 데이터가 담긴 버퍼입니다. 
// len : 전송할 데이터의 바이트 수입니다.
// flags : 데이터 전송을 제어하는 비트 플래그입니다. 웬만해선 0으로 둡니다.
int send(SOCKET sock, const char* buf, int len, int flags);
// 실행 성공 시 전송한 데이터의 길이를, 에러가 발생하면 -1을 반환합니다.

 

데이터를 받을 때는 recv 함수를 사용합니다.

// sock : 데이터를 받을 때 사용할 소켓입니다.
// buf : 데이터를 복사해 넣을 버퍼입니다.
// len : 버퍼에 넣을 수 있는 데이터 크기의 상한선입니다.
// flags : 데이터 수신을 제어하는 비트 플래그입니다. 
int recv(SOCKET sock, char* buf, int len, int flags);
// 실행에 성공하면 수신한 바이트의 길이를 반환하며 에러시 -1을 반환합니다.

 

다음에는 객체 직렬화에 대해 알아보겠습니다.