화재 조기감지 코드 - (2)소켓 통신

2021. 9. 16. 00:11화재 조기감지 프로젝트

이전 포스팅에서는 센서와 라즈베리 간의 통신을 위해 SPI 통신을 사용했습니다. 이번 포스팅에서는 (1)에 이어서 라즈베리와 PC 간의 통신을 위해 사용한 socket 통신 부분 코드를 설명합니다.

 

파일 형태는 SPI 통신 코드와 동일합니다. sock.cc / sock.hh로 헤더파일과 함수 본체 파일을 나눈 후 코드를 다듬었습니다. 

 

1. sock.hh

#ifndef SOCK_H_
#define SOCK_H_

#include <sys/socket.h> // 소켓프로그래밍 함수 선언
#include <netinet/in.h>
#include <arpa/inet.h>

#include <signal.h>
#include <time.h>

#define PORT 3000
#define SERVER "192.168.123.100"
#define BUFSIZE 127
#define RECEIVE_BUF_SIZE 1024

int init_sock(int *client_dsc);
int send_sock(int *client_dsc, adc_t *adc, int adc_ch[], int n);	// 모두 포인터로 통일하기
//int send_data(char *data, int i_data_size);
int close_sock(int *client_dsc);

#endif /*SOCK_H_*/

먼저 소켓 관련 라이브러리와 각종 정의를 모은 sock.hh 헤더파일입니다. 여러 define이 있는데, TCP/IP에서 인터넷 통신은 아래 그림과 같이 이루어지므로 port와 통신 대상인 서버 ip 정의가 반드시 필요합니다.

 

TCP/IP의 인터넷 통신

 

이 그림에서는 12345라는 포트번호를 사용하는 클라이언트 프로세스가 11-AA-22-BB-33-CC라는 MAC 주소(물리적 주소)와 10.1.1.3이라는 IP 주소를 가진 서버의 포트 번호 80번을 사용하는 서버 프로세스와 연결되어 있습니다.  

 

위 그림을 참고하여 port는 PC 프로그램 기준으로 1000, 2000, 3000과 같은 선택지가 있었는데 이 중 3000으로 설정했고, 서버는 PC이므로 PC의 IP를 적었습니다. 이렇게 define을 함으로써 센서와 연결된 라즈베리 클라이언트 프로세스와 port번호 3000을 가지고 IP 주소 192.168.123.100를 가진 서버 프로세스와 연결될 수 있습니다.

 

그림에 socket fd라는 것이 있는데, 이는 선언된 모든 함수의 인자에서 보이는 client_dsc와 동일한 녀석입니다. 소켓 역시 파일로 다뤄지기 때문에 프로세스는 소켓을 사용할 때 file descriptor를 사용합니다. 때문에 소켓을 열고 닫으며 데이터를 보낼 때 file descriptor인 client_dsc를 반환값으로 받습니다.

 

2. sock.cc

#include "spi.hh"
#include "sock.hh"
#include "file_save.hh"

int init_sock(int *client_dsc){
	signal(SIGPIPE,SIG_IGN);	
	struct sockaddr_in server_addr, client_addr;
	int so_reuseaddr, socoptlen;
	int dd;

	if((*client_dsc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1){
		perror("Error : Socket failed");
		return -1;
	}
	bzero(&client_addr, sizeof(struct sockaddr_in));
	bzero(&server_addr, sizeof(struct sockaddr_in));

	client_addr.sin_family = AF_INET;
	client_addr.sin_port = htons(PORT);
	client_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(PORT);
	server_addr.sin_addr.s_addr = inet_addr(SERVER);

	so_reuseaddr = 1;
	socoptlen = sizeof(so_reuseaddr);
	if(setsockopt(*client_dsc, SOL_SOCKET, SO_REUSEADDR, (char*)&so_reuseaddr, socoptlen)<0){
		perror("setsockopt() failed");
		return -1;
	}
	if(bind(*client_dsc, (struct sockaddr *) &client_addr, sizeof(client_addr))<0){
		perror("bind() error");
		return -1;
	}
	if(dd=connect(*client_dsc, (struct sockaddr*)&server_addr, sizeof(server_addr))<0){
		perror("connect() failed");
		return -1;
	}
	printf("connect: %d\n",dd);
    return 0;
}

int send_sock(int *client_dsc, adc_t *adc, int adc_ch[], int n){
	char udp_buf[BUFSIZE];
	int ptr_buf = 1;
    udp_buf[0] = '1';

	for (int i=0; i<n; i++, adc++) {
		udp_buf[ptr_buf++] = 'A'+adc_ch[i];
		udp_buf[ptr_buf++] = (adc->adc_value/1000)%10 + '0';
		printf("%c", udp_buf[ptr_buf++]);
		udp_buf[ptr_buf++] = (adc->adc_value/100 )%10 + '0';
		udp_buf[ptr_buf++] = (adc->adc_value/10  )%10 + '0';
		udp_buf[ptr_buf++] = (adc->adc_value)     %10 + '0';
		udp_buf[ptr_buf++] = '/';
	}

	if (ptr_buf == 0) {
		perror("error: no data exist");
		return -1;
	}

	udp_buf[ptr_buf++] = '#';
	udp_buf[ptr_buf] = '\0';

	if (send(*client_dsc, udp_buf, strlen(udp_buf)+1, 0) == -1){
		perror("error: send failed");
		return -1;
	}
	return 0;
}

int close_sock(int *client_dsc){
	if (close(*client_dsc) == -1){
		perror("error: close failed");
		return -1;
	}
	return 0;
}

두 번째 파일은 헤더파일에 선언한 함수들을 정의한 sock.cc 입니다. 먼저 소켓 함수를 통한 통신 절차는 아래 그림과 같습니다.

1) init_sock(int *client_dsc)

