CS(Computer Science)/UNIX

UNIX - [소켓 프로그래밍 예제 - 2]

seongmik 2022. 12. 27. 21:52
728x90
반응형
 👉 이 절에서는 소켓을 이용한 간단한 예제 프로그램을 작성해본다. 소켓에는 같은 시스템에 있는 프로세스끼리 데이터를 주고받을 때 사용하는 유닉스 도메인 소켓과 다른 시스템의 프로세스와 통신을 하는 인터넷 소켓이 있다. 이 절에서는 예제를 통해 각각의 사용 방법을 알아보자.

 

유닉스 도메인 소켓 예제

유닉스 도메인 소켓(unix domain socket)은 같은 시스템에서 통신이 일어나므로 TCP/IP 프로토콜을 직접 이용할 필요가 없다. 따라서 유닉스 도메인 소켓에서 사용하는 소켓 주소 구조체의 항목도 IP 주소가 아닌 경로명을 지정하도록 되어 있다. 이는 파이프나 시스템 V IPC에서 특수 파일을 통신에 사용하는 것과 같다고 생각하면 된다.

소켓 주소 구조체의 항목이 다른 것을 제외하면 유닉스 도메인 소켓이든 인터넷 소켓이든 소켓 함수를 사용하는 방식은 동일하다.

유닉스 도메인 소켓 예제는 서버와 클라이언트 프로그램으로 나뉜다. 예제 11-6에 나타낸 서버 프로그램은 “hbsocket”이라는 이름으로 소켓을 생성한 후 클라이언트의 접속을 기다리다가, 클라이언트가 접속하면 보내온 메시지를 읽어 출력하는 간단한 구조로 되어 있다.

// [예제 11-6](1)
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define SOCKET_NAME "hbsocket"

