본문 바로가기
학부공부/OS_운영체제

19. Socket programming basic [Interesting topic to study]

by sonpang 2021. 11. 28.
반응형

안녕하세요. 오늘은 Socket programming에 대한 기초적인 내용을 알아볼까 합니다. 네트워크 관련 교과목을 배우셨거나 TCP/IP 통신 프로그래밍을 해보신 분들은 익숙하실 수도 있겠지만 저처럼 익숙하지 않으신 분들도 있으실거라 생각합니다. 다른 [Interesting topic to study] 포스팅도 그러하듯이 이 포스팅도 운영체제 과목에서 꼭 이해하고 넘어가야하는 내용이 아닐수 있습니다. 저는 운영체제 과목을 아직 수강하지 않아 이 부분을 교수님들께서 얼마나 깊이 또는 비중있게 다루실지 모르지만 폭넓게 기반지식을 다지자는 의미에서 [Interesting topic to study] 포스팅을 이어나갈 것입니다.

 

IPC에 대한 기초적인 내용과 메커니즘 별 구체적인 기초 예제들은

2021.11.24 - [학부공부/OS_운영체제] - 17. IPC(2)

 

17. IPC(2)

안녕하세요. 오늘은 저번 시간에 이어 IPC에 대해 알아보는 마지막 시간입니다. 저번 포스팅에서는 IPC Model과 각 model별 구체적 메커니즘 : 테크닉에 대해 알아보았었습니다. 특히 Signal에 대해 많

ku320121.tistory.com

포스팅에서 알아보았습니다. 

 

 

19.01. Socket

일단 socket의 사전적 의미는 구멍, 연결입니다. 표준 규격에 따라 무언가를 연결할 수 있도록 만들어진 구멍형태의 연결부라고 정리할 수 있을 것 같습니다. 보통 전기 인프라에서 연결부를 소켓이라고도 합니다. 네트워크 분야에서 소켓도 사전적 의미와 크게 다르지 않는데, 프로세스가 네트워크에서 data를 송수신할 수 있도록 제공하는 연결부가 바로 network socket입니다.

 

조금 더 자세히 이야기하자면 네트워크에 연결하기 위해 정해진 규약(protocol)에 따라 만들어집니다. OSI 7 Layer(Open System Interconnection 7 Layer)의 4번째 계층인 TCP(Transport Control Protocol)에서 동작하는 소켓을 일반적으로 사용하는데 이를 TCP/IP 소켓 또는 TCP 소켓이라고 합니다.

 

 

19.02. TCP/IP Socket programming

소켓으로 네트워크 통신을 하기 위해서는 소켓을 만들고, data를 주고 받는 절차에 대해 이해할 필요가 있습니다. 더 높은 수준에서 통신하기 위해서는 OS와 프로그래밍 언어가 제공하는 소켓 API 사용법을 알아야 합니다. 또한 여러가지 고려사항이 있는데 네트워크 단절, 트래픽 증가로 인한 데이터 전송 지연, 시스템 resource 관리 등에 대한 고려도 필요합니다. 네트워크 환경에서는 매우 다양한 issue가 있죠. 그나마 대부분의 프로그래밍 언어와 개발 플랫폼에서 소켓 API를 지원하고 많은 reference가 존재하는 것을 다행으로 생각할 수 있을 것 같습니다.

 

 19.02.1. Client Socket & Server Socket

일반적으로 프로세스가 소켓을 통해 connection을 하기 위해서는 어느 하나의 프로세스가 필요로 하는 프로세스에 연결을 요청해야 합니다. IP address와 port number로 프로세스를 식별하고 data 송수신을 위한 connection을 수립한다고 알리는 거죠. 해당 요청은 받아들이는 프로세스가 받아들이거나 준비되지 않았다면 무시합니다. 그래서 요청을 받아들이는 프로세스에서는 어떤 connection 요청을 받아들일 것인지 시스템에 등록을 합니다. 일반적으로 port number로 식별하고 요청이 들어왔을 때 처리할 수 있도록 준비를 해놓습니다.

이때 요청을 보내는 소켓을 client socket, 받아들이는 소켓을 server socket이라고 합니다. 두 소켓은 역할에 따라 구별한 것이라 호출하는 API function(종류와 순서 정도)에만 차이가 있습니다. Data의 송수신은 요청 수락의 결과로 만들어지는 새로운 소켓을 통해 처리됩니다.

 

