본문으로 바로가기

Linux로 시작해보는 간단한 소켓 프로그래밍

category Linux 2017.04.08 00:18

아직 소켓에 대한 개념을 모르시겠다구요?

아래의 버튼을 클릭하여 Socket 포스트를 미리 읽어보시기 바랍니다.


네트워크 프로그래밍의 기초 - Socket


BSD Unix Socket 구성

유닉스에서 소켓 프로그래밍을 할 때는 파일로 하여금 통신을 합니다. 그 이유는 유닉스의 리소스 구성은 파일로 이루어져 있기 때문입니다. 그래서, 유닉스 프로그래밍을 할 때 read, write 함수를 사용할 때도 파일 디스크립터를 사용하여 입력하고 출력합니다.


실제 Unix Programming에서 파일을 새로 열 때, open 함수를 사용합니다. open 함수는 int 형 값을 반환하며 이 반환 값은 File descripter Index입니다. 


이미지 출처: (한국소프트웨어진흥원 저, Network Programming)


유닉스에서 소켓 프로그래밍을 할 경우, 파일을 이용해야 합니다. 그 이유는 유닉스에서 모든 파일, 하드웨어, 파이프, 소켓 등을 전부 파일로 취급하고 있기 때문입니다. 아래의 터미널 사진을 보면, 유닉스는 하드웨어 등을 모두 파일로 표현하고 있다는 것을 알 수 있습니다.


위 사진은 실제 Linux에서 하드웨어를 담당하는 디렉터리의 내용을 보여준 모습입니다. 가장 대표적으로 스토리지 영역을 담고 있는 sda* 부분은 SSD와 HDD에 대한 정보로, 이 모두 파일로 이루어져 있고, 마운트 지점을 통해 파일에 액세스할 수 있도록 되어 있습니다.


소켓 역시, 이렇게 파일로 전송하고 받게 됩니다. 유닉스에서 제공하는 함수를 통해 파일에 액세스 하려면, 해당 파일 디스크립터를 사용해야 하기 때문에, 우리는 잠깐 위쪽의 사진을 통해 파일 디스크립터가 어떤 것인지 알아보겠습니다.


우리가 C Language 등의 프로그래밍 언어를 통해서 파일에 접근을 하려면, 파일의 정보가 필요합니다. 해당 파일이 어떤 파일인지, 무슨 권한을 가지고 있는지 등의 정보가 말이지요. 유닉스에서는 이를 C 언어의 구조체에 할당되어 담겨져 있으며 이 구조체를 가리키는 포인터들로 구성된 테이블이 존재하는데, 바로 이 테이블의 Index값이 파일 디스크립터 입니다.


소켓도 파일처럼 이런 디스크립터를 기지고 있는데, 우리는 이를 소켓번호라고 합니다. 간략하게 말하자면, 소켓을 개설하여 얻은 파일 디스크립터이지요. 유닉스에서는 소켓도 파일로 다루기 때문에, 당연히 소켓에서도 디스크립터가 존재합니다.



소켓 생성 준비

소켓을 만들어보기 전에, 먼저 네트워크 프로그램을 개발하기 위해 어떤 것이 필요한지 얘기해보도록 하겠습니다. 각 호스트에는 호스트를 구별하는 식별자인 IP 주소가 존재합니다. 하지만 한 호스트에는 여러 네트워크 응용 프로그램을 사용할 수 있죠. 그럼 한 호스트에서 여러 네트워크 응용 프로그램을 구동하기 위해서는 각 프로그램의 식별자 또한 필요합니다. 로컬에서는 이를 PID(프로세스 식별자ID)가 존재하지만, 네트워크에서는 이를 PID로 구분하지 않고, 포트 번호로 구분합니다.


포트번호는 TCP/UDP 패킷에 16bit 형태로 같이 저장되어 전송하게 됩니다. 1 ~ 65535번 번호를 가지고 있고, TCP와 UDP가 각각 같은 번호로 동작할 수도 있습니다. 이 얘기는 한 응용 프로그램에서 TCP와 UDP를 동시에 사용할 수 있다는 이야기지요.


