목차
i - 소개
서버가 클라이언트로 부터 오는 요청을 받아 처리하기 위해서는 먼저 client socket을 생성할 수 있도록 서버의 listen socket을 설정해야 한다. socket을 LISTEN 상태로 만들어서 client로 부터 오는 요청을 받을 수 있도록하는 방법을 알아보자.
1 - listen socket
1.1 - 소켓이란?
서버와 클라이언트가 서로 접속하고 통신을 하기 위해서 소켓(socket)을 이용한다. 소켓은 네트워크 통신 기능을 제공하는 인터페이스다(Network Application Interface). 네트워크 통신을 하기 위해서는 하드웨어에서 신호를 받아 운영체제에서 처리해주는 등의 저수준 과정이 필요하다. 소켓은 그런 저수준 영역에서 네트워크 통신을 위해 어떻게 처리하는지 알지 않아도 어플리케이션 구현단에서 편리하게 네트워크 통신 기능을 사용할 수 있게 해준다. 마치 TV 뒤의 소켓에 플러그를 꽂으면 TV 내부에서 영상을 어떻게 처리하는지 몰라도 영상을 볼 수 있는 것 처럼 말이다. 소켓을 이용할 때 소켓 함수가 받는 인자들에 대한 데이터를 어플리케이션 단에서 처리해서 넣어주면 편리하게 네트워크 기능을 이용할 수 있다.
[이런 사진이라도 있으면 좋지 않을까 싶어서 올리는 사진] https://www.samsung.com/latin_en/support/tv-audio-video/smart-tv-audio-input-and-output-connections-optic/
1.2 - LISTEN 상태
서버와 클라이언트가 통신을 위해 연결하기 위해서는 서버에서 클라이언트의 요청을 기다리고 있어야 한다. 서버에 있는 소켓이 LISTEN 상태일때 클라이언트의 접속 요청 신호를 받을 수 있다. 이 소켓을 listen socket 이라고 부른다. listen socket이 클라이언트의 접속 요청을 받은 이후에 클라이언트와 접속을 하면 그 클라이언트와 통신할 수 있는 소켓이 생성된다. 클라이언트와 통신할 수 있는 이 소켓을 client socket 이라고 부른다. 이 글에서는 서버가 클라이언트의 접속 요청 신호를 받을 수 있는 상태로 만들기 위해 listen socket을 설정하는 과정까지 알아본다.
1.3 - LISTEN 상태로 설정하는 방법
listen socket을 설정하기 위해서 3가지 과정이 필요하다. 먼저, 소켓 함수 socket()을 이용해서 소켓을 생성한다. 이후 소켓 함수 bind()를 이용해서 소켓에 대해 프로토콜, 포트와 IP주소를 설정한다. 마지막으로 소켓 함수 listen()을 이용하면 소켓이 접속 요청 신호를 받을 수 있는 LISTEN 상태가 된다. socket(), bind(), listen()을 순서대로 이용해서 listen socket을 설정하는 과정을 C++(C도 가능)로 구현한 코드와 함께 다음 단락에서 구체적으로 알아보자.
2 listen socket 설정하기(with C/C++)
2.1 - socket()
socket() 함수로 소켓을 생성하면서 사용할 프로토콜과 IP 주소 체계를 설정한다. 소켓을 사용해서 통신을 하려면 양단이 같은 프로토콜을 사용해야한다. 예를들면 대표적으로 TCP, UDP와 같은 프로토콜이 있다. socket() 함수는 사용자가 설정한 프로토콜을 사용해서 통신할 수 있도록 리소스를 할당해 소켓을 만들고, 접근할 수 있도록 특정값을 리턴한다. 이 값을 소켓 디스크립터(Socket Descriptor)라고 부르며, 각종 소켓 함수를 호출할 때 인수로 전달하여 사용한다.
listen_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listen_sock_ == -1) {
// error handling...
}
C++
복사
위는 socket() 함수를 이용해서 소켓을 만드는 코드다. 3개의 인자가 들어가는 것을 확인할 수 있는데 각 인자들에 대해서 알아보자. 첫번째 인자는 IP 주소 체계를 설정한다. AF_INET는 IPv4 주소 체계를 사용한다는 의미이다. 두번째 인자는 소켓 타입을 지정해서 사용할 프로토콜을 특성을 나타낸다. SOCK_STREAM은 신뢰성 있는 데이터 전송기능을 가진 연결형 프로토콜을 사용한다고 소켓타입을 지정한다. 세번째 인자는 프로토콜을 명시적으로 지정한다. IPPROTO_TCP는 TCP 프로토콜을 사용한다고 명시적으로 지정하는 것이다. TCP와 UDP 프로토콜은 앞에서 주소체계, 소켓타입 2가지만으로도 지정이 가능해서 보통은 0을 넣는다. 여기서도 0을 써도 되지만 굳이 TCP 설정 매크로를 넣은 이유는 그냥 한번 써보고 싶었다. ㅎㅎㅎ
소켓 디스크립터는 Linux나 macOS 환경에서 파일 디스크립터(FD)와 같이 관리된다. main문에서 바로 socket() 함수를 실행해서 반환값을 보면 3인 것을 확인할 수 있다. 이는 사용할 수 있는 FD가 STDIN(0), STDOUT(1), STDERR(2) 바로 다음 값인 3이기 때문이다.
2.2 - bind()
bind() 함수로 소켓의 지역(local) IP주소와 지역 포트번호를 설정한다. 지역 IP주소와 지역 포트번호는 각각 자신의 IP주소와 포트번호를 말한다. IP주소와 포트번호를 설정하기 위해서는 소켓 주소 구조체를 사용해야한다. 소켓 주소 구조체는 네트워크 프로그램에 필요한 주소를 담는 구조체로 다양한 소켓 함수에 인수로 사용된다. 프로토콜 체계에 따라 주소의 저장 방식이 다르므로 다양한 소켓 주소 구조체가 있다. TCP/IP 프로토콜을 사용하는 경우 sockaddr_in나 sockaddr_in6 구조체를 사용한다. sockaddr_in는 32비트 주소를 저장할 수 있고 sockaddr_in6는 64비트 주소를 저장할 수 있다. IPv4 주소를 사용할 때는 sockaddr_in 구조체를 사용한다. 다음 코드에서 소켓 주소 구조체의 주소값을 설정하고 bind() 함수를 사용하는 예를 보자.
struct sockaddr_in s_addr_in; // 소켓 주소 구조체
memset(&s_addr_in_, 0x00, sizeof(s_addr_in)); // 구조체 0으로 초기화
s_addr_in.sin_family = AF_INET; // IPv4 주소체계 사용 설정
s_addr_in.sin_addr.s_addr = htonl(INADDR_ANY); // 서버의 지역 IP 주소 설정
s_addr_in.sin_port = htons(4242); // 서버의 지역 포트 번호 설정
retval = bind(listen_sock_, reinterpret_cast<struct sockaddr*>(&s_addr_in), sizeof(s_addr_in));
if (retval == -1) {
// error handling...
}
C++
복사
위에서 IP주소와 포트번호를 설정할 때 htonl()과 htons()를 사용하는 것을 볼 수 있다. 호스트 바이트로 저장된 값을 네트워크 바이트로 저장된 값인데 응용프로그램이 소켓함수에 데이터를 넘겨주기 전에 호출해서 사용한다. 이유는 각 환경에 따라서 바이트가 정렬되는 규칙(빅 엔디언, 리틀 엔디언)이 다를 수 있기 때문에 네트워크에서 사용하는 규칙으로 맞춰주는 것이다. host to network 라서 함수 이름이 hton*() 인것을 알 수 있다. 뒤에 l과 s가 붙은 것은 각각 32비트와 16비트를 다룰 수 있다는 의미(long, short)다.
bind() 함수는 첫번째 인자로 소켓 디스크립터를 받고 두번째 인자로 소켓 주소 구조체의 주소를 받는다. 이 때 주소의 타입은 항상 (struct sockaddr *)로 넘겨줘야 한다. 세번째 인자는 소켓 주소 구조체의 크기를 넣는다. 함수가 -1을 반환하지 않고 0을 반환한다면 사용할 소켓의 지역 IP주소와 지역 포트번호가 성공적으로 설정된 것이다.
2.3 - listen()
listen() 함수는 소켓을 LISTEN 상태로 설정한다. 클라이언트의 접속을 감지할 수 있는 상태로 설정하는 것이다. 앞에서 socket()과 bind()는 주소 정보 등을 설정하느라 복잡한 과정을 거쳐야했지만 listen()은 모든 것이 다 설정된 소켓을 가져다 써서 LISTEN 상태로만 바꿔주면 되기 때문에 상대적으로 간단하게 사용할 수 있다.
retval = listen(listen_sock_, SOMAXCONN);
if (retval == -1) {
// error handling...
}
C++
복사
listen()이 받는 인자로 소켓 디스크립터 외에 하나가 더 필요하다. 그 두번째 인자는 서버가 당장 처리하지 않더라도 접속 가능한 클라이언트의 개수이다. 클라이언트가 접속 요청을 하면 listen queue 라는 곳에 클라이언트의 정보가 저장된다. listen socket이 처리하기 전에 저장할 수 있는 queue의 길이를 의미하는 것이다. SOMAXCONN 인자로 넣으면 그 queue의 길이의 최대값을 사용하는 것이다. 반환값으로 listen()이 0을 반환하면 성공적으로 소켓을 LISTEN 상태로 설정한 것이다.
o - 마무리
서버의 socket이 클라이언트의 요청을 받을 수 있도록 LISTEN 상태로 설정하는 방법과 구현 코드를 살펴봤다. listen socket은 클라이언트의 접속 요청을 감지하는 것까지 가능하다. 실제로 클라이언트와 연결하여 통신하기 위해서는 listen socket이 접속 요청을 감지한 이후에 accept() 소켓 함수로 클라이언트와 연결하여 client socket을 만들고 그 client socket을 이용해서 클라이언트와 통신이 가능하다.
※ 참고자료
•
TCP/IP 소켓 프로그래밍 [책]