소켓 통신 코드 분석 2편

강한친구·2022년 3월 12일
0

Server Studies

목록 보기
12/27

일단 함수들을 설명하기전에, sockaddr, sockaddr_in 구조체부터 살펴봐야한다.

sockaddr

소켓의 주소를 담는 역할을 하는 구조체이다.
정의는 다음과 같다.

struct sockaddr { 
	u_short sa_family; // address family, 2 bytes 
    char sa_data[14]; // IP address + Port number, 14 bytes 
};

unsigned short 자료형을 쓰는 sa_family는 주소체계를 구분하기 위한 변수이며, 2바이트이다.
sa_data는 실제 주소를 저장하기 위한 변수이다. 14바이트로 구성되어 있다.
즉, 이 둘은 16바이트로 주소를 잡아주는 역할을 한다.

sockaddr_in

struct sockaddr_in { 
  short sin_family; // 주소 체계: AF_INET 
  u_short sin_port; // 16 비트 포트 번호, network byte order 
  struct in_addr sin_addr; // 32 비트 IP 주소 
  char sin_zero[8]; // 전체 크기를 16 비트로 맞추기 위한 dummy 
}; 
struct in_addr { 
	u_long s_addr; // 32비트 IP 주소를 저장 할 구조체, network byte order 
};

위의 sockaddr에서 sa_family가 AF_INET인 경우 사용하는 구조체이다. sockaddr을 그대로 사용할 경우 sa_data에 ip 주소와 포트번호가 같이 들어가서, 이를 편하게 구분하기 위해 사용한다.

  • sin_family = 항상 AF_INET을 지정
  • sin_port = 포트번호를 가진다. 포트는 0~65535이다. 따라서 2바이트로 나타ㅐㄹ 수 있는 값이다.
  • sin_addr = 호스트(서버)의 ip주소를 나타낸다. INADDR로 시작하는 값을 가져야한다.
    • 실제로 이 값으로 INADDR_ANY를 사용하는것을 볼 수 있는데 이는 서버의 ip주소를 자동으로 찾아서 대입해주는 기능을 하는 함수이다. 서버는 NIC를 2개 이상 가지고 있는 경우가 많은데 이런 경우 멀티 네트워크 카드를 동시에 지원해주고 특정 IP를 지정해주면 서버컴퓨터가 바뀌면 코드를 다시 써야하지만 그렇지 않아도 된다는 장점이 있다.
  • sin_zero = 모두 0으로 채워둔 8바이트 16비트짜리 더미데이터이다. sockaddr 구조체와 크기를 맞추는 목적으로 사용한다.

이 밖에도 IPv6일떄 사용되는 sockaddr_in6 구조체, AF_UNIX, AF_LOCAL일때 쓰는 sockaddr_un 구조체가 있다.

INADDR?

공식 홈페이지에선 이렇게 설명하고 있다.

IPv4 주소를 대표하는 구조체이며, 동시에 타입을 정의한다. 또한, in_addr 구조체로 변환이 가능하다.

