소켓은 프로세스 간의 네트워크 통신을 가능하게 하는 인터페이스로, 두 컴퓨터 간의 데이터 송수신을 가능하게 한다. 소켓은 네트워크 프로그램에서 클라이언트와 서버 간의 연결을 설정하고 유지하는 데 사용된다. 소켓 프로그래밍은 주로 TCP/IP 프로토콜을 사용하며, TCP와 UDP 소켓으로 나눌 수 있다.
TCP 소켓은 신뢰성 있는 연결형 소켓이다. TCP(Transmission Control Protocol)는 데이터가 손실되지 않도록 보장하며, 데이터의 순서도 유지된다. TCP 소켓을 사용하면 서버와 클라이언트 간의 연결이 설정되고, 이 연결을 통해 데이터를 주고받을 수 있다. TCP 소켓은 웹 브라우징, 이메일 송수신, 파일 전송 등 신뢰성이 중요한 응용 프로그램에서 많이 사용된다.
UDP 소켓은 비연결형 소켓이다. UDP(User Datagram Protocol)는 데이터의 순서나 손실을 보장하지 않지만, 빠른 전송 속도를 제공한다. UDP 소켓은 연결 설정 과정이 없기 때문에 TCP 소켓보다 속도가 빠르며, 실시간 스트리밍, 온라인 게임, VoIP(Voice over IP) 등에서 주로 사용된다. UDP 소켓은 데이터 손실을 감수하고서라도 빠른 전송이 필요한 경우에 적합하다.
소켓 프로그래밍을 이해하기 위해서는 소켓의 생성, 바인딩, 청취, 연결, 데이터 송수신, 종료 등의 과정을 이해해야 한다.
소켓 생성: 먼저, 클라이언트와 서버는 각각 소켓을 생성해야 한다. 소켓을 생성하기 위해서는
socket()
함수를 호출한다. 이 함수는 소켓에 대한 파일 디스크립터를 반환하며, 이를 통해 소켓에 접근할 수 있다. 예를 들어, 다음과 같이 소켓을 생성할 수 있다.
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
서버 소켓 바인딩: 서버는 생성한 소켓을 특정 IP 주소와 포트 번호에 바인딩해야 한다. 이를 통해 클라이언트가 서버에 접속할 수 있는 주소를 설정하게 된다. 바인딩은
bind()
함수를 사용하여 수행된다. 예를 들어, 다음과 같이 서버 소켓을 바인딩할 수 있다.
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
bind(server_socket, (struct sockaddr*)&server_addr,sizeof(server_addr));
서버 소켓 청취: 서버는 클라이언트의 연결 요청을 대기하기 위해 소켓을 청취 상태로 만들어야 한다. 이를 위해
listen()
함수를 호출한다. 이 함수는 서버 소켓이 수신 대기 상태로 전환되며, 클라이언트의 연결 요청을 받을 준비를 한다.
listen(server_socket, 5);
클라이언트 소켓 연결 요청: 클라이언트는 서버에 연결 요청을 보내기 위해 서버의 IP 주소와 포트 번호를 지정해야 한다. 그런 다음
connect()
함수를 호출하여 서버에 연결 요청을 보낸다. 예를 들어, 다음과 같이 클라이언트 소켓을 서버에 연결할 수 있다.
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_port = htons(8080);
connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
서버 소켓 연결 수락: 서버는 클라이언트의 연결 요청을 수락하기 위해
accept()
함수를 호출한다. 이 함수는 클라이언트와의 연결이 설정된 새로운 소켓을 반환한다. 이 소켓을 통해 클라이언트와 데이터를 주고받을 수 있다.
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int new_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_len);
데이터 송수신: 연결이 성공적으로 설정되면, 클라이언트와 서버는 데이터를 주고받을 수 있다. 데이터를 송수신하기 위해
send()
와recv()
함수를 사용할 수 있다. 예를 들어, 클라이언트가 서버에 데이터를 보내고, 서버가 이를 수신하는 과정은 다음과 같다.
send(client_socket, message, strlen(message), 0); // 서버에서 데이터 수신
char buffer[1024];
int bytes_received = recv(new_socket, buffer, sizeof(buffer), 0);
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);
연결 종료: 데이터 송수신이 완료되면, 클라이언트와 서버는 각각 소켓을 종료해야 한다. 이를 위해
close()
함수를 호출한다. 소켓을 적절히 종료하지 않으면 자원이 낭비될 수 있다.
close(client_socket);
close(new_socket);
close(server_socket);
Socket과 File Descriptor의 관계
소켓을 생성할 때 소켓 함수를 호출하면, 파일 디스크립터가 반환된다. 예를 들어, 소켓을 생성할 때 socket() 함수를 호출하면 소켓에 대한 파일 디스크립터가 반환된다. 이 파일 디스크립터는 프로세스가 소켓을 통해 네트워크 통신을 할 수 있도록 한다. 이후, 소켓에 데이터를 송수신할 때도 이 파일 디스크립터를 사용하여 read(), write(), send(), recv() 등의 함수를 호출하게 된다.
참고로, 서버가 연결을 받을때 사용하는 소켓과 실제 데이터를 주고 받는 소켓은 서로 다르다. 연결 요청을 받은 서버는 accept() 함수를 통해서 새로운 소켓을 만들고 별도의 fd를 할당한다. 해당 소켓은 클라이언트와의 연결을 위한 것이다. 만약 연결이 종료되면 해당 소켓은 닫히지만, 서버 연결을 받아주는 소켓은 닫히지 않는다.
서버가 다수의 클라이언트 연결을 효율적으로 관리하기 위해서는 select(), poll(), epoll() 등의 함수가 사용된다. 이러한 함수들은 여러 파일 디스크립터를 동시에 감시하고, 데이터가 수신 가능한 소켓을 식별하여 적절히 처리할 수 있게 한다.
블로킹(Blocking) vs 논블로킹(Non-Blocking) 소켓
블로킹 소켓
블로킹 소켓은 소켓 함수 호출이 완료될 때까지 호출한 프로세스나 스레드가 대기 상태에 놓이는 방식이다. 일반적으로 소켓이 기본적으로 블로킹 모드로 설정된다. 다음은 블로킹 소켓의 주요 특성과 동작 방식에 대한 설명이다.
읽기와 쓰기: 블로킹 모드에서는
read()
,recv()
,write()
,send()
등의 함수 호출 시, 데이터가 실제로 전송되거나 수신될 때까지 호출한 프로세스가 대기한다. 예를 들어,recv()
함수는 데이터가 도착할 때까지 반환되지 않으며, 데이터가 도착하면 이를 읽고 반환한다.연결: 클라이언트 소켓의 경우
connect()
함수 호출 시 서버와의 연결이 완료될 때까지 대기한다. 서버 소켓의 경우accept()
함수 호출 시 클라이언트의 연결 요청이 들어올 때까지 대기한다.장점: 블로킹 소켓은 구현이 상대적으로 단순하며, 코드가 직관적이고 이해하기 쉽다. 동기적으로 동작하기 때문에 프로그램의 흐름을 따라가기가 쉽다.
단점: 블로킹 소켓은 특정 소켓 작업이 완료될 때까지 대기하므로, 네트워크 지연이나 다른 이유로 작업이 지연될 경우 전체 프로그램이 멈추게 될 수 있다. 특히, 다수의 소켓을 동시에 처리해야 하는 경우 비효율적일 수 있다.
논블로킹 소켓
논블로킹 소켓은 소켓 함수 호출이 즉시 반환되며, 함수 호출 시 데이터가 준비되지 않은 경우에도 프로세스나 스레드가 대기하지 않는 방식이다. 논블로킹 모드는 소켓의 파일 디스크립터를 설정하여 사용할 수 있다. 다음은 논블로킹 소켓의 주요 특성과 동작 방식에 대한 설명이다.
설정: 소켓을 논블로킹 모드로 설정하기 위해서는 파일 디스크립터의 속성을 변경해야 한다. 예를 들어,
fcntl()
함수를 사용하여 소켓을 논블로킹 모드로 설정할 수 있다.
int flags = fcntl(socket_fd, F_GETFL, 0);
fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK);
읽기와 쓰기: 논블로킹 모드에서는
read()
,recv()
,write()
,send()
등의 함수 호출 시, 데이터가 준비되지 않은 경우 에러 코드(EWOULDBLOCK 또는 EAGAIN)를 반환하며 즉시 반환된다. 따라서, 프로그램은 다른 작업을 수행하거나 나중에 다시 시도할 수 있다.연결: 클라이언트 소켓의 경우
connect()
함수 호출 시 연결이 즉시 이루어지지 않으면 즉시 반환되며, 이후 연결 상태를 확인하여 완료될 때까지 반복적으로 시도할 수 있다. 서버 소켓의 경우accept()
함수 호출 시 연결 요청이 없으면 즉시 반환된다.장점: 논블로킹 소켓은 대기 시간이 없기 때문에 다수의 소켓을 동시에 처리하는 데 적합하다. 이벤트 기반 프로그래밍이나 멀티태스킹 환경에서 유리하며, 전체 시스템의 응답성을 높일 수 있다.
단점: 논블로킹 소켓은 구현이 복잡하며, 데이터의 준비 상태를 계속 확인해야 하기 때문에 프로그램이 복잡해질 수 있다. 또한, 에러 처리가 복잡해지고, 코드가 덜 직관적일 수 있다.
요약
소켓의 개념과 파일 디스크립터: 소켓은 네트워크 통신을 가능하게 하는 인터페이스로, 두 컴퓨터 간의 데이터 송수신을 담당한다. 소켓은 파일 디스크립터를 통해 식별되며, 이 파일 디스크립터를 통해 프로세스는 소켓에 접근하고 데이터를 주고받을 수 있다.
소켓 연결 과정: 소켓 연결은 클라이언트와 서버 간의 여러 단계를 거쳐 설정된다. 서버는 소켓을 생성하고, IP 주소와 포트 번호에 바인딩한 후 클라이언트의 연결 요청을 청취한다. 클라이언트는 서버에 연결 요청을 보내고, 서버는 이를 수락하여 연결을 설정한 후 데이터를 송수신하게 된다.
블로킹 소켓과 논블로킹 소켓: 블로킹 소켓은 소켓 함수 호출이 완료될 때까지 대기 상태에 놓이는 방식으로 구현이 단순하지만, 네트워크 지연 시 전체 프로그램이 멈출 수 있다. 논블로킹 소켓은 소켓 함수 호출이 즉시 반환되는 방식으로 다수의 소켓을 동시에 처리할 수 있어 효율적이지만, 구현이 복잡하고 프로그램의 응답성을 유지해야 하는 추가 작업이 필요하다.