리눅스 멀티스레드 파일 공유 프로그램 - 2 본문
이번에는 연습으로 c++로 소켓 프로그램을 구현해보려 한다.
c++로는 처음 구현해 보아 gpt의 힘을 빌려
공부를 진행하기로 했다.
결과부터 확인을 해보면
이게 서버의 결과
이건 클라이언트의 결과이다.
서버의 코드부터 분석을 진행해본다.
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>
#define PORT 8080
#define BUFFER_SIZE 1024
using namespace std;
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
int opt = 1;
// 소켓 파일 디스크립터 생성
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
std::cerr << "Socket creation error" << std::endl;
return -1;
}
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
std::cerr << "setsockopt failed" << std::endl;
close(server_fd);
return -1;
}
// 주소 구조체 설정
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
//cout << sizeof(address) << endl;
//cout << sizeof((struct sockaddr *)&address) << endl;
// 소켓을 주소와 바인드
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
std::cerr << "Bind failed" << std::endl;
return -1;
}
// 클라이언트의 연결을 대기
if (listen(server_fd, 3) < 0) {
std::cerr << "Listen failed" << std::endl;
return -1;
}
// 연결 수락
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
std::cerr << "Accept failed" << std::endl;
return -1;
}
// 클라이언트로부터 메시지 읽기
int valread = read(new_socket, buffer, BUFFER_SIZE);
std::cout << "Message received: " << buffer << std::endl;
// 클라이언트에 메시지 보내기
const char *message = "Hello from server";
send(new_socket, message, strlen(message), 0);
std::cout << "Hello message sent" << std::endl;
// 소켓 종료
close(new_socket);
close(server_fd);
return 0;
}
struct sockaddr_in {
sa_family_t sin_family; // 주소 체계(AF_INET)
in_port_t sin_port; // 16-bit 포트 번호
struct in_addr sin_addr; // 32-bit IP 주소
char sin_zero[8]; // 패딩 (사용되지 않음)
};
main함수부터 쭉 보면
sockaddr_in이라는 구조체가 존재한다.
이는 <netinet/in.h> 헤더파일에 존재하는 친구이다.
위와 같은 구조를 가지고 있으며 목적은 IPv4의 정보를 관리하기 위해서이다.
// 소켓 파일 디스크립터 생성
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
std::cerr << "Socket creation error" << std::endl;
return -1;
}
다음 소켓 디스크립터를 생성한다.
AF_INET과 SOCK_STREAM은 변수가 아닌 상수로서 존재한다.
AF_INET은 IPv4라는 것을 알리기 위함이며
SOCK_STREAM은 TCP통신을 위함이다.
세 번째 매개변수는 소켓의 프로토콜을 지정하는데 보통 0이 디폴트이다.
TCP 소켓일 경우 'IPPROTO_TCP' 상수를 전달하기도 하지만 0을 사용하기도 한다.
UDP 소켓일 경우 'IPPROTO_UDP' 상수를 전달한다.
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
std::cerr << "setsockopt failed" << std::endl;
close(server_fd);
return -1;
}
이 조건문은 없어도 실행은 되나
빠르게 주소를 재사용하기 위해서 넣어 놓았다.
서버 소켓이 종료된 후에도 빠르게 같은 주소로 소켓을 다시 열 수 있게 해 준다.
즉 소켓 재사용 및 타임아웃을 설정한데 쓴다.
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
함수의 원형이다.
- sockfd: 옵션을 설정할 소켓의 파일 디스크립터
- level: 옵션의 레벨을 지정합니다. 일반적으로 SOL_SOCKET을 사용하여 소켓 레벨 옵션을 설정
- optname: 설정할 옵션의 이름을 지정합니다. 예를 들어, 소켓의 재사용을 위해 SO_REUSEADDR을 사용
- optval: 설정할 옵션의 값을 담고 있는 포인터
- optlen: 설정할 옵션의 크기를 나타내는 변수
주요 옵션
- SO_REUSEADDR: 소켓이 사용하는 주소를 다시 사용할 수 있도록 허용, 주로 서버 소켓을 종료한 후 즉시 같은 포트 번호로 소켓을 다시 열 때 사용
- SO_REUSEPORT: 같은 포트를 여러 소켓이 공유하여 사용할 수 있도록 허용, 다중 프로세스 또는 스레드에서 서버 소켓을 공유하여 부하 분산을 위해 사용.
- SO_KEEPALIVE: TCP 소켓이 유휴 상태일 때 TCP keep-alive 메시지를 보내도록 설정. 이를 통해 연결이 유효한지 확인 가능.
- SO_SNDBUF: 소켓의 송신 버퍼 크기를 설정 이는 송신 측에서 버퍼링 되는 데이터의 양을 조절
- SO_RCVBUF: 소켓의 수신 버퍼 크기를 설정 이는 수신 측에서 버퍼링되는 데이터의 양을 조절
- SO_RCVTIMEO: 소켓의 수신 타임아웃을 설정 이는 소켓이 데이터를 수신하기 위해 대기하는 시간을 제어
전부 알아둘 필요는 없겠지만
빠른 개발을 위해 SO_REUSEADDR과 SO_REUSEPORT를 이용하였다.
// 주소 구조체 설정
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
sockaddr_in 구조체에 데이터를 담는다.
htons는 네트워 바이트 순서로 변환 즉 빅 엔디안 형식으로 바꾸어준다.
// 소켓을 주소와 바인드
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
std::cerr << "Bind failed" << std::endl;
return -1;
}
아까 struct sockaddr_in address에 데이터를 넣어두었다.
bind()를 통해 소켓에 주소를 할당해 준다.
많은 소켓 함수들은 범용 주소 구조체인 sockaddr을 사용한다.
address를 타입캐스트 해준다.
// 클라이언트의 연결을 대기
if (listen(server_fd, 3) < 0) {
std::cerr << "Listen failed" << std::endl;
return -1;
}
listen을 통해 클라이언트의 연결을 대기한다.
두 번째 매개변수는 대기열의 크기이다.
동시에 대기할 수 있는 연결 요청의 최대 수이다.
// 연결 수락
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
std::cerr << "Accept failed" << std::endl;
return -1;
}
첫 번째 매개변수는 연결을 수락할 서버 소켓의 파일 디스크립터이며
두 번째는 클라이언트의 주소 정보를 저장할 구조체
세 번째는 구조체의 크기
원래 address에 서버의 정보가 담겨있었다면
해당 함수를 통해 클라이언트의 정보로 바뀐다.
new_socket에 새로운 소켓 디스크립터가 만들어진다.
// 클라이언트로부터 메시지 읽기
int valread = read(new_socket, buffer, BUFFER_SIZE);
std::cout << "Message received: " << buffer << std::endl;
// 클라이언트에 메시지 보내기
const char *message = "Hello from server";
send(new_socket, message, strlen(message), 0);
std::cout << "Hello message sent" << std::endl;
// 소켓 종료
close(new_socket);
close(server_fd);
read() 함수는 파일디스크립터를 통해 데이터를 읽는데
소켓 프로그래밍에서는 소켓을 매개변수로 데이터를 읽을 수 있다.
즉 read(), send()에 소켓을 이용해서 데이터를 주고받을 수 있다.
마지막 서버 디스크립터와 만들어진 소켓을 종료한다.
다음 클라이언트 코드에 대해 분석을 진행한다.
'개발 > 개인공부' 카테고리의 다른 글
리눅스 멀티스레드 파일 공유 프로그램 - 6 (0) | 2024.06.10 |
---|---|
리눅스 멀티스레드 파일 공유 프로그램 - 5 (0) | 2024.06.08 |
리눅스 멀티스레드 파일 공유 프로그램 - 4 (0) | 2024.06.08 |
리눅스 멀티스레드 파일 공유 프로그램 - 3 (0) | 2024.06.08 |
리눅스 멀티스레드 파일 공유 프로그램 - 1 (1) | 2024.06.07 |