CS/Network

[Chapter 12] 소켓을 이용한 네트워크 프로그래밍 (서버-클라이언트 프로그래밍)

devrabbit22 2025. 2. 17. 23:34

일반적으로 서버-클라이언트 모델은 네트워크 서비스를 제공하는 서비스 제공자와 서비스 이용자 사이의 관계를 표현한다. 서비스를 제공하는 프로그램을 서버 프로세스, 서버와 연결을 시도하여 서비스를 제공받는 프로그램을 클라이언트 프로세스라 한다. 

연결형 서비스

소켓은 네트워크 통신을 위한 소프트웨어 교신점이 된다.

두 개의 독립 프로세스가 네트워크를 통해 통신하려면 논리적인 연관 관계를 맺어주는 소켓이 필요하다. 소켓을 이용한 전형적인 서버-클라이언트 프로그램을 작성하는 방법과 같다.

 

서버와 클라이언트의 동작

  • 서버 프로세스는 다수의 클라이언트에 공개되는 Well-known 포트로 자신의 소켓 주소를 설정한 후에, 클라이언트의 연결 요청을 기다린다.
  • 클라이언트 요구에 따라 연결이 설정되면 서버 프로세스가 제공하는 서비스가 시작된다. 이러한 서비스는 여러 클라이언트에 반복적으로 이루어지며, 동작 과정은 다음과 같다.

서버의 동작

  1. 서비스 교신점(호스트의 IP 주소, 포트 번호)공개
  2. 클라이언트로부터 발생하는 서비스 요구 대기
  3. 클라이언트에 서비스 제공
  4. 해당 클라이언트에 서비스 제공 완료
  5. 단계 2로 이동

클라이언트 프로세스는 서버의 Well-Known 포트를 이용해 원하는 서버와 접속을 시도하고, 연결이 이루어지면 서버가 제공하는 서비스를 이용한다.

클라이언트의 동작

  1. 원하는 서비스를 제공하는 서버 확인
  2. 해당 서버와 연결 시도
  3. 서버에 서비스 요청
  4. 서비스 이용 완료

TCP를 이용한 통신 절차

프로그램 컴파일 방법

  • 클라이언트와 서버 프로그램을 따로 컴파일 한 후 서버, 클라이언트 순으로 실행한다.
  • 컴파일 명령의 -I 옵션은 소켓 함수를 지원하는 라이브러리를 지칭하는 것으로, 실행 파일에 해당 라이브러리 코드를 포함해준다.
  • 컴파일이성공하면 time_client와 time_server라는 두 개의 실행 파일이 현재 디렉토리에 생성된다.
% cc -o time_client time_client.c -lsocket -lnsl
% cc -o time_server time_server.c -lsocket -lnsl

클라이언트 프로그램

  • 서버 프로그램이 실행되는 호스트의 IP 주소 211.223.201.30과 포트 번호 5010은 실행 환경에 따라서 적당한 값으로 변경해야 한다.
  • 포트 번호를 변경하면 뒤에 소개하는 time_server.c의 포트 번호도 함께 변경해야 한다. 그러나 서버 프로그램은 자신의 호스트 주소를 설정하는 부분에 INADDR_ANY를 사용하기 때문에 IP 주소는 변경할 필요가 없다.

클라이언트 프로그램 연결형

/* 컴파일에 필요한 헤더 파일 */
#include<sys/types.h>
#include<sys/socket.h>

#include<netinet/in.h>

#define TIME_SERVER	"211.223.201.30"	/* 서버 프로그램이 실행되는 호스트의 IP 주소 */
#define TIME_PORT	5010			/* 서버 프로그램의 Well-known 포트 번호 */

main()
{
	int sock;			/* 생성된 소켓 번호를 보관하기 위한 파일 디스크립터 */
	struct sockaddr_in server;	/* 서버의 소켓 주소를 생성하기 위한 변수 */
	char buf[256];			/* 서버로부터 수신하는 시간 정보를 보관하는 데이터 공간 */

	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) /* 소켓 생성 */
		exit(1); /* 오류 발생 시 프로그램 종료 */

	/* 서버 프로세스를 지칭하기 위한 소켓 주소를 만드는 과정 */
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(inet_addr(TIME_SERVER)); /* htonl() 생략 여부 */
	server.sin_port = htons(TIME_PORT);

	if (connect(sock, (struct sockaddr *)&server, sizeof(server)) /* 서버 프로세스와 연결 시도 */
		exit(1);

	if (recv(sock, buf, sizeof(buf), 0) == -1) /* 서버 프로세스로부터 시간 정보 수신 */
		exit(1);

	printf("Time information from server is %s", buf); /* 수신 정보를 화면에 출력 */
	close(sock);
}

서버 프로그램

시간 정보를 제공하는 서버 프로그램이다. While문을 이용하여 무한 반복하는 구조이므로 사용자가 강제 종료하기 전까지 계속 수행된다. 따라서 클라이언트 프로그램을 여러 번 반복 실행해도 서버는 매번 시간 정보를 제공할 수 있다.

서버 프로그램 연결형

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#include<time.h>

#define TIME_PORT	5010			/* 서버 프로그램의 Well-known 포트 번호 */