(1) 먼저 socket() 함수로 어떤 프로토콜을 가진 소켓으로 통신할 것인지 결정하여 소켓을 생성합니다.

	if((*client_dsc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1){
		perror("Error : Socket failed");
		return -1;
	}

인자 1: 프로토콜 체계 결정. PF와 AF 둘 중 아무거나 써도 무방.

 (AF/PF에 대해 더 알고싶다면.. -> https://www.bangseongbeom.com/af-inet-vs-pf-inet.html )

인자 2: 전송 방식 결정 

 - TCP: SOCK_STREAM

 - UDP: SOCK_DGRAM

인자 3: 프로토콜 정보. 하나의 프로토콜 체계 안에 데이터의 전송 방식이 동일한 프로토콜이 둘 이상 존재할 때 사용됨. TCP, UDP 사용 시엔 0을 넣어도 무방.

 

(2) bind() 함수로 주소정보를 생성한 소켓에 할당하여 프로세스가 사용할 소켓 fd와 컴퓨터의 IP주소, port를 묶습니다.

	bzero(&client_addr, sizeof(struct sockaddr_in));
	bzero(&server_addr, sizeof(struct sockaddr_in));
	
	// 구조체 주소정보 정의
	client_addr.sin_family = AF_INET;
	client_addr.sin_port = htons(PORT);
	client_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(PORT);
	server_addr.sin_addr.s_addr = inet_addr(SERVER);

	// 소켓 세부사항 설정
	so_reuseaddr = 1;
	socoptlen = sizeof(so_reuseaddr);
	if(setsockopt(*client_dsc, SOL_SOCKET, SO_REUSEADDR, (char*)&so_reuseaddr, socoptlen)<0){
		perror("setsockopt() failed");
		return -1;
	}
	if(bind(*client_dsc, (struct sockaddr *) &client_addr, sizeof(client_addr))<0){
		perror("bind() error");
		return -1;
	}

그런데 bind() 사용 전에 낯선 함수가 보입니다. setsockopt()함수는 소켓의 기본 설정 외의 세부 설정이 필요할 때 사용하는 함수로써 인자는 다음과 같습니다.

 

① setsockopt()

- 인자 1(원형-sock): 옵션 확인을 위해 소켓의 파일 디스크립터 전달

- 인자 2(원형-level): 확인(getsockopt) 또는 변경(setsockopt)할 옵션의 프로토콜 레벨 전달

- 인자 3(원형-optname): 확인 또는 변경할 옵션의 이름 전달

- 인자 4(원형-optval): 확인결과의 저장을 위한 버퍼의 주소 값 전달

- 인자 5(원형-optlen): optval 버퍼의 크기 지정

 

*각 레벨에 따라 굉장히 많은 옵션이 있습니다. 그 중 SOL_SOCKET은 일반적인 레벨로, 이에 따른 옵션명 SO_REUSEADDR은 이미 사용중인 주소나 포트에 대해서도 바인드를 허용한다는 의미입니다.*

(더 자세한 레벨과 옵션을 알고 싶다면.. -> https://jhnyang.tistory.com/262 )

 

② bind()

- 인자 1: 소켓 파일 디스크립터

- 인자 2: *sockaddr 구조체로 캐스팅 된 sockaddr_in 구조체 주소. 이 때 sockaddr_in 구조체 멤버들의 정의가 필요합니다.

// 구조체 내부모습
struct sockaddr_in
{
    sa_family_t    sin_family;  주소체계(Address Family)
    uint16_t       sin_port;    16비트 TCP/UDP PORT번호
    struct in_addr sin_addr;    32비트의 IP주소
    char           sin_zero[8]; (struct sockaddr*)로 캐스팅 시 byte의 column 맞추기. 0으로 채워짐
}
 
struct in_addr{
    in_addr_t    s_addr;  32비트의 IPv4 인터넷 주소가 담김.
}
 
struct sockaddr
{
    sa_family_t sin_family    주소체계(Address Family)
    char        sa_data[14];  주소정보
}

- 인자 3: 인자 2에 넣는 구조체 변수의 길이

 

(3) connect() 함수로 서버(원격 호스트)와 연결합니다.

- 연결된 정보는 두 번째 인자(원형-*remote_host)에 저장되며, 성공 시 0, 오류 시 -1을 반환합니다.

	if(dd=connect(*client_dsc, (struct sockaddr*)&server_addr, sizeof(server_addr))<0){
		perror("connect() failed");
		return -1;
	}
	printf("connect: %d\n",dd);

 

2) send_sock(int *client_dsc, adc_t *adc, int adc_ch[], int n)

이번 코드 수정에서 가장 길이를 축약시킬 수 있었던 부분입니다. init_sock()으로 소켓을 정의하고 연결까지 해 놨으니 이젠 데이터를 보낼 때입니다. 

int send_sock(int *client_dsc, adc_t *adc, int adc_ch[], int n){
	char udp_buf[BUFSIZE];
	int ptr_buf = 1;
	udp_buf[0] = '1';

	for (int i=0; i<n; i++, adc++) {
		udp_buf[ptr_buf++] = 'A'+adc_ch[i];
		udp_buf[ptr_buf++] = (adc->adc_value/1000)%10 + '0';
		printf("%c", udp_buf[ptr_buf++]);
		udp_buf[ptr_buf++] = (adc->adc_value/100 )%10 + '0';
		udp_buf[ptr_buf++] = (adc->adc_value/10  )%10 + '0';
		udp_buf[ptr_buf++] = (adc->adc_value)     %10 + '0';
		udp_buf[ptr_buf++] = '/';
	}

먼저 for을 돌려 선택한 채널에 맞춰서 채널 개수만큼 udp_buf에 데이터를 넣습니다. 인자로 받아온 구조체 포인터를 이용하여 main함수에 있는 read_ads8028() 함수에서 계산되었던 구조체 포인터 adc의 adc_value 멤버 데이터를 가져와 데이터의 앞부분부터 버퍼에 저장합니다. 알파벳에 adc_ch[i]를 더하는 이유는 input 채널이 0부터가 아닌 다른 채널부터 들어올 수 있기 때문입니다. (ex) adc_ch[] = {2, 3} -> 각각 C, D가 담김.)

 

	if (ptr_buf == 0) {
		perror("error: no data exist");
		return -1;
	}

	udp_buf[ptr_buf++] = '#';
	udp_buf[ptr_buf] = '\0';

	if (send(*client_dsc, udp_buf, strlen(udp_buf)+1, 0) == -1){
		perror("error: send failed");
		return -1;
	}
	return 0;
}

 버퍼에 값을 잘 담았으면 send()함수로 버퍼인 udp_buf를 client_dsc에 전송합니다. 정상 처리 시 보낸 byte 수를 반환하며, 실패 시 -1를 반환합니다.

- 원형: send(int fd, void* buffer, size_t n, int flags) 

 

3) close_sock(int *client_dsc)