typedef struct in_addr {
  union {
    struct {
      UCHAR s_b1;
      UCHAR s_b2;
      UCHAR s_b3;
      UCHAR s_b4;
    } S_un_b;
    struct {
      USHORT s_w1;
      USHORT s_w2;
    } S_un_w;
    ULONG S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

구조체의 실 사용례

    struct sockaddr_in address;

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

우리가 작성한 서버코드에는 이런식으로 쓰였다.
bind 직전에 미리 작업하여 bind에 값을 넣어주기 위해 쓰인다.

분석해보면 알겠지만

  • sin_family에는 AF_INET
  • sin_addr에는 INDOOR_ANY
  • sin_port 에는 htons( PORT )
    가 들어간다.

이 과정전에

memset(&serv_addr, 0, sizeof(serv_addr));

을 통해 메모리 초기화를 하는 경우도 있는것같다. 없어도 작동은 하는것같다.

htons

htons는 데이터를 네트워크 바이트 순서로 변환하는 함수이다. 포트는 2바이트 단위의 자료형이기때문에 htons를 통해 byte order로 정리한다.
자세한 내용은 여기 에 있다.

이제 필요한건 다 알아봤으니 bind로 넘어가자

Bind()

	int bind(int sockfd, (struct sockaddr) *myaddr, socklen_t addrlen);
  • sockfd = 주소정보를 소켓에 받는 역할을 하는 함수이다.
    sock의 반환값인 피일 디스크립터. ip와 port 정보를 담고 있다.

  • myaddr = 할당하고자 하는 주소정보를 지니는 구조체 변수의 주소값을 가진다.

  • addrlen은 주소의 길이이다.

    이 함수도 다른 함수와 마찬가지로 성공시 0, 실패시 -1을 반환한다.

    실제로 작성하면 다음과 같다.

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

앞서 socket에서 결과값을 받은 server_fd를 sockfd로 넣고,
sockaddr struct의 address 레퍼런스 주소값을 두번째로,
마지막으로는 해당 address 의 길이를 넣은것을 알 수 있다.

listen()

int listen(int sock, int backlog);

간단한 역할을 하는 함수인만큼 그 내부구성도 간단하다.
sock으로 ㄷ연결요청 대기상태로 두고자하는 소켓의 파일디스크립터를 받는다.
backlog는 연결요청 대기 큐의 크기정보를 전달한다. 이 크기만큼 클라이언트의 연결요청을 대기시킨다.

    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

여기서는 3으로 값을 줬지만 다른 값을 줘도 되는듯하다.

accpet()

대망의 마지막 함수이다.

int accept(int sock, (struct sockaddr*) addr, socklen_t* addrlen) 
  • sock = 소켓의 파일 디스크립터이다.
  • addr = sockaddr 구조체를 받는다. 연결요청한 클라이언트의 주소정보를 담을 변수의 주소값이다.
  • addrlen = 두번째 인자로 전달된 addr의 크기정보를 전달한다. 다만 addr 미리 크기정보를 전달한 다음 주소를 전달한다.
  if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {
        perror("accpet");
        exit(EXIT_FAILURE);
    }
struct sockaddr_in st_clnt_addr;
    int clnt_addr_size = sizeof(st_clnt_addr);
    int acceptret = accept(serv_sock, (struct sockaddr*) &st_clnt_addr, (socklen_t*)&clnt_addr_size );
    if(acceptret == -1) errhandle((char*)"accpet() err!");

accept의 경우 두가지 방법이 있다. 기존에 사용했던 sockaddr address에 클라이언트 정보를 넣어주는 방법과,

sockaddr_in clnt_address를 만들어서 그 안에 값을 넣어주는 방법이 있다.

addrlen의 경우, 확인해보면 int형으로 되어있는것을 알 수 있는데, 함수는 socklen_t 를 요구하고 있다. 따라서 앞에 (socklen_t)을 붙여줘야만 오류를 방지할 수 있다.

send, recv, read, write

끝난줄 알았는데 아니였다.

valread = read( new_socket, buffer, 1024 );
    printf("%s\n", buffer);
    send(new_socket, hello , strlen(hello), 0 );
    printf("Hello message sent\n");
    return 0;

서버쪽은 메세지 주고받기가 이렇게 구성되어 있다.

사실 read send / write recv 는 같은 역할은 한다.
근데 왜 다르게 쓰는걸까? 일단 근본적으로 windows에서는 read, wrtie가 없다. read write를 가진 헤더파일이 없기 때문이다.

그 이유는 이 글 참고하자.

우선 사용된 함수부터 알아보자

read

ssize_t read(int fd, void *buf, size_t nbytes);

fd는 파일 디스크립터, buf는 수신한 데이터를 저장할 버퍼 주소값, 그리고 nbyte는 수신할 최대 바이트수이다.

server의 경우 client의 hello message를 먼저 수신하고 보내기때문에 read가 먼저오게 된다.

    valread = read( new_socket, buffer, 1024 );
    printf("%s\n", buffer);

우리가 작성한 코드를 보면 accept로 받은 clnt_fd를 넣어주고 위에 선언한 버퍼, 그리고 1024 바이트를 지정해주고 client socket이 보내주는 메세지를 받는것을 알 수 있다.

wrtie

ssize_t write(int fd, void *buf, size_t nbytes);

read와 똑같은데 데이터를 보내는 역할을 한다.
fd는 데이터를 보낼 소켓의 파일 디스크립터, 즉 클라이언트 디스크립터이다.

send / recv

int send(int socket, const void *msg, size_t len, int flags);
int recv(int socket, void *buf, size_t len, int flags);
  • socket = 통신의 대상이 되는 fd
  • msg = 보낼 자료의 포인터 (미리 작성한 char*문, 혹은 buffer 주소)
  • void *buf = 메세지를 저장할 버퍼포인터
  • size_t len = 메세지의 크기
  • flag = 플래그는 옵션을 넣을수 있는 공간이다.

recv는 잘 쓰이지 않는거같고
read write send가 자주 쓰이는것같다.

close

다 끝났으면

int close(int fd);

로 닫아주면 된다.

이렇게 하면 서버쪽 코드분석은 끝이다.
거의 유사한 구조인 클라이언트쪽 분석도 해보면
tcp/ip소켓통신의 기본을 익힌셈이다.

그다음은 본격적으로 채팅프로그램을 만들어볼 수 있으면 좋겠다.

0개의 댓글