네트워크 프로그래밍 - 소켓 통신 다중 접속 서버 코드( C++ 구현 )
안녕하세요. WH입니다.
오늘은 지난 번 글에 설명드렸던 아래 그림을 C++로 코딩해볼게요
뭐 달라지기야 하겠습니까만!
매일 C로만 코딩하면 재미없잖아요.
C++ 로 구현해보도록 할게요
혹시 지난번 글이 생각나지 않으신다면
아래 글을 참조해 주세요
2022.02.14 - [임베디드 리눅스] - 네트워크 프로그래밍 - 다중 접속 서버 구현을 위한 기초 (feat. 프로세스, fork(), 좀비 프로세스 )
2022.02.14 - [임베디드 리눅스] - 네트워크 프로그래밍 - 다중 접속 서버 기초 이론 ( feat. 시그널 핸들링 )
2022.02.14 - [임베디드 리눅스] - 네트워크 프로그래밍 - 다중 접속 서버 이론
자 우리는 위의 그림1을 코딩해서 최종적으로 그림 2의 모습으로 구동되는 서버를 만들거에요
깔끔하게 코드먼저 보고 가실게요
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#define BUFSIZE 4096
void read_childproc(int sig);
void error_handling(char* message);
class Socket
{
protected :
int fd;
public:
Socket()
{
fd = socket(AF_INET, SOCK_STREAM, 0);
}
};
class ServerSocket : public Socket
{
private :
int clnt_sock;
public:
bool BindSocket(char * port)
{
struct sockaddr_in addr;
bzero((char*)&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(port));
int flag = bind(fd, (struct sockaddr*) &addr, sizeof(addr));
if (flag==-1)
return false;
else
return true;
}
bool ListenSocket(int num)
{
int flag = listen(fd, num );
if (flag==-1)
return false;
else
return true;
}
bool AcceptSocket()
{
struct sockaddr_in addr;
socklen_t addr_size;
addr_size = sizeof(addr);
clnt_sock = accept(fd, (struct sockaddr *)&addr, &addr_size);
if (clnt_sock == - 1)
return false;
else
return true;
}
bool SendFile(char * filename)
{
char buf[BUFSIZE];
FILE* file = fopen(filename, "rb");
fseek(file, 0, SEEK_END);// 파일 끝으로 이동
size_t fsize = ftell(file);// 파일 크기 계산
fseek(file, 0, SEEK_SET);
int nsize = 0;
while( nsize != fsize ){
int fpsize = fread(buf, 1, BUFSIZE, file);
nsize += fpsize;
send(clnt_sock, buf, fpsize, 0);
}
fclose(file);
}
void CloseListenSocket()
{
close(fd);
}
void CloseAcceptSocket()
{
close(clnt_sock);
}
};
int main ( int argc, char * argv[])
{
int state;
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
ServerSocket serv_sock;
if(!(serv_sock.BindSocket(argv[1])))
error_handling("bind() error.\n");
if(!(serv_sock.ListenSocket(5)))
error_handling("Listen() error.\n");
while(1)
{
if(!(serv_sock.AcceptSocket()))
continue;
else
puts("new client connected");
pid = fork();
if(pid == -1)
{
serv_sock.CloseAcceptSocket();
continue;
}
if(pid == 0)
{
serv_sock.CloseListenSocket();
serv_sock.SendFile("img.jpg");
serv_sock.CloseAcceptSocket();
puts("client disconnected.");
return 0;
}
else
serv_sock.CloseAcceptSocket();
}
serv_sock.CloseListenSocket();
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id : %d \n", pid);
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
쓰고 보니까 기네요.. ㅎㅎ 많이 길어요
짤 땐 몰랐는데 짜면 왜 긴 것일까요..
그렇지만 설명을 드려야겠죠?? 천천히 할게요
class Socket
이 클래스를 만든 이유부터 설명드리겠습니다. 그림 1을 집중해서 봐주세요. 우리는 소켓을 통해서 통신을 하죠. 그럼 소켓이 몇개가 필요할까요? 현재는 서버 소켓만 만들었지만, 클라이언트 소켓까지 만든다고 하면 가장 기본적인 소켓은 3개가 필요할 겁니다. 서버에서 연결 대기까지를 담당하는 소켓, 클라이언트와 데이터를 교환할 소켓, 그리고 마지막으로 클라이언트에서 서버와 통신할 소켓 이렇게 말이죠.
그럼 소켓이 가지고 있어야할 가장 기본적인 요소는 무엇일까요? 파일 디스크립터입니다. 전의 내용들을 살펴 보며, 각 함수의 반환 값들이 무엇이었는 가를 생각해보면 소켓에 부여되는 것은 파일 디스크립터라는 것을 알 수 있겠고, 그 기능은 고유 데이터로 넣어주면 되겠네요. 이게 이해가 가지 않으시면....전 글들을 봐주세요. 시스템 콜 부분과 파일 디스크립터 부분을 참조해주세요.
class Socket
{
protected :
int fd;
public:
Socket()
{
fd = socket(AF_INET, SOCK_STREAM, 0);
}
};
그래서 이렇게 기본 데이터를 넣고 구성했습니다. 그리고 생성자로 통신 규약, 그리고 IP 체계를 설정해 주었습니다. 또한 fd는 protected로 구성했는데, 이유는 서버 socket과 클라이언트 socket에서 fd값에 접근해야 하기 때문입니다. 왜냐? 서버에서는 bind, listen 함수를 구현해야고 client에서는 connect 함수를 구현해야하는데 그때 전달 인자로 fd( file_descriptor 가 들어가기 때문입니다.
class ServerSocket : public Socket
이번에는 서버 소켓입니다. 서버에는 2 개의 소켓이 있습니다. 상속받은 기본 소켓은 bind와 listen 즉 연결 대기 상태까지 만드는데 사용됩니다. 그 다음 accept를 통해 클라이언트와 통신할 소켓이 필요합니다. 해당 소켓을 위해 멤버 변수에 새로운 clnt_sock 을 주어 해당 accept의 파일 디스크립터를 줄겁니다. 또한 자 천천히 생각해봅시다. 기본 소켓은 bind 해준다고 했습니다. 그럼 ip 주소와 port 번호를 주어야 하고 해당 입력은 따로 관리 되다가 bind시에 형변환으로 들어가 주면 됩니다. 그렇다면, sockaddr_in addr 라는 변수를 만들어, 통신 규약, 사용할 주소, 포트를 정의하고 형변환을 통해 상속 받은 소켓에 연결해 줍니다. 아.. 깜박하고 넘어갈번 했네요. char * port 즉 매개 변수로 받는 부분은 포인터형 변수죠? 이 부분은 후에 main에서 char * port[] 변수에서 port 번호만 따오기 위한 변수 설정인데요. 결국 ** 포인트 형에서 해당 인자만 가져오기 위해 * 형을 매개변수로 넘겨줬다라는 것만 기억하시면 될것 같아요. 즉 실행 파일에서 ./server 1234 라고 실행하면 1234만을 따오기 위함이다라고 기억해주시면 되겠습니다.
bool BindSocket(char * port)
{
struct sockaddr_in addr;
bzero((char*)&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(port));
int flag = bind(fd, (struct sockaddr*) &addr, sizeof(addr));
if (flag==-1)
return false;
else
return true;
}
아래 함수들은 사실, 소켓 통신 구현을 통해 모두 다루었던 부분을 멤버 함수화 해놓은 부분이기 때문에 넘어가도록하겠습니다. 넘어가려다 한번 더 짚는다는 마음으로 설명드리고 갑니다. 사실 제가 블로그를 정리하기 시작했던 이유도, 불친절한 설명들 때문에 고생했던 기억이 있었다는 점을 깨닫고 다시 왔습니다. 그 다음 listen함수 입니다. listen은 대기 큐를 만들고 몇 개의 대기 상태를 만들지를 정한다고 했었죠? 그렇기 때문에 해당 매개 변수로 큐의 크기를 설정해줄 수 있도록 구성했습니다. 역시 bool 형을 사용한 이유는 main에서 오류를 체크하기 위함이겠죠? 클래스에서는 값에 직접 접근하는데에 제한이 많기 때문에, 이렇게 bool 형을 많이 사용한답니다. int 형을 사용하기도 해요 0 -1 1 이런 값들을 받고 조건 분기를 할때 말이죠 여튼~ 넘어가볼까요?
bool ListenSocket(int num)
{
int flag = listen(fd, num );
if (flag==-1)
return false;
else
return true;
}
다음으로는 Accept입니다. Accpet는 실제 클라이언트와 연결을 받아들이고, 데이터 통신을 하는 부분이라고 생각하시면 되요. 데이터를 주고 받을 통로가 필요하겠죠? 그리고 누구의 정보를 토대로 데이터를 보낼까요? 클라이언트의 정보를 토대로 보내겠죠? 그럼 우리가 채워 넣을 값이 존재한다기 보다 클라이언트에게 받은 정보를 토대로 채워놔야겠네요. 그럼 해당 정보를 넣을 수 있는 틀만 만들어 줍시다. 클라이언트 ip 주소와 연결중인 포트 정보 등이 들어갈 수있는 틀은 바로 sockaddr_in 이고 accpet 에서 역시 sockaddr * 형으로 형변환 해주어야 겠네요.
bool AcceptSocket()
{
struct sockaddr_in addr;
socklen_t addr_size;
addr_size = sizeof(addr);
clnt_sock = accept(fd, (struct sockaddr *)&addr, &addr_size);
if (clnt_sock == - 1)
return false;
else
return true;
}
이제 파일을 보내는 부분에 대해서 멤버 함수화 해볼까요? 버퍼가 필욯하겠네요. 그리고 파일을 오픈하고, 파일 크기를 재서 해당 크기만큼 지속적으로 나눠서 보내면 되겠어요. 왜 이렇게 구성하냐하면, 파일크기가 엄청 커서 한번에 파일을 못담으면 그게 버그이기 때문이죠. 저번 예제에서는 256 으로 넣었는데 뭐 사실 2048 이나 4096으로 보낸답니다. 핵심 아이디어는 파일 크기를 재고 버퍼를 더해서 그 파일 크기가 될때까지 send 한다는 것이죠.
bool SendFile(char * filename)
{
char buf[BUFSIZE];
FILE* file = fopen(filename, "rb");
fseek(file, 0, SEEK_END);// 파일 끝으로 이동
size_t fsize = ftell(file);// 파일 크기 계산
fseek(file, 0, SEEK_SET);
int nsize = 0;
while( nsize != fsize ){
int fpsize = fread(buf, 1, BUFSIZE, file);
nsize += fpsize;
send(clnt_sock, buf, fpsize, 0);
}
fclose(file);
}
그 다음은 소켓을 닫아주는 기능입니다. 두개의 소켓이 있으니 닫아주는 기능을 두개 만들면 되겠네요. 사실 CloseListenSocket()은 class::Socket 에 있어도 되요. 여기 넣은 이유는 위에서는 정말 기본 기능만 놓기를 바랐기 때문입니다. 사실 이부분까지가 딱 헤더 파일이 되어야 깔끔하겠지만, 우리는 학습용 코드니까 메인 함수 역시 같이 넣어보도록하겠습니다.
void CloseListenSocket()
{
close(fd);
}
void CloseAcceptSocket()
{
close(clnt_sock);
}
메인 함수에서 해주어야 할 일은, 우선 좀비 프로세스를 막아주어야하고, 그 다음 소켓 객체를 생성하고, 바인드, 리슨, 후에 무한 루프로 연결 대기상태에서 accpet를 돌려가며 연결시 파일 전송 후 연결을 끊어주면 되겠습니다. 말로 보니까 되게 쉽네요 그쵸?
좀비 프로세스를 막기 위한 함수를 하나 정의합니다.
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id : %d \n", pid);
}
해당 함수는 뭐 간단하죠? 종료되길 기다리는 데 부모프로세스가 할게 하도록하는 함수에요. 이 함수를 struct sigaction 의 sa_handler에 정의해 줍니다. 이 함수로 신호를 제어하겠다 이런 말이죠. 좀비 프로세스를 막는 코드 부분은 아래와 같습니다.
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
시그널 처리에 대한 글은 이전 글로 들아가서 해당 내용을 참조해 주세요. 다음으로는 소켓을 생성하고 bind, listen 그 과정 사이에, error_handling하는 모습에 대한 것인데요. 함수를 호출하면 해당 기능이 동작과 동시에 bool type return 값을 통해 해당 과정이 실패 했는지를 판단할 수 있겠지요.
ServerSocket serv_sock;
if(!(serv_sock.BindSocket(argv[1])))
error_handling("bind() error.\n");
if(!(serv_sock.ListenSocket(5)))
error_handling("Listen() error.\n");
여기까지 하면 지금 연결 통로를 만들고 연결 대기 상태가 된거죠? 이제, accpet를 만들어서, 연결 신호를 기다리다가 들어오면 연결하고, fork를 통해 자식프로세스로 복제후, 연결을 기다리는 것과 연결을 하는 부분을 나눠주고 자식 프로세스에서는 통신 후 해당 부분을 소켓을 닫아주도록 코딩하면 되겠네요. 바로 아래와 같이요
while(1)
{
if(!(serv_sock.AcceptSocket()))
continue;
else
puts("new client connected");
pid = fork();
if(pid == -1)
{
serv_sock.CloseAcceptSocket();
continue;
}
if(pid == 0)
{
serv_sock.CloseListenSocket();
serv_sock.SendFile("img.jpg");
serv_sock.CloseAcceptSocket();
puts("client disconnected.");
return 0;
}
else
serv_sock.CloseAcceptSocket();
}
serv_sock.CloseListenSocket();
return 0;
사실 이 무한 루프는 빠져나갈 일이 없어요. 즉 계속 accept를 기다리는 상태가 되지요 따라서, 맨 아래 serv_sock.CloseListenSocket(); return 0;가 실행될 일이 없어요. 해당 코드를 남겨놓은 이유는 타이머 설정등을 통해 빠져나가는 break를 주었을 때 해당 부분을 빼먹지 말자는 느낌으로 적어놓을 것이지요. 즉, 서버 소켓을 열려있다는 것을 알려드리고 싶어서 적어놓았답니다.
여기까지 오시느라 수고 많으셨습니다. 매우매우 긴글이었어요.
쓰는 것도 지친데, 알고 쓰는 것도 힘든데 읽으신 여러분들에게 정말 큰 박수를 보냅니다.
다음 글에서는 멀티 플렉싱을 가지고 다중 서버를 구축하는 글로 찾아오겠습니다.
이상 WH였습니다.