자 그럼 이제 소켓을 만들기 위해 필요한 것을 전체적으로 보자면 다음과 같습니다.


- 통신에 사용할 프로토콜 (TCP, UDP)

- 클라이언트 IP 주소

- 클라이언트 포트 번호

- 서버 IP 주소

- 서버 포트 번호


실제 프로그래밍할 떄, 클라이언트 IP 주소와 포트 번호는 별도로 구성할 필요가 없습니다. 어차피 클라이언트 프로그램은 운영체제에서 자동으로 IP 주소와, 포트 번호를 부여하기 떄문입니다. 따라서 우리는 보내고자 하는 목적지인 서버의 IP 주소와 포트 번호, 그리고 사용할 프로토콜만 알면 됩니다.



소켓 생성

이제 소켓을 만들기 위해 필요한 준비물은 모두 갖춰졌습니다. 우리는 이제 소켓을 어떻게 만드는지만 알면 됩니다. 그럼 이제 시작해보겠습니다.


네트워크 프로그램을 개발하려면, 두 프로그램을 개발 해야 합니다. 하나는 서버 프로그램, 또 하나는 클라이언트 프로그램입니다. 둘이 서로 통신을 할 것이기 때문에 두 프로그램 모두 소켓을 사용해야 합니다. 그래서 저는 두 프로그램 모두 socket 함수를 사용하기 위해 다음과 같이 첫 코드를 작성하였습니다.


먼저 포트 번호를 임의 지정하기 위해서 프로그램의 argument 범위를 2개로 늘렸습니다. 프로그램을 실행할 때, 사용자가 원하는 포트번호를 적고, 해당 포트번호로 서버가 열리도록 프로그램을 구성한 것입니다.


소켓을 생성할 떄는 socket() 함수를 사용합니다. socket() 함수는 Ubuntu Linux를 기준으로 netinet/in.h 파일을 include 하시면 됩니다.

오류 처리를 위해 if 조건문을 사용하여 음수가 나오면, 오류 메시지가 표시되도록 하였습니다.


socket 함수 첫 인자 값에는 PF_INET이라는 매크로 상수를 적었습니다. 이는 프로토콜 체계로 인터넷을 사용하겠다는 이야기입니다. 보통 IPv4 주소 체계를 사용할 떄, 이 상수를 사용합니다. IPv6를 체계를 사용한다면 PF_INET6을 사용하시면 됩니다.


SOCK_STREAM에 들어가는 인자는 서비스 타입을 이야기합니다. 쉽게 말하자면, TCP를 사용할 것이냐, UDP를 사용할 것이냐를 물어보는 것이라고 보면 쉽습니다. SOCK_STREAM을 사용하면 TCP, SOCK_DGRAM을 사용하면 UDP입니다. 그 외 SOCK_RAW도 존재하지만, 이는 TCP/UDP 계층을 거치지 않고, 바로 IP 계층을 이용하는 방법으로 실제 자신의 프로그램에서 TCP나 UDP를 사용하지 않고자 하고, 직접 구축하고자 할 떄 사용하는 고급 방법입니다.



인터넷 소켓 주소 생성

우리는 인터넷을 사용합니다. 따라서 네트워크 프로그램을 개발하려면, 인터넷을 사용한 프로그램을 개발해야 합니다. 앞에 소켓에서 우리는 인터넷 프로토콜 체계를 사용하기로 하였으니 주소 체계도 인터넷 소켓 주소 체계로 생성해야 합니다. 그래서 우리는 인터넷 전용 소켓 주소인 sockaddr_in 구조체를 사용하기로 했습니다.


sockaddr_in 구조체는 3개의 멤버 변수가 존재합니다. sin_family는 우리가 위에서 만든 소켓에 대한 주소 체계입니다. 나머지는 포트번호와 IP 주소인데, 여기서 저는 IP 주소를 따로 지정하지 않고, 운영체제에서 주어진 IP 주소를 사용하기 위해 INADDR_ANY라 하는 미리 정의된 매크로 상수를 사용하였습니다.