19.02.2. Socket API

Reference에 이해하기 좋은 문장과 모식도가 있어 가져와봤습니다. 소켓이 처리되는 흐름을 간단한 문장으로 표현한 것인데요.

 

클라이언트 소켓(Client Socket)은 처음 소켓(Socket)을 [1]생성(create)한 다음, 서버 측에 [2]연결(connect)을 요청합니다. 그리고 서버 소켓에서 연결이 받아들여지면 데이터를 [3]송수신(send/recv)하고, 모든 처리가 완료되면 소켓(Socket)을 [4]닫습니다(close).

 

서버 소켓(Server Socket)은 처리 과정이 조금 복잡합니다. 일단 클라이언트와 마찬가지로, 첫 번째 단계는 소켓(Socket)을 [1]생성(create)하는 것입니다. 그리고 서버 소켓이 해야 할 두 번째 작업은, 서버가 사용할 IP 주소와 포트 번호를 생성한 소켓에 [2]결합(bind)시키는 것입니다. 그런 다음 클라이언트로부터 연결 요청이 수신되는지 [3]주시(listen)하고, 요청이 수신되면 요청을 [4]받아들여(accept) 데이터 통신을 위한 소켓을 생성합니다. 일단 새로운 소켓을 통해 연결이 수립(ESTABLISHED)되면, 클라이언트와 마찬가지로 데이터를 [5]송수신(send/recv)할 수 있습니다. 마지막으로 데이터 송수신이 완료되면, 소켓(Socket)을 [6]닫습니다(close).

 

아래의 모식도로 클라이언트 소켓과 서버 소켓의 실행흐름을 파악할 수 있을 것 같습니다.

 

19.03. Clinet socket programming

socket()

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

위의 모식도에서도 살펴봤듯이 소켓을 생성하는 것을 가장 먼저 하게 됩니다. 이때 소켓에 option을 줄 수 있습니다.(family, raw등이 있고 앞서 말씀드린 TCP socket을 위해서는 stream type으로 지정합니다. UDP socket을 위해서는 datagram type을 지정하죠.)

 

참고로 처음에 소켓이 만들어지는 시점에는 연결 대상에 대한 정보가 전혀 없습니다. 마냥 연결구만 만들어 놓은 것이라 생각하면 될 것 같습니다.

 

connect()

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);

socket()을 통해 생성한 소켓을 연결대상을 지정하고 연결 요청을 하기위해 connect() API를 호출합니다. 

 

IP address + Port number > Target

 

Target을 IP 주소와 포트 number로 식별하여 연결 요청을 보냅니다. block 방식으로 동작하기 때문에 성공, 거절, 시간 초과등의 결과가 결정되기 전까지는 connect()의 실행이 종료되지 않습니다. 그래서 connect() API의 실행결과를 즉시 return 받을 수는 없습니다.

 

send() & recv()

#include <sys/socket.h>
int send(int sockfd, const void *buf, size_t len, int flags);
int recv(int sockfd, const void *buf, size_t len, int flags);

connect() API 호출이 성공하면 send()와 recv() API를 통해 data를 주고 받을 수 있습니다. 여기서도 마찬가지로 block방식으로 동작합니다. 특히 recv()는 data가 수신되거나 error가 발생하기 전까지 실행을 종료하지 않기 때문에 data 수신 작업은 send()에 비해 조금 더 복잡합니다.(send()의 경우에는 data를 보내는 것이 자기자신이기 때문에 data의 size, 보내는 시간 등에 대한 정보를 알기 때문에 recv()에 비해 정보를 많이 가지고 있다고 생각할 수 있습니다.)

 

그래서 data 수신을 위한 recv()는 소켓의 생성과 연결이 완료되면 새로운 스레드를 하나 만들어 recv()를 실행하고 data가 수신되길 기다립니다.

 

close()

#include <unistd.h>
int close(int sockfd);

data 송수신 과정을 완료하면, 즉 data 송수신이 필요없게 된다면 socket을 닫기 위해 close() API를 호출합니다. 닫힌 소켓은 더 이상 유효하지 않게되므로 해당 소켓을 사용하여 data를 송수신할 수 없게되고 다시 data 송수신을 하고 싶다면 면 소켓 생성과정부터 다시 시작해야 합니다.

 

 