main()
{
	int sock, sock2;
	struct sockaddr_in server, client;
	int len;
	char buf[256];
	time_t today;

	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		exit(1); /* 오류 발생 시 프로그램 종료 */

	/* 자신의 소켓 주소를 생성하는 과정 */
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	server.sin_port = htons(TIME_PORT);

	/* 소켓에 Well-known 주소로 이름 부여 */
	if (bind(sock, (struct sockaddr *)&server, sizeof(server))
		exit(1);

	if (listen(sock, 5)) /* 서버 프로세스의 대기 큐 */
		exit(1);

	while(1) { /* 서버 프로세스는 시스템이 동작하는 동안 항상 클라이언트의 요구에 대기 */
		/* 클라이언트 프로세스로부터 연결 요구를 기다리는 시스템 콜 */
		if ((sock2 = accept(sock, (struct sockaddr *)&client, &len)) == -1)
			exit(1);

		/* 시스템에서 인지하는 시간 정보를 얻어 문자열 형태로 변환하는 과정 */
		time(&today);
		strcpy(buf, ctime(&today));

		send(sock2, buf, strlen(buf) + 1, 0); /* 클라이언트에 시간 정보 전송 */
		close(sock2);
	}
}

비연결형 서비스

  • 연결형 서비스에서 설명한 예제 프로그램을 비연결형으로 변형하면, 비연결형 서비스에서는 connect()와 accept() 함수로 연결을 설정하는 과정이 생략되며, 데이터 송수신을 위한 send(), recv() 함수 대신에 sendto(), recvfrom() 함수를 사용한다.
  • 비연결형 서비스에서는 전송 데이터마다 수신자의 소켓 주소를 함께 전송한다.

UDP를 이용한 통신 절차

클라이언트 프로그램

  • 클라이언트 프로세스는 자신이 생성한 소켓의 주소를 지정하기 위해 client 변수에 IP 주소와 소켓 주소를 지정한다.
  • 중간에 표기된 IP 주소 INADDR_ANY는 자기 자신을 의미하고, 소켓 주소 0은 시스템에서 임의의 포트를 할당하라는 의미이다.
  • 서버와 통신하려면 sendto() 함수에 필요한 서버 주소를 표시하는데, server 변수를 이런 목적으로 사용한다.
  • bind() 함수를 성공적으로 실행하면 클라이언트가 먼저 서버에 문자 데이터를 전송한다.
  • sendto() 함수는 다섯 번째 매개변수 server에 서버의 주소를 표시하여 데이터가 서버 프로세스에 올바로 전송될 수 있도록 한다.
  • 마지막으로 recvfrom() 함수를 이용해 서버가 전송한 시간 정보를 전송 받아 출력한다.
  • recvfrom()의 다섯 번째와 여섯 번째 매개변수를 0으로 지정하면 시간 정보를 송신하는 서버의 주소 값을 다시 받아들이지 않겠다는 의미이다.

클라이언트 프로그램 비연결형

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define TIME_SERVER "211.223.201.30"
#define TIME_PORT 5010

main()
{
	int sock;
	struct sockaddr_in server, client;
	int server_len = sizeof(server);
	char buf[256];
	int buf_len;

	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(inet_addr(TIME_SERVER));
	server.sin_port = htons(TIME_PORT);

	if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
		exit(1);
	
	client.sin_family = AF_INET;
	client.sin_addr.s_addr = htonl(INADDR_ANY);
	client.sin_port = htons(0);

	if (bind(sock, (struct sockaddr *)&client, sizeof(client)) < 0)
		exit(1);

	buf[0] = '?'; buf[1] = '\0';
	buf_len = sendto(sock, buf, strlen(buf) + 1, 0, (struct sockaddr *)&server, server_len);

	if (buf_len < 0)
		exit(1);

	buf_len = recvfrom(sock, buf, 256, 0, (struct sockaddr *)0, (int *)0);

	if (buf_len < 0)
		exit(1);

	printf("Time information from server is %s", buf);
	close(sock);
}

서버 프로그램

서버 프로세스는 변수 server를 이용하여 자신의 소켓 주소를 바인드한다.

서버는 먼저 임의의 클라이언트 프로세스로부터 데이터가 입력되기를 기다리는데, udp_client.c에서 전송한 문자가 입력되면 이를 화면에 출력해준다. 이때 recvfrom() 함수의 다섯 번째 매개변수에 송신자인 클라이언트의 주소가 입력된다.

따라서 sendto() 함수는 이 주소를 이용하여 클라이언트에게 시간 정보를 전송할 수 있다.

서버 프로그램 비연결형

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<time.h>

#define TIME_PORT	5010

main()
{
	int sock;
	struct sockaddr_in server, client;
	int server_len;
	char buf[256];
	time_t today;

	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		exit(1); /* 오류 발생 시 프로그램 종료 */

	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	server.sin_port = htons(TIME_PORT);

	if (bind(sock, (struct sockaddr *)&server, sizeof(server))
		exit(1);

	while(1) {
		buf_len = recvfrom(sock, buf, 256, 0, (struct sockaddr *)&client, &client_len);

		if (buf_len < 0)
			exit(1);

		printf("Server: Got %s\n", buf);

		time(&today);
		strcpy(buf, ctime(&today));

		send(sock, buf, strlen(buf) + 1, 0, (struct sockaddr *)&client, client_len);
	}
}

Reference

쉽게 배우는 데이터 통신과 컴퓨터 네트워크

https://soso-hyeon.tistory.com/110

 

[쉽게 배우는 데이터 통신과 네트워크] CH12. 소켓을 이용한 네트워크 프로그래밍

01 소켓의 주소 체계와 서비스소켓은 운영체제에서 제공하는 통신 프로토콜을 편리하게 사용할 수 있도록 도와주는 역할을 한다. 1 소켓의 주소 체계소켓은 프로토콜 종류에 따라 다양한 방식

soso-hyeon.tistory.com