int main(void) {
	char buf[256];
	struct sockaddr_un ser, cli;
	int sd, nsd, len, clen;
	
	// 소켓을 생성하고 소켓 주소 구조체에 값을 지정한다.
	if ((sd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// 소켓을 생성하고 소켓 주소 구조체에 값을 지정한다.
	memset((char *)&ser, 0, sizeof(struct sockaddr_un));
	ser.sun_family = AF_UNIX; // 소켓 패밀리를 AF_UNIX로 지정하고
	strcpy(ser.sun_path, SOCKET_NAME); // 소켓 경로명을 지정한다.
	len = sizeof(ser.sun_family) + strlen(ser.sun_path);

	// bind 함수로 소켓 기술자를 소켓 주소 구조체와 연결해 이름을 등록한다.
	if (bind(sd, (struct sockaddr *)&ser, len)) {
		perror("bind");
		exit(1);
	}
	
	// listen 함수를 호출해 통신할 준비를 마쳤음을 알린다.
	if (listen(sd, 5) < 0) {
		perror("listen");
		exit(1);
	}
	
	printf("Waiting ...\\n");
	// accept 함수를 호출해 클라이언트의 접속 요청을 수락하고 새로운 소켓 기술자를 생성해 nsd에 저장한다.
	if ((nsd = accept(sd, (struct sockaddr *)&cli, &clen)) == -1) {
		perror("accept");
		exit(1);
	}

	// 클라이언트가 보낸 메시지를 recv 함수로 받아서 출력한다.
	if (recv(nsd, buf, sizeof(buf), 0) == -1) {
		perror("recv");
		exit(1);
	}

	printf("Received Message: %s\\n", buf);
	// 출력을 완료했으므로 소켓을 닫는다.
	close(nsd);
	close(sd);
	
	return 0;
}

예제 11-6에 나타낸 클라이언트도 “hbsocket”이라는 이름으로 소켓을 생성한 후 서버측과 연결해 메시지를 전송하는 프로그램이다.

// [예제 11-6](2)
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define SOCKET_NAME "hbsocket"

int main(void) {
	int sd, len;
	char buf[256];
	struct sockaddr_un ser;
	
	// 소켓을 생성하고 소켓 주소 구조체에 값을 지정한다.
	if ((sd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// 소켓을 생성하고 소켓 주소 구조체에 값을 지정한다.
	memset((char *)&ser, '\\0', sizeof(struct sockaddr_un));
	ser.sun_family = AF_UNIX; // 소켓 패밀리를 AF_UNIX로 지정하고
	strcpy(ser.sun_path, SOCKET_NAME); // 소켓 경로명을 지정한다.
	len = sizeof(ser.sun_family) + strlen(ser.sun_path);
	
	// 소켓 주소 구조체에 지정한 서버로 connect 함수를 사용해 연결을 요청한다.
	if (connect(sd, (struct sockaddr *)&ser, len) < 0) {
		perror("bind");
		exit(1);
	}

	strcpy(buf, "Unix Domain Socket Test Message");
	// 서버로 메시지를 전송한다.
	if (send(sd, buf, sizeof(buf), 0) == -1) {
		perror("send");
		exit(1);
	}
	close(sd);
	
	return 0;
}

→ 실행 결과를 보면 클라이언트가 보낸 메시지를 서버에서 받아 출력했음을 알 수 있다. 실행 시에는 서버를 먼저 실행한다.

[결과 - 서버]
# ex11_6s.out
Waiting ...
Received Message: Unix Domain Socket Test Message
[결과 - 클라이언트]
# ex11_6c.out
#

 

 👉 bind 오류 처리 예제 11-6을 한 번 실행한 후 다시 실행하려고 하면 다음과 같은 메시지가 출력된다.
# ex11_6s.out
bind: Address already in use
👉 이는 소켓 파일이 이미 있다는 의미다. 따라서 소켓 파일([예제 11-6]의 경우 hbsocket)을 미리 삭제하거나 서버 프로그램에서 소켓을 생성(예제 11-6 15행)하기 전에 다음을 호출해 파일을 지우도록 프로그래밍해야 한다.
unlink(SOCKET_NAME);

 

인터넷 소켓 예제

인터넷 소켓(internet socket)은 서로 다른 시스템 사이에 통신하므로 TCP/IP 프로토콜을 직접 이용한다. 따라서 소켓 주소 구조체의 항목에 IP 주소와 포트 번호를 지정해야 한다.

[예제 11-7]은 [예제 11-6]처럼 간단한 메시지를 주고받는 프로그램이다. [예제 11-6]과 달리 서버에서 클라이언트로 메시지를 보내도록 했다. 메시지의 내용은 서버에 접속한 클라이언트의 주소를 알려주는 것이다.

예제 11-7에 나타낸 서버 프로그램은 소켓을 생성한 후 IP 주소와 연결하고 클라이언트의 접속을 기다리다가, 클라이언트가 접속하면 클라이언트의 주소를 읽어 메시지를 작성한 후 클라이언트에 전송하는 프로그램이다. 이 예제에서는 서버 시스템의 IP 주소는 192.168.162.133이고, 클라이언트 시스템의 IP 주소는 192.168.162.131이라고 가정한다.

// [예제 11-7](1) 인터넷 소켓(서버)
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define PORTNUM 9000

int main(void) {
	char buf[256];
	struct sockaddr_in sin, cli;
	int sd, ns, clientlen = sizeof(cli);

	// socket 함수의 인자로 AF_INET과 SOCK_STREAM을 지정해 소켓을 생성한다.
	if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// 서버의 IP 주소(192.168.162.133)를 지정하고, 포트 번호는 9000으로 지정해 소켓 주소 구조체를 설정한다.
	memset((char *)&sin, '\0', sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(PORTNUM);
	sin.sin_addr.s_addr = inet_addr("192.168.162.133");

	// bind 함수로 소켓의 이름을 정하고 접속 요청을 받을 준비를 마쳤음을 알린다.
	if (bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
		perror("bind");
		exit(1);
	}

	if (listen(sd, 5)) {
		perror("listen");
		exit(1);
	}

	// accept 함수로 클라이언트의 요청을 수락한다.
	if ((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
		perror("accept");
		exit(1);
	}

	// send 함수로 메시지를 전송한다.
	sprintf(buf, "Your IP address is %s", inet_ntoa(cli.sin_addr));
	if (send(ns, buf, strlen(buf) + 1, 0) == -1) {
		perror("send");
		exit(1);
	}
	// 사용을 마친 소켓을 모두 닫는다.
	close(ns);
	close(sd);
	
	return 0;
}
// [예제 11-7](2) 인터넷 소켓(클라이언트)
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define PORTNUM 9000

int main(void) {
	int sd;
	char buf[256];
	struct sockaddr_in sin;

	// socket 함수의 인자로 AF_INET과 SOCK_STREAM을 지정해 소켓을 생성한다.
	if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// 서버의 IP 주소(192.168.162.133)를 지정하고, 포트 번호는 9000으로 지정해 소켓 주소 구조체를 설정한다.
	memset((char *)&sin, '\0', sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(PORTNUM);
	sin.sin_addr.s_addr = inet_addr("192.168.162.133");

	// connect 함수로 서버에 연결을 요청한다.
	if (connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
		perror("connect");
		exit(1);
	}

	// 연결되면 서버에서 오는 메시지를 받는다.
	if (recv(sd, buf, sizeof(buf), 0) == -1) {
		perror("recv");
		exit(1);
	}
	close(sd);
	printf("From Server : %s\\n", buf);

	return 0;
}

→ [예제 11-7]의 서버 프로그램과 클라이언트 프로그램을 컴파일할 때는 socket 라이브러리와 nsl 라이브러리를 모두 링크해야 한다.

# gcc -o ex11_7s ex11_7-inet-s.c -lsocket -lnsl
# gcc -o ex11_7c ex11_7-inet-c.c -lsocket -lnsl

→ 다음 실행 결과를 보면, 서버에서 보낸 메시지를 클라이언트에서 출력함을 알 수 있다. 서버 프로그램을 먼저 실행한 다음 클라이언트를 실행한다.

[결과 - 서버]
# ex11_7s.out
[결과 - 클라이언트]
# ex11_7c.out
From Server : Your IP address is 192.168.162.131

연습문제

  1. TCP와 UDP에는 어떤 차이점이 있으며, 용도는 어떻게 구별하는가?
    • TCP는 Checksum 기법을 사용해서 데이터의 신뢰성을 보장해준다. 하지만 UDP에 비해 속도가 느리다. 따라서 신뢰성이 중요한 프로그램을 작성할 때 사용한다. UDP는 데이터의 신뢰성을 보장해주지 않고, 보낼 목적지를 매번 지정해줘야 한다. 하지만 Checksum을 사용하지 않기 때문에, TCP에 비해 상대적으로 속도가 빠르다. 따라서 Ping 등의 속도가 중요한 명령에서 주로 사용한다.
  2. TCP/IP로 통신할 때 IP 주소 외에 포트 번호가 필요한 이유가 무엇인가?
    • 포트 번호가 필요한 이유는 어떤 정보를 담고있는지 구별해주기 위해서이다. 0~1023번까지의 포트 번호는 잘 알려진 포트 번호 (well-known port number)로 지정돼있고. 그 외의 포트 번호는 사용자가 자유롭게 지정해서 사용 가능하다.
  3. TCP/IP로 통신할 때 바이트 순서에 주의해야 하는 이유는 무엇인가?
    • 바이트 순서에 주의해야하는 이유는 컴퓨터 아키텍쳐에 따라 정수 바이트형을 저장하는 방법이 다르기 때문이다. 이는 2가지 방법, 리틀 엔디언과 빅 엔디언이 있는데 대표적으로 intel은 리틀 엔디언을 사용한다. 따라서 통신중인 두 컴퓨터의 바이트 순서가 다르다면 잘못된 정보를 읽을 수 있으므로 바이트 순서를 변환해주는 함수를 사용하는 등등 주의해야한다.
  4. 호스트명과 IP 주소를 모두 사용하는 이유는 무엇인가?
    • 호스트명과 IP 주소를 모두 사용하는 이유는 인간의 입장에서 사용하기에는 호스트명이 더 직관적이고, 컴퓨터 입장에서 사용하기에는 IP주소가 더 효율적이기 때문에 두 가지 방법을 모두 사용한다.
  5. accept 함수가 클라이언트의 접속을 허용하면서 socket 함수가 생성한 소켓을 놔두고 새로운 소켓을 리턴하는 이유는 무엇인가?
    • accept 함수가 리턴하는 소켓 디스크립터는 데이터를 주고받는데 사용하고, 기존의 소켓 디스크립터는 추가 소켓 연결을 형성하는데 사용하기 위해서 기존의 소켓 디스크립터는 그대로 두고, 새로운 소켓 디스크립터를 리턴한다.
  6. 잘 알려진 포트 번호를 입력받아 이에 해당하는 서비스명을 출력하는 프로그램을 작성하라.
  7. 잘 알려진 서비스명을 입력받아 포트 번호를 출력하는 프로그램을 작성하라.
  8. /etc/hosts 파일에 특정 호스트명이 있는지 확인하고, 있으면 해당 IP 주소를 출력하는 프로그램을 작성하라.
  9. 사용자가 입력한 메시지를 서버로 전달하고 적절한 응답을 받아 출력하는 프로그램을 작성하라. 사용자가 q를 입력할 때까지 서버는 계속 동작하고 있어야 한다.
  10. 같은 시스템에서 클라이언트가 보낸 파일명을 읽어 해당 파일을 열고 내용을 출력하는 프로그램을 작성하라.

 

** 위 글은 한빛아카데미의 "유닉스 시스템 프로그래밍"의 436page 이후를 참고해서 작성했습니다. **

728x90
반응형

'CS(Computer Science) > UNIX' 카테고리의 다른 글

UNIX - [PIPE - 2]  (0) 2022.12.31
UNIX - [PIPE]  (3) 2022.12.30
UNIX - [MEMORY MAPPING]  (0) 2022.12.30
UNIX - [SIGNAL]  (1) 2022.12.29
UNIX - [소켓 프로그래밍 기초 - 1]  (0) 2022.12.26