티스토리 뷰
네트워크 통신의 경우, 크게 TCP와 UDP로 나누어 진다.
UDP는 빠르지만 불안정한 전송을, TCP는 안정적인 전송을 제공하지만 상대적으로 느린 전송을 제공한다.
현 프로젝트에서는 속도보다는 안정성이 더 중요하므로 TCP을 사용한다.
TCP 통신 과정
흔히 TCP라고 하면 3 Way-Handshake를 가장 많이 떠올린다.
쉽게 말하면, 서버와 클라이언트 간의 연결을 간단히 표현한 것이다.
자세한 내용은 다음 게시글을 참고한다.
2023.05.01 - [컴퓨터 공학 및 알고리즘] - 네트워크
네트워크
네트워크 컴퓨터, 스마트폰, 서버 등 다양한 디바이스들이 서로 연결되어 정보를 공유할 수 있는 구조. 현재의 IP 기반의 네트워크는 미 국방성에서 1969년 진행했던 아르파넷(ARPANET) 프로젝트에
lhs9602.tistory.com
오늘은 그것보다 c언어에서 소켓 통신하는 과정을 다룰 것이다.
c에서 소켓 통신을 아래와 같은 구조를 가진다.
실행과정은 다음과 같다.
- socket()로 서버와 클라이언트에서 소켓을 생성한다. 이 소켓에는 사용할 프로토콜과 IP주소 체계가 설정되어 있다.
- 서버는 bind()로 생성된 소켓에 IP 주소와 포트 번호를 할당한다.
- 그리고 listen()으로 해당 소켓으로 오는 통신 요청을 기다린다.
- 클라이언트는 connect로 서버에 통신을 요청한다. 이때 connect함수에서 bind()의 역할까지 수행한다.
- 그렇게 클라이언트에서 통신 요청이 오면, accept()로 연결을 수락한다.
- 이제 서로 간의 통신이 연결되어 데이터를 자유롭게 보낼 수 있다. read나 write로 데이터를 주고 받을 수 있는데, 이번 프로젝트에서는 send,recv를 사용할 석이다.
- 그렇게 통신하다가 연결을 종료할때, close()로 종료할 수 있다. 이는 free나 fclose와 같이, 할당된 소켓을 해제하기 위해 필요하다.
이제 실제 프로젝트의 코드를 살펴보자
서버 소켓 통신
우선 서버의 경우, 두 파트로 나누어진다.
서버의 소켓을 설정하는 파트와 select() 파트로 말이다.
우선 현 프로젝트에서는 소켓 함수들을 사용자 함수로 만들어 사용했는데. 이는 예외처리를 원할히 하고, 중복된 코드를 줄이기 위함이다.
//main 코드
int server_socket_fd = 0;
server_socket_fd = socket_create(AF_INET, SOCK_STREAM, PROTOCOL);
int opt = 1;
setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in address;
server_address_set(&address, AF_INET, PORT);
socket_bind(server_socket_fd, &address);
socket_listen(server_socket_fd, 50);
//소켓 함수
int socket_create(int domain, int type, int protocol)
{
if (0 == domain)
{
printf("socket_create의 매개변수가 올바르지 않습니다.\n");
return -1;
}
int socket_fd = 0;
socket_fd = socket(domain, type, protocol);
if (socket_fd < 0)
{
printf("소켓 생성에 실패했습니다.\n");
printf("오류 코드: %d\n", errno);
printf("오류 메시지: %s\n", strerror(errno));
}
return socket_fd;
}
void server_address_set(struct sockaddr_in *address, int domain, int port)
{
if (0 == domain)
{
printf("server_address_set의 매개변수가 올바르지 않습니다.\n");
return;
}
memset(address, 0, sizeof(*address));
address->sin_family = domain;
address->sin_addr.s_addr = INADDR_ANY;
address->sin_port = htons(port);
}
void socket_bind(int socket_fd, struct sockaddr_in *address)
{
if (-1 == socket_fd || NULL == address)
{
printf("socket_bind의 매개변수가 올바르지 않습니다.\n");
return;
}
if (-1 == bind(socket_fd, (struct sockaddr *)address, sizeof(*address)))
{
printf("바인딩에 실패했습니다.\n");
printf("오류 코드: %d\n", errno);
printf("오류 메시지: %s\n", strerror(errno));
close(socket_fd);
}
}
int socket_accept(int socket_fd, struct sockaddr *address, socklen_t *addrlen)
{
if (-1 == socket_fd || NULL == addrlen)
{
printf("socket_accept의 매개변수가 올바르지 않습니다.\n");
close(socket_fd);
return -1;
}
int client_socket_fd = 0;
client_socket_fd = accept(socket_fd, address, addrlen);
if (-1 == client_socket_fd)
{
printf("\n");
printf("연결에 실패했습니다.\n");
printf("오류 코드: %d\n", errno);
printf("오류 메시지: %s\n", strerror(errno));
close(socket_fd);
}
return client_socket_fd;
}
조금 길지만 실패시 오류의 원인을 출력하고, 소켓을 정리해주는 코드이다.
server_socket_fd = socket_create(AF_INET, SOCK_STREAM, PROTOCOL);
socket()의 AF_INET, SOCK_STREAM은 각각 ipv4주소 체계와 TCP 프로트콜을 사용한다는 것을 의마한다. 해당 함수의 결과물로 통신에서 사용되는 파일 디스크 립터을 반환한다.
setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt()
은 소켓의 옵션을 지정하는 함수로, SO_REUSEADDR를 옵션으로 소켓이 종료된 후에도 빠르게 동일한 주소와 포트를 재사용할 수 있도록 설정하였다.이는 테스트 시 반복적으로 프로그램 종료와 실행을 반복할 때, 대기시간을 없애기 위함이다.
server_address_set(&address, AF_INET, PORT);
void server_address_set(struct sockaddr_in *address, int domain, int port)
{
if (0 == domain)
{
printf("server_address_set의 매개변수가 올바르지 않습니다.\n");
return;
}
memset(address, 0, sizeof(*address));
address->sin_family = domain;
address->sin_addr.s_addr = INADDR_ANY;
address->sin_port = htons(port);
}
server_address_set()은 sockaddr_in 구조체의 설정을 위한 함수이다.
- address->sin_family : 주소 체계 설정을 설정한다. AF_INET는 IPV4 주소 체계이다.
- address->sin_addr.s_addr : 연결을 허용하는 ip주소를 의미한다. INADDR_ANY은 모든 주소를 허용한다는 의미.
- address->sin_port : 연결이 들어오는 포트를 의미한다. htons(port)처럼 htons를 사용해 포트번호를 변환시킨다.
socket_bind(server_socket_fd, &address);
socket_listen(server_socket_fd, 50);
bind()는 소켓에 sockaddr_in 구조체 저장된 주소 정보로 바인드한다.
listen은 해당 소켓을 연결 대기 상태로 바꾸고, 최대 대기 수를 설정한다. 현재는 50명까지 대기가 가능하다.
참고로 이는 한번에 가능한 대기 인원일 뿐, 소켓과 연결될 수 있는 최대 클라이언트의 수가 아니다.
select의 코드는 다음과 같다.
fd_set readfds;
int client_socket[MAX_CLIENTS];
memset(client_socket, 0, sizeof(client_socket));
int addrlen = sizeof(address);
int max_sd = 0;
struct timeval timeout;
memset(&timeout, 0, sizeof(timeout));
while (TRUE)
{
select_init(server_socket_fd, client_socket, &readfds, &max_sd, &timeout);
select(max_sd + 1, &readfds, NULL, NULL, &timeout);
if(FD_ISSET(server_socket_fd, &readfds){
int new_socket = 0;
new_socket = socket_accept(server_socket_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);
send(new_socket, serialized_data, sizeof(transfer_header_t) + transfer_header.total_size, 0);
...
}
...
}
void select_init(int server_socket_fd, int *client_socket, fd_set *readfds, int *max_sd, struct timeval *timeout)
{
FD_ZERO(readfds);
FD_SET(server_socket_fd, readfds);
*max_sd = server_socket_fd;
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (client_socket[i] > 0)
{
FD_SET(client_socket[i], readfds);
if (client_socket[i] > *max_sd)
{
*max_sd = client_socket[i];
}
}
}
timeout->tv_sec = WAIT_TIME; // WAIT_TIME = 5
timeout->tv_usec = 0;
}
select()는 주어진 파일 디스크립터 집합에서 I/O 이벤트를 감지하는 시스템 콜이다.
간단히 말해 미리 감시할 파일 디스크립터의 범위를 설정하고, 해당 범위안에서 읽기,쓰기 이벤트가 발생시 이후 코드를 작동하게 만드는 것이다.
주로 소켓 통신에서 사용되며, 이렇게하면 다수의 클라이언트로 부터 통신이 어느 시점에 들어와도 대응이 가능하다.
우선 파일 디스크립터를 fd_set에 저장한다. 이후, fd_set의 최대값을 찾아서 +1한 다음 select()에 인자에 포함시킨다.
그 후, timeval 구조체에 할당된 시간만큼 대기, 혹은 이벤트 감지하면 이후의 코드를 실행시킨다.
여기서는 FD_ISSET(server_socket_fd, &readfds) 즉, server_socket_fd에 입력이 발생하면, accept로연결을 허용한다.
이후 send로 초기 동기화 데이터를 클라이언트에 전송한다.
이외에도 다른 코드가 있지만, 그건 다음 게시글에서 설명할 것이다.
클라이언트 소켓 통신
int socket_fd = 0;
socket_fd = socket_create(AF_INET, SOCK_STREAM, PROTOCOL);
// 서버 주소 설정
struct sockaddr_in address;
server_address_set(&address, AF_INET, PORT);
// 서버에 연결 시도
socket_connect(socket_fd, &address);
클라이언트의 경우, 서버에 비해 매우 간단하다.
server_address_set()까지는 서버와 동일하며 이후 connect()를 사용하여 연결한다.
recv(client_soket_fd, &transfer_header, sizeof(transfer_header_t), 0)
이후 서버에서 데이터를 보내면 recv로 받는다.
받은 때, recv의 반환값으로 성공과 실패를 확인 가능하다.
다만, send와 recv둘다 보낼 데이터와 받을 데이터의 크기를 명확히 넣어야하기에 고정 크기의 헤더를 사용한다.
이것에 관련해서는 직렬화 파트에서 자세히 설명하겠다.
이번 게시물에서는 tcp연결 후, 서버가 클라이언트에게 초기 동기화 데이터를 전송하고 서버가 이를 저장한다는 것까지만 이해하면된다.
'프로젝트 > 파일 동기화' 카테고리의 다른 글
프로젝트: 파일 동기화 - 스레드 (0) | 2024.02.17 |
---|---|
프로젝트: 파일 동기화 - 동기화 리스트와 파일 변경 감지 (0) | 2024.02.15 |
프로젝트: 파일 동기화 - 해쉬 테이블 (1) | 2024.02.14 |
프로젝트: 파일 동기화 - 동기화 리스트.txt 읽기 (1) | 2024.02.10 |
프로젝트: 파일 동기화 - 요구사항 (0) | 2024.02.07 |