본문 바로가기

임베디드 리눅스

네트워크 프로그래밍-소켓 통신 서버 구현 3

728x90
반응형

 

안녕하세요. 개발자 WH입니다.

시간이 정말 눈 깜박하면 하루가 지나갑니다.

시간은 지나가고 물가는 오르는 데, 뭐만 안오르네요 ㅎㅎ

여튼 이번 글도 이어서 시작하겠습니다.

 

오늘은 저번 글에 이어서 내용을 진행할 것이기 때문에

저번 내용이 궁금하신 분들은 아래 글을 참조해주세요.

2022.02.03 - [분류 전체보기] - 네트워크 프로그래밍 - 소켓 통신 서버 구현 2

 

네트워크 프로그래밍 - 소켓 통신 서버 구현 2

안녕하세요. WH입니다. 이번 글에서 정리할 내용은 소켓 프로그래밍 기본 코드 구현 및 시스템 콜 정리입니다. 물론 파일을 전송하는 것까지는 아니고, 서버와 연결됨을 확인하는 정도 선까지만

developer-wh.tistory.com

반응형
반응형

우선 코드를 봐야겠죠? 여러분 사실 이건 비밀인데요.

이런 글을 읽을 때는 코드를 여러번 작성해 보는 것이 진짜 도움이 많이 됩니다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[]){
    int serv_sock, clnt_sock;
    
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;
    
    char message[] = "This is a server."
    if(argc != 2){
    	printf("Usage : %s<prot>\n", argv[0];
        exit(1);
    }
    
    serv_sock=socket(AF_INET, SOCK_STREAM,0);
    if(serv_sock == -1)
    	error_handling("socket() errer.\n");
    
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_familly=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock,(struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1)
    	error_handling("bind() error.\n");
    
    if(listen(serv_sock,5)==-1)
    	error_handling("listen() error.\n");
    
    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
    if(clnt_sock==-1)
    	error_handling("accept{} error.\n");
    
    write(clnt_sock,message,sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

여튼 오늘은 bind 부터 시작하도록 합니다.

 

1. 소켓에  주소를 할당하는 bind()

bind() 에는 3가지 인자가 전달됩니다. bind의 의미 먼저 설명해보면, socket()을 통해 파일 디스크립터를 받은 sockfd에 *addr 주소를 할당할 것이며, 주소정보를 담은 변수를 통해 성공 여부를 판단하겠다라는 의미니다.

//함수 원형
int bind(int sockfd, struct sockadddr *addr, socklen_t addrlen);

sockfd

  - sockfd의 반환 값은 socket()을 통해 받은 파일 디스크립터입니다. 

*addr

  - 저번 글에서 sockaddr에 14바이트에는 IP와 PORT에 대한 정보가 들어가 있다고 했죠? 그리고 실제 사용 예시를 보면 sockaddr_in을 형 변환하여 sockaddr로 변환합니다. 이는 즉, PORT와 IP에 대해 우리가 관리하기 쉽도록 sockaddr_in에서 쓰고 데이터 처리가 편하도록 sockaddr로 변환한다라는 의미가 2번째 전달 인자의 의미 입니다.

addrlen

  - 문자의 내용을 토대로 유추할 수 있는 것처럼 주소 정보를 담은 변수의 길이를 나타냅니다. 

 

bind()는 성공시 0, 실패시 -1을 반환합니다. 어렵나요? 쉽게 한마디로 정리해드리면 bind를 통해 소켓에 ip주소와 port 번호를 할당했다고 생각하시면 됩니다.

 

2. 연결 요청 대기 listen()

지금까지 단계로 소켓은 ip와 port 번호를 가지게 되었습니다. 이제 클라이언트가 해당 소켓에 연결할 수 있도록 그 요청을 대기하는 상태로 만들어야 되는데요. listen() API가 이 역할을 합니다. 즉, listen함수가 호출된 후에야 클라이언트에서 connect을 호출할 수 있게 된딥니다.

 

//함수 원형
int listen(int sock, int backlog);

sock

  - 소켓의 디스크립터를 넣어주면 되는데요, 지금 연결을 담당하고 있는 소켓은 위 코드에서 serv_sock 이고 serv_sock은 socket()을 통해 티스크립터를 받았음으로 serv_sock이 들어가게 되겠죠

backlog

  - 연결요청을 대기하는 큐의 크기인데요. 위 예제에서는 5개의 연결 대기가 가능한 큐의 크기를 설정했습니다. 최대 5개의 큐가 들어갈 수 있습니다. 음 사실 대기한다는 의미에서 버퍼라고 생각하시면 편하실까요? 컴퓨터에서 데이터를 잠깐 대기시키는 역할을 하는 버퍼는 큐로 구현된답니다.

3. 연결 요청 수락 accept()

대기 중인 클라이언트의 요청을 수락하고 데이터를 주고 받을 수 있게 하는 함수가 바로 accept()인데요. accept()는 성공 실패 여부를 판단하는 반환값이 아닌 파일 디스크립터를 반환한답니다. 무슨 의미냐면, 즉 accept()를 통해 새로 할당 받은 소켓을 이용해 데이터 송수신을 한다라는 의미입니다. 그럼 전에 있던 serv_sock은요? serv_socket은 연결 요청을 대기시키는 과정까지가 역할이에요.

 

그러면 쉽게 예상할 수 있겠죠? 새로운 소켓에는 무엇을 줘야할까요? 소켓 디스크립터, IP와 PORT 번호, 그리고 주소 길이가 들어가겠네요?

//함수 원형
int accept(int sock, struct socakddr *addr, socketlen_t *adddrlen);

//위 코드에서 사용
clnt_addr_size = sizeof(clnt_addr)
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);

 

여기까지 잘따라 오셨죠? 음.. 조금만 조금만 더 깊게 들어가 볼게요. 난 여기까지만 할래하시는 분들은 넘어가도 좋아요.

 

  - accpet() 함수를 호출하면 호출한 프로세스는 블락됩니다. 클라이언트로부터 3way hand shake 요청이 올 때까지요. 요청이 들어오면 incompleted Queue에 그 정보를 넣어 놓고 다시 클라이언트에게 SYN+ACK를 보냅니다. 클라이언트는 SYN+ACK를 받으면 connect() 호출이 정상적으로 리턴 되며 연결이 성공했다고 인지후 ACK를 서버로 보내게 된답니다. 여기서 서버가 ACK를 받으면 Incompleted Queue에 있던 클라이언트 request를 Completed Queue로 옮긴 답니다. 이 Queue에 옮기고 나면 서버의 커널이 블락 되어있던 프로세스를 깨우면서 새로운 connected 소켓을 만들고 서버와 클라이언트가 connected 소켓을 통해 데이터를 주고 받게 되는 거랍니다. 이게 바로 3 way hand shake에요.

 

  만일 Incompleted Queue가 다 찬 상황에서 클라이언트가 요청을 보내면 서버는 아무것도 하지 않고 그냥 dicard합니다. 즉, 연결이 거절되었다고 알려주는게 아니라 그냥 버린답니다. 그래야만 클라이언트에서 ACK를 받지 못했 타임 아웃이 발생하고 다시 SYN을 보내게 되거든요. Queue가 비었을 때 요청을 처리하게 끔 만들어 준것이죠. 뭐 이정도 하고 정리를 해보자면, accept() 호출은 서버에서 마지막 ACK를 받은 후에 반환이 된답니다. 즉 listen() 호출 후 클라이언트로부터 연결요청(SYN)을 받았다고 accpet()가 반환된다는 게 아니라는 말이죠. SYN이 오면 단순이 SYN+ACK를 클라이언트에게 응답으로 보내주고 Incompleted Queue에 클라이언트 요청 정보를 담는게 끝이라는 말입니다.

 

4. 데이터 송수신 write() 및 연결 해제 close()

마지막으로 wirte() 함수 인데요, write()함수를 통해 실제 데이터를 출력할 수 있답니다. 읽으면 무슨 함수가 필요할까요? 바로 read() 함수 입니다. 해당 내용은 아래 글에서 다뤘음으로 넘어가도록 하겠습니다. 기억이 나질 않는다면 아래글을 참조해주세요.

2022.02.03 - [임베디드 리눅스] - 네트워크 프로그래밍-파일 디스크립터

 

네트워크 프로그래밍-파일 디스크립터

안녕하세요? WH입니다. 네트워크 프로그래밍 부분은 사실 간단한 예제만 코드로 보여드리고 넘어가려고 했는 데, 처음 글을 작성하고 보니 사실 너무 불친절하다는 생각이 들었습니다. 그래서

developer-wh.tistory.com

와 긴 여정이었어요. 여기까지 적는게 쉽지만은 않았지만

기본 서버구현이 끝이 났네요. 다음 글에서는 파일을 전송할 수있도록 코드를 올려볼지,

더 깊은 지식을 다뤄볼지 고민해보겠습니다.

사실, 이 글 전에 라이브러리를 정리해서 올리려다가, 보류했습니다.

커널 내용까지 알아야할 것같아서요.. 그건 차차하도록 하자구요.

그럼 이상 WH였습니다.

728x90
반응형