목표
전 글에서는 C++로 UDP Socket 만들어 데이터를 보내보았습니다. 이번 글에서는 UDP Socket으로 데이터를 받는 코드를 작성해 보겠습니다.
UDP로 데이터 받기
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <iostream>
#include <string>
using namespace std;
int main(){
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // Socket 열기
if (s < 0) return 1; // 오류가 있으면 s는 음수
string buf = "Hello World";
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(10000);
sin.sin_addr.s_addr = inet_addr("127.0.0.1");
int numBytes = sendto(s, buf.c_str(), buf.length(),
0, (struct sockaddr *) &sin, sizeof(sin));
cout << "Sent: " << numBytes << endl;
// 전 글에서 코드를 가져와 아래 코드를 추가하였습니다.
char buf2[65536];
memset(&sin, 0, sizeof(sin));
socklen_t sin_size = sizeof(sin); // socklen_t 는 주로 소켓 주소의 길이를 나타내는 데이터 타입으로 사용됩니다.
numBytes = recvfrom(s, buf2, sizeof(buf2), 0,
(struct sockaddr *) &sin, &sin_size);
cout << "Recevied: " << numBytes << endl;
cout << "From " << inet_ntoa(sin.sin_addr) << endl;
close(s);
return 0;
}
/**
* 출력
* Sent: 11
* Received: 11
* From: 127.0.0.1
*/
아래 추가된 코드는 UDP 소켓을 통해 데이터를 수신하고, 수신된 데이터의 크기와 데이터를 보낸 클라이언트의 IP 주소를 출력합니다.
recvfrom()
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd: 데이터를 수신할 소켓의 파일 디스크립터입니다.
- buf: 수신된 데이터를 저장할 버퍼의 포인터입니다.
- len: 'buf'에서 수신할 수 있는 최대 바이트 수입니다.
- flags: 다양한 옵션을 설정하기 위한 플래그입니다. 대부분의 경우, 특별한 옵션이 필요하지 않을 때 '0'으로 설정됩니다.
- src_addr: 데이터를 보낸 상대방의 주소 정보를 저장하는 'struct sockaddr' 타입의 포인터입니다.
- addrlen: 'src_addr'의 초기 크기를 입력으로 제공하며, 'recvfrom()' 호출 후에는 실제 주소의 길이로 업데이트됩니다.
함수의 반환 값은 다음과 같습니다.
- 성공: 수신된 바이트 수
- 오류: '-1' (오류의 원인은 'errno' 변수를 통해 확인할 수 있습니다.)
내 Socket 이 쓰는 Port 번호
UDP Header는 송신자, 수신자에 대해서 각각 주소 요소인 port를 써야 합니다. 앞의 코드 예에서 sendto()를 통해서 상대방 port는 기재했었습니다. 그럼 내 port 번호는 어떻게 된 것일까요?
- 내 socket의 port 번호를 명시하지 않으면 -> 빈 소켓 번호 아무거나 배정합니다.
- 내 socket 의 port 번호를 명시하려면 -> bind() 함수를 사용합니다.
memset(&sin, 0, sizeof(sin));
sin_size = sizeof(sin);
int result = getsockname(s, (struct sockaddr *) &sin, &sin_size);
if(result == 0) {
cout << "My addr: " << inet_ntoa(sin,sin_addr) << endl;
cout << "My port: " << ntohs(sin.sin_port) << endl;
}
/**
* 출력
* My addr: 0.0.0.0
* My port: 44804
*/
위 코드는 사용한 랜덤 포트 번호를 출력하는 코드입니다.
getsockname() 함수는 주어진 소켓 파일 디스크립터 's'에 연결된 로컬 주소 정보를 가져옵니다. 이 정보는 'sin' 구조체에 저장되며, 'sin_size' 변수는 함수 호출 후에 해당 주소의 실제 길이로 업데이트됩니다.
내 Socket 이 쓸 Port 번호 지정
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(10000 + 아무숫자);
if (bind(s, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
cerr << strerror(errno) << endl;
return 0;
}
이 코드는 주어진 소켓 's'에 특정 IP 주소와 특정 포트 번호에 bind 하는 기능을 합니다. 바인드가 성공적으로 이루어지면 해당 주소에서 네트워크 트래픽을 수신할 수 있게 됩니다.
- INADDR_ANY: 시스템의 모든 IP 주소에 bind 한다는 것을 의미합니다.
- htons() 함수는 호스트 바이트 순서의 값에서 네트워크 바이트 순서의 값으로 변환합니다.
TCP와 UDP가 같은 포트 번호를 쓸 수 있을까?
- UDP와 TCP는 각각 2바이트짜리 "port"라는 L4 주소 요소를 갖습니다.
그럼 UDP 포트 80번과 TCP 포트 80번은 같은 것일까요?
- IP header 에는 L4 protocol을 구분하는 field 가 있습니다.
- TCP와 UDP는 각각 다른 protocol 숫자를 씁니다. (참고: TCP(6), UDP(17))
- 따라서 TCP 포트 번호와 UDP 포트 번호가 같더라도 둘은 완전히 다른 의미이므로 공존할 수 있습니다.
- 즉, TCP 80번 포트와 UDP 80번 포트는 완전히 다른 것입니다.
- 이는 TCP 80 포트를 bind 한 서버와 UDP 80 포트를 bind 한 서버는 공존할 수 있으며, 받는 트래픽도 완전히 다르다는 것을 의미합니다.
bind() 함수
'bind()' 함수는 소켓 프로그래밍에서 중요한 함수 중 하나입니다. 이 함수는 소켓에 로컬 주소를 할당하는 데 사용됩니다. 주로 서버 애플리케이션에서 사용되며, 클라이언트가 서버에 연결할 수 있도록 특정 IP 주소와 포트 번호에 서버 소켓을 바인드 하는 데 사용됩니다.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 바인드 할 소켓의 파일 디스크립터입니다. 이 소켓은 'socket()' 함수를 호출하여 생성됩니다.
- addr: 바인드할 로컬 주소를 나타내는 포인터입니다. 대부분의 경우, 'struct sockaddr_in' 또는 'struct sockaddr_in6' 타입의 주소 정보를 포함하는 구조체의 포인터로 캐스팅됩니다.
- addrlen: 'addr'로 지정된 주소 구조체의 크기입니다.
함수의 반환 값은 다음과 같습니다.
- 성공: 0
- 실패: -1 (오류가 발생한 경우, 'errno' 변수에 오류 코드가 설정됩니다.)
bind() 관련 주의점
- bind()는 client 든 server든 관계없이 할 수 있습니다.
- 즉, client socket, server socket의 동작을 변경하는 함수가 아닙니다.
- 다만 통상적으로 서버의 port는 미리 정해져야 client 가 요청을 보낼 수 있으므로 대개 서버 측에서는 bind()를 호출합니다.
- 그러나 서버가 자기의 IP 주소와 Port를 Redis / Zookeeper 등의 특수한 목적의 DB에 등록하게끔 하는 "service directory"를 구현한 경우, bind() 대신 getsockname()을 한 결과를 Redis / Zookeeper에 저장할 수 있습니다.
- 이 경우 client는 Redis / Zookeeper에서 서버의 IP, Port를 알아내서 sendto() 합니다.
bind() 대상
- bind() 함수는 <IP 주소, port> 쌍을 bind 합니다.
- IP는 자기가 가진 네트워크 카드의 IP를 의미하며, 그 IP로 들어오는 트래픽을 받아준다는 의미입니다.
- 예: 아래 그림에서 <IP1, 80>에 bind 하면 목적지가 IP1 주소인, 즉, 서브넷 1로부터 오는 트래픽 중 80번 포트로 들어오는 것을 받겠다는 의미입니다.
- 이때 L3 헤더의 목적지 주소는 IP1, L4헤더의 목적지 포트는 80이 기재되었을 것입니다.
Q) 아래처럼 IP1, IP2를 갖는 서버가 있습니다.
프로그램 P1이 <IP1, 80>을 bind 했습니다.
프로그램 P2가 <IP2, 80> 을 bind 할 수 있을까요?
A) 가능합니다. Bind 정보는 IP와 port를 쌍으로 묶어 판단하기 때문입니다. P1은 목적지 IP가 IP1인 트래픽만, P2는 목적지 IP가 IP2인 트래픽만 읽을 수 있습니다.
UDP Socket에서 기억할 것
- sendto()에서 전송 요청한 데이터 중 일부만 전송되는 경우는 없습니다.
- 다 보내거나(= return 값은 보낸 bytes 수), 에러이거나 (= return 값은 -1)
- recvfrom()에서 일부만 수신하는 경우는 없습니다.
- 다 받거나(=return 값은 받은 bytes 수), 에러이거나 (=return 값은 -1)
- 상대가 제대로 수신했는지 확인할 방법이 없습니다.
- 유실돼도 괜찮은 트래픽에 쓰는 것이 UDP입니다.
'CS' 카테고리의 다른 글
[Network] TCP Socket Programming - 데이터 전송하기 (1) | 2023.10.22 |
---|---|
[Network] Protobuf, JSON (0) | 2023.10.22 |
[Network] Socket Programming 메모리 관리 (1) | 2023.10.21 |
[Network] UDP Socket 만들어보기 - 데이터 보내기 (3) | 2023.10.15 |
[Network] Socket Programming (TCP, UDP) (0) | 2023.10.13 |