19.04. Server socket programming

socket()

클라이언트 소켓과 마찬가지로 소켓을 생성합니다.

 

bind()

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

bind() API는 IP 주소와 포트 number를 소켓에 결합합니다.(argument로 무엇이 필요한지 감이 오시죠?) 여러 개의 프로세스는 시스템이 관리하는 port(0~65535)중 하나의 port number를 사용합니다.(port에 대해서는 별도로 포스팅이 있으니 참고해주시면 좋을 것 같습니다.) 만약 소켓이 사용하는 포트 number가 다른 소켓의 포트 number와 중복된다면 어떤 소켓이 처리해야 할지 결정해야 합니다. OS에서 소켓들이 중복된 포트 number를 사용하지 않도록 연결정보를 관리하는 이유기도 하죠.(kernel이 그 역할을 할 겁니다.) bind() API는 해당 소켓이 지정된 포트 number를 사용할 것이라 OS에 요청하는 API인 겁니다. 요청한 포트 number 다른 소켓이 사용한다면 error를 return합니다.

 

일반적으로 서버 소켓은 고정된 port number를 사용합니다.

 

listen()

#include <sys/socket.h>
int listen(int sockfd, int backlog);

소켓 binding을 하면 연결 요청을 받아들일 준비를 마친 것입니다. 이제는 클라이언트에 의한 연결 요청이 올 때까지 기다리면 되겠죠. listen() API가 그 역할을 수행합니다. listen() API가 대기 상태에서 빠져나오는 경우는 클라이언트 요청이 수신되거나 소켓이 close()하는 등 error가 발생하는 경우입니다. 중요한 점은 listen() return value로만은 클라이언트 연결 요청이 성공했는지 실패했는지 알 수 없습니다. 시스템 내부의 queue에 클라이언트 연결 요청에 대한 정보를 볼 수 있는데 listen()상태의 클라이언트와의 연결 상태는 not established state 대기 상태로 표현됩니다.

 

accept()

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

대기 중인 연결 요청을 queue에서 꺼내와 연결을 완료하는 것은 accept() API입니다. 실질적인 connection요청을 받아들이는 역할을 수행하는 것이죠. 다만 최종적으로 data 통신을 위해 연결되는 소켓은 bind()와 listen() API에서 사용한 서버 소켓이 아니라는 것을 상기할 필요가 있습니다. 최종적으로 클라이언트 소켓과 연결이 만들어지는 소켓이 서버 소켓이 아니라 accept API 내부에서 새로 established된 소켓이라고 정리 할 수 있습니다.

 

서버 소켓의 핵심 역할은 클라이언트 연결 요청을 수신하는 것입니다. 하나의 연결 요청을 처리하기 위한 서버 소켓의 역할을 설명드리자면 소켓에 포트 number를 binding하고 요청 대기 queue를 생성하여 클라리언트의 요청을 대기합니다. 그리고 accept() API에서 data 송수신을 위한 새로운 소켓을 만들고 서버 소켓의 대기 queue에 쌓인 첫 번째 연결 요청을 mapping시킵니다. 서버 소켓은 또 다른 연결 요청을 처리하기 위해 listen하거나 close하는 것만 남게됩니다. 

 

sned() & recv()와 close()

클라이언트 소켓 처리 과정과 동일합니다. 다만 차이가 있다면 서버 소켓에서의 close()는 socket() API를 통해 생성된 서버 소켓과 accept() API 호출을 통해 생성된 소켓도 처리한다는 것입니다.

 

 

19.04. Another issue

클라이언트 소켓 요청은 한번에 동시 처리가 가능한가?

요청은 system의 queue에 쌓인 순서대로 처리됩니다.

 

IP와 port number는요?

cmd에서 ipconfig를 통해 IP address를 알 수 있고 port number는 소켓이 만들어지고나서 특정 port를 bind하거나 시스템에 의해 할당됩니다. 일반적으로 debug message를 출력하여 확인할 수 있습니다.

 

block방식은요?

