일단 함수들을 설명하기전에, sockaddr, sockaddr_in 구조체부터 살펴봐야한다.
소켓의 주소를 담는 역할을 하는 구조체이다.
정의는 다음과 같다.
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바이트로 주소를 잡아주는 역할을 한다.
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 주소와 포트번호가 같이 들어가서, 이를 편하게 구분하기 위해 사용한다.
이 밖에도 IPv6일떄 사용되는 sockaddr_in6 구조체, AF_UNIX, AF_LOCAL일때 쓰는 sockaddr_un 구조체가 있다.
공식 홈페이지에선 이렇게 설명하고 있다.
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에 값을 넣어주기 위해 쓰인다.
분석해보면 알겠지만
이 과정전에
memset(&serv_addr, 0, sizeof(serv_addr));
을 통해 메모리 초기화를 하는 경우도 있는것같다. 없어도 작동은 하는것같다.
htons는 데이터를 네트워크 바이트 순서로 변환하는 함수이다. 포트는 2바이트 단위의 자료형이기때문에 htons를 통해 byte order로 정리한다.
자세한 내용은 여기 에 있다.
이제 필요한건 다 알아봤으니 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 의 길이를 넣은것을 알 수 있다.
int listen(int sock, int backlog);
간단한 역할을 하는 함수인만큼 그 내부구성도 간단하다.
sock으로 ㄷ연결요청 대기상태로 두고자하는 소켓의 파일디스크립터를 받는다.
backlog는 연결요청 대기 큐의 크기정보를 전달한다. 이 크기만큼 클라이언트의 연결요청을 대기시킨다.
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
여기서는 3으로 값을 줬지만 다른 값을 줘도 되는듯하다.
대망의 마지막 함수이다.
int accept(int sock, (struct sockaddr*) addr, socklen_t* addrlen)
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)을 붙여줘야만 오류를 방지할 수 있다.
끝난줄 알았는데 아니였다.
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를 가진 헤더파일이 없기 때문이다.
그 이유는 이 글 참고하자.
우선 사용된 함수부터 알아보자
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이 보내주는 메세지를 받는것을 알 수 있다.
ssize_t write(int fd, void *buf, size_t nbytes);
read와 똑같은데 데이터를 보내는 역할을 한다.
fd는 데이터를 보낼 소켓의 파일 디스크립터, 즉 클라이언트 디스크립터이다.
int send(int socket, const void *msg, size_t len, int flags);
int recv(int socket, void *buf, size_t len, int flags);
recv는 잘 쓰이지 않는거같고
read write send가 자주 쓰이는것같다.
다 끝났으면
int close(int fd);
로 닫아주면 된다.
이렇게 하면 서버쪽 코드분석은 끝이다.
거의 유사한 구조인 클라이언트쪽 분석도 해보면
tcp/ip소켓통신의 기본을 익힌셈이다.
그다음은 본격적으로 채팅프로그램을 만들어볼 수 있으면 좋겠다.