소켓 사용 절차

이제까지는 소켓을 사용하는 방법을 알아보았습니다. 소켓은 유닉스에서 파일 디스크립터처럼 소켓 디스크립터라는 똑같은 속성을 가진 것으로 동작하고 있다는 사실을 알았습니다.


그럼 이 소켓을 사용하여 어떻게 통신을 할 수 있는지 알아보도록 하겠습니다.



네트워크 프로그램은 먼저 서버 프로그램이 구동되어야 클라이언트 프로그램이 동작할 수 있습니다. 그래서 저도 서버 프로그램 코드를 먼저 구현할 것이며, 우리는 여기서 소켓 함수를 사용해서 소켓 생성까지 마쳤기 때문에, 그 다음인 bind 함수에 대해서 나아가면 됩니다. 


bind() 함수는 소켓번호와 소켓주소를 묶어 놓는 함수입니다. 소켓번호는 응용 프로그램 내에서만 알고 있고, 소켓주소는 네트워크에서만 알고 있는 주소이기 때문에, 이 두 쌍을 묶어서 같은 것이라고 표기해야 할 필요가 있습니다.


소켓번호와 소켓주소를 묶어놓기 위해, 소켓번호를 첫 번쨰 인자값으로 주고, 인터넷 소켓 주소는 구조체이므로 포인터로 넘깁니다. 마지막으로 소켓 주소 구조체의 크기까지 넣어주면 bind가 완성됩니다. 오류 처리는 필수입니다.


이제 모든 준비가 끝났습니다. 서버는 이제 listen 함수를 이용해 대기모드로 들어갑니다. 대기모드는 클라이언트가 신호를 줄 때 까지 기다리는 함수로, 마치 손님이 오기를 기다리는 것과 같다고 생각하시면 됩니다. listen 함수에서 마지막 인자 값은 동시 접속 허용 클라이언트 수입니다.


클라이언트와 연결에 성공하면, 서버는 accept() 함수를 호출하고, 클라이언트와 통신을 하기 위한 소켓을 반환하게 됩니다. 고로 서버 프로그램에는 2개의 소켓이 필요한 것이죠. Listen을 위한 소켓과 클라이언트 연결을 받기 위한 Accept를 위한 소켓.


accept 소켓은 클라이언트와 직접 데이터를 주고 받기 위한 연결 고리로, listen socket은 계속 다음 연결을 받기 위한 용도로 사용됩니다.


포트 번호를 입력하고, 프로그램을 실행하면, 구동 중인 것을 확인할 수 있습니다.


위에서 입력한 50061 포트번호가 사용 중입니다. 


클라이언트도 서버 프로그램처럼 위와 같이 작성하시면 쉽게 네트워크 프로그램을 개발하실 수 있습니다.



마치며...

소켓 프로그래밍에 대한 서적과 자료들은 많이 있지만, 사실 접하기도 어렵고, 쉽지 않은 프로그래밍 중에 하나 입니다. 무엇보다도 소켓 프로그래밍에는 운영체제에서 일부 커널 리소스를 사용하거나 네트워크에서 IP 주소 등의 프로토콜을 이해할 수 있어야 쉽게 할 수 있는 프로그래밍이기도 합니다.


소켓 프로그래밍 포스트는 예전부터 계획했던 것이지만, 어떤 방식으로 접근해야 좋을지를 여러 번 고민한 끝에 이제서야 작성하게 되었습니다.


제가 앞으로 코딩하게 될 파트도 네트워크 프로그래밍 비중이 높아지면서, 점점 이 프로그래밍에 재미를 붙이게 되었습니다. 다음 포스트는 쓰레드 등을 사용해서 네트워크 프로그래밍을 하는 포스트를 작성하도록 하겠습니다.


comments powered by Disqus

티스토리 툴바