connect()의 경우에 return을 받아오는 시점은 서버의 응답에 의해 연결 요청 결과가 결정되는 시점입니다. 서버에 의해 연결이 established되었든 refuesed되었든 그 결정이 이루어진 것입니다. TCP연결 과정은 실질적으로 OS의 network module에서 처리되기 때문에 listen() function으로 OS에 port를 binding하고 대기하고 있다면 클라이언트 연결 요청에 바로 응답을 보내게 되어 있습니다. 만약 서버 쪽에서 listen() function으로 대기하고 있다면 클라이언트의 connect() function은 바로 return한다는 의미입니다.(서버에서 accept()하는 시점과 connect() function이 return하는 시점은 다릅니다.)

 

Non-blocking방식이 있다면 소켓을 non-blocking으로 제공하고 API를 호출하면 function이 block되지 않기 때문에 소켓의 상태를 지속적으로 pooling해서 확인해야 할 겁니다. Data를 읽어들일 때 recv()를 수행하는 별도의 스레드를 두는 이유이기도 합니다.(스레드 개념을 도입하지 않는다면 Non blocking I/O로 recv queue를 계속 check해야 할 수도 있습니다.) 

 

주소정보는요?

struct sockaddr_int
{
    sa_family_t      sin_family;   // Address Family
    uint16_t         sin_port;     // 16bit TCP/UDP PORT
    struct in_addr   sin_addr;     // 32bit IP address
    char             sin_zero[8];  
}

sin_family는 다양한 주소체계가 있어 구별하기 위한 parameter입니다. OS과목에서 IPC topic으로 소켓을 다루신다면 로컬 통신을 위한 유닉스 프로토콜의 주소체계인 AF_LOCAL을 사용하고 IPv4 인터넷 프로토콜에 적용하는 주소체계를 사용하는 경우에는 AF_INET를 사용합니다. sin_port와 sin_addr는 네트워크 byte 순서로 저장하고요. sin_zero는 특별한 의미를 가지진 않습니다. 단순히 sockaddr_int의 size를 sockaddr과 맞추기 위해 추가한 것입니다.

struct sockaddr
{
    sa_family_t    sin_family    // Address Family
    char           sa_data[14];  
}

 

 

오늘은 Socket programming의 기초에 대한 부분을 정리하는 시간을 가졌습니다. Socket은 port를 우리가 공유하기 때문에 general하게 사용할 수 있는 IPC라고 말씀드렸습니다. 최소한의 공통분모가 존재를 이유로 port가 존재하는 것이죠. 만약 이러한 공통분모가 존재하지 않는다면 훨씬 상위의 메커니즘이 필요할 것입니다. Pipe와 signal은 machine boundary를 벗어나지 못했습니다. 다만 소켓은 general하긴 하지만 notification이라는 부가적인 메커니즘이 따라가 주어야 합니다. 예를 들면 I/O 디바이스에서 discovery는 굉장히 중요한 문제인데 임의의 디바이스가 paring을 하는 것을 어떻게 할지 고민해 보시면 좋을 것 같습니다.(한가지 방법은 두 디바이스 모두 서버로 동작하도록 서버소켓을 열고 connection을 기다릴 수 있습니다. 그러면 어떠한 디바이스든 클라이언트로 상대 디바이스에게 connection을 요청할 수 있습니다. 아니면 명시적으로 서버를 지정해 서버소켓을 열고 다른 디바이스는 클라이언트로 connection을 시작할 수도 있죠. paring과 connection은 의미가 다릅니다. paring은 두 장치가 서로의 존재를 알고 인증에 필요한 key를 공유하여 암호화된 연결이 가능하다는 것이고 connection은 RFCOM(Radio Frequency COMmunication) 채널을 공유하여 데이터 송수신이 가능하다는 것을 의미합니다. 즉, connection되었다는 것은 연결된 블루투스 소켓을 가져 data 송수신이 가능하다는 의미입니다.)

소켓 프로그래밍과 관련하여 조금 더 높은 수준의 이론을 쌓고 싶은 분들께는 RFC 문서를 읽어보시는 것을 추천드립니다.

반응형

'학부공부 > OS_운영체제' 카테고리의 다른 글

21. CPU Steal Time [Interesting topic to study]  (0) 2021.11.30
20. CPU Scheduling(1)  (0) 2021.11.30
18. Message Queue [Interesting topic to study]  (2) 2021.11.25
17. IPC(2)  (6) 2021.11.24
16. Port [Interesting topic to study]  (0) 2021.11.24

댓글