소켓을 열고 데이터를 보냈다면 마무리로 소켓을 닫아야겠죠? close() 함수로 소켓을 닫아 통신을 종료합니다.

int close_sock(int *client_dsc){
	if (close(*client_dsc) == -1){
		perror("error: close failed");
		return -1;
	}
	return 0;
}

 

3. final_v1.cc (maic code)

#include "spi.hh"
#include "sock.hh"
#include "file_save.hh"

...

int main(int argc, char** argv) {
	int spi_device = 0;
	int *spi_cs_fd;
	int adc_channel[] = {0, 1, 2, 3};
	int adc_size = sizeof(adc_channel)/sizeof(int);
	adc_t *adc;
	int *client_dsc; // client file descriptor
	int *fd;

	if (spi_open_port(spi_cs_fd, spi_device) < 0)
		return -1;

	init_sock(client_dsc); // socket open ~ 설정 ~ connect까지. 
	init_file(fd);

	//for(int i=0;i<50;i++) {
	while(1){
		adc = read_ads8028(spi_cs_fd, adc_channel, adc_size);
		send_sock(client_dsc, adc, adc_channel, adc_size); // 데이터 전송
		make_save_str(fd, adc, adc_size);
		usleep(500000);
	}

	spi_close_port(spi_cs_fd, spi_device);
	close_sock(client_dsc); // socket 닫기
	close_file(fd);

	return 0;
}
...

마지막으로 메인 함수에 지금까지 설명한 함수들을 가져와서 init_sock() -> send_sock() -> close_sock() 순으로 실행 코드를 작성하는 것으로 소켓 부분 코드를 마무리합니다.

 

 

[Reference]

https://reakwon.tistory.com/81

https://aronglife.tistory.com/entry/NetworkTCPIP-%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D2-%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84

https://jhnyang.tistory.com/262