네트워크 프로그래밍-소켓 통신 구현 1
안녕하세요. 개발자 WH입니다.
이번 글은 소켓 구현을 위한 서버와 클라이언트의 구조를 알아보고
각각 의미하는 바를 짚고 넘어가고자 합니다.
이 개념을 한번에 이해하는 것은 조금은 어렵기 때문에
이 전 글에서 네트워크에 대한 기본적인 지식을 가지고 읽는다면 더욱 도움이 될 것 같네요.
2022.01.28 - [임베디드 리눅스] - 네트워크 프로그래밍-TCP/IP 패킷 전송
2022.01.28 - [임베디드 리눅스] - 네트워크 프로그래밍 - OSI 7계층
결국 우리가 하는 일은 TCP 통신을 활용해서 정보를 주고 받는 일을 할 겁니다.
즉 현재 구현하고자 하는 하는 부분은 TCP 통신의 3 way handshake를 구현하려고 하는 거죠..
좋은 그림이 있어서 가져왔습니다. 즉 이 그림을 구현할 겁니다.
위 그림을 보면 뭔지는 모르겠지만, 무엇인가를 주고 받죠?
TCP/IP 패킷 전송 글에서, 다루었다시피, 데이터를 전달하는 출입구가 있어야 합니다.
그리고 우리는 그 출입구를 소켓이라 부르고 운영체제에서 지원해 줍니다.
해당 출입구를 통해, 서버와 클라이언트는 연결될 수 있고
그 소켓을 통해 데이터( 파일을 포함 )를 주고 받게 되는 겁니다.
서버가 조금은 까다롭지만
천천히 따라오시길 바랍니다. 코드 전에 각 함수의 역할에 대해 설명하고
다음 글에서 코드에 대해 다뤄보도록 하겠습니다.
서버
Socket 생성( 데이터 출입구 ) -> Socket에 유일한 이름 연결 ( bind() ) -> 응답 대기 ( listen() ) -> 클라언트를 받기( accept() ) -> 클라이언트 요청에 따른 서비스 수행 ( read() & write() ) -> 연결 종료 ( close() )
클라이언트
Socker 생성 -> 서버에 연결시도 ( connect() ) -> 서비스 받아드림 ( read() & write ) -> 연결 종료 ( close() )
우리는 소켓을 만들어야 되기 때문에 소켓에 대해 조금은 자세히 알아볼 필요가 있습니다.
소켓의 구성요소
1. 인터넷 프로토콜 ( TCP, UDP, raw IP etc)
2. 로컬 IP 주소
3. 로컬 포트
4. 원격 IP 주소
5. 원격 포트
소켓의 유형
1. 연결지향형
- 각 소켓끼리 연결된 상태에서 데이터를 주고 받는 것을 말하는데요, 전에 다뤘던 TCP/IP 가 연결지향형 소켓에 해당한답니다. 열견된 상태에서 통신을 하기 때문에 연결 대상외 다른 대상과는 통신이 불가능합니다. 다른 대상과 연결을 하려면 새로운 소켓을 통해 데이터를 주고 받아야 겠죠? 연결지향형 소켓의 특징은 데이터를 보내두고 제대로 받았는 지 중간중간 확인을 하기 때문에 안정적으로 데이터를 보낼 수 있습니다. 그렇지만 그만큼 속도가 느리죠. 그렇지만 안정적이라는 장점 때문에 데이터가 소실되어서는 안되는 경우 TCP를 황용하게 됩니다.
2. 비연결지향형
- 연결되지 않은 상태에서 내가 원하는 주소로 데이터를 보낼 수 있는 방법을 말하는데요, UDP 통신이 여기 해당한답니다. 데이터를 보내고 난 후에 확인작업을 해주지 않기 때문에 데이터가 다 수신을 했는지 확인이 불가능 합니다. 그렇지만 그만큼 속도가 빠르죠. 인터넷 방송 스트리밍이 아마 여기에 해당되지 않을까 싶습니다.
1. 소켓 생성
통신을 위해 첫 번째 해야할 일은 소켓을 생성하는 일입니다. 소켓 생성시에 유형을 지정할 수 있으며, 대표적으로 TCP or UDP 타입은 선택하여 지정할 수 있습니다.
2. 소켓 바인딩 : bind()
bind에 들어가는 파라미터는 IP와 PORT번호가 있습니다. 시스템 상에서 많은 수의 프로세스가 동작하기 때문에 서버에 접근할 수 있는 가상화된 포트를 지정해야하며, 운영체재는 소켓들이 중복된 포트 번호를 사용하지 않도록, 내부적으로 포트 번호와 소켓 연결 정보를 관리합니다. bind() 호출과정에서 중복되는 포트 사용이 있으면 운영체제가 포트 할당을 거부하고, API는 에러를 리턴합니다.
참고 API란 : 어플리케이션 소프트웨어를 빌드하고 통합하기 위한 정의 및 프로토콜 세트인 Application Programming Interface를 뜻합니다. 즉 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 긴으을 제어할 수 있게 만든 인터페이스 입니다.
3. 연결 요청 대기 : listen()
서버 소켓에 포트 번호를 bind하고 나면, 서버 소켓을 통해 클라이언트의 요청을 받을 준비가 된 것입니다. 클라이언트의 연결 요청이 수신될 때까지 기다려야 해요,. 물론 대기 상태를 빠져나오는 경우도 있는데, 요청이 수신되는 경우와, 에러가 발생하는 경우 빠져나오게 됩니다. 클라이언트 연결 요청에 대한 정보는 시스템 내부적으로 관리되는 큐에 쌓이게 됨으로, 대기 중인 연결 요청을 큐로부터 꺼내와서, 연결을 수립하기 위해서는 accept()를 호출하면 된답니다.
4. 연결 수립 : accept()
accept()를 호출하면 소켓 간 연결이 수립되는 데, 특이한 부분은 accept()가 호출되어 소켓 간 연결을 수립할 때 새로운 소켓을 만들어 연결한다는 점입니다. 그럼 한 가지 의문이 들겠죠? 아까 만든 서버 소켓은 뭐하는데? 서버 소켓은 클라이언트 연결을 대기하고 연결 수립 요청을 하고 소켓을 닫는 역할을 한답니다.
5. 데이터 송수신 : send(), recv()
연결된 소켓을 통해 데이터를 보낼 때는 send() 수신에는 recv() API를 사용하는데, 두 API 모두 블럭 방식으로 동작해요. 때문에 두 호출이 모두 결과가 결정되기 전까지 API가 리턴되지 않습니다. 특히 recv() 같은 경우는 한번 실행되면 언제 어떤 데이터가 전송되어 올 것인지 알 수 없기 때문에, 데이터 수신을 위해서는 별도의 스레드를 실행하여 데이터를 기다린답니다.
참고 스레드 : 프로세스 내에서 실제로 작업을 수행하는 주체를 의미하며, 모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다. 운영체제 관련해서는 별도로 정리하겠습니다.
6. 연결 종료
데이터 송수신이 필요없게 되면, 소켓을 닫기 위해 close() API를 호출하는데요, 해당 소켓은 닫힌 이후 재사용이 불가능합니다. 위에서 소켓을 2 번 만들었어요. 연결을 대기, 수립 요청, 닫는 역할을 하는 서버소켓과 연결 수립 시 accept() 호출시 연결을 수립할 때 만들어진 소켓. 따라서 close()는 두번 사용해줘야 한답니다.
여기까지 네트워크프로그래밍-서버에 관한 기본적인 내용들을 다뤄봤습니다.
더욱 자세한 이야기는 다음 글에서 다루도록 하겠습니다.
오늘도 즐거운 하루되세요. 이상 WH였습니다.