본문 바로가기

임베디드 리눅스

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

728x90
반응형

안녕하세요. WH입니다.

이번 글에서 정리할 내용은 소켓 프로그래밍 기본 코드 구현 및 시스템 콜 정리입니다.

물론 파일을 전송하는 것까지는 아니고,

서버와 연결됨을 확인하는 정도 선까지만 구현해보도록 할게요.

 

우선 코드입니다. 기본 내용이 기억이 나질 않으신다면 아래 글을 참조하시길 바랄게요.

2022.02.03 - [임베디드 리눅스] - 네트워크 프로그래밍-소켓 통신 구현 1

 

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

안녕하세요. 개발자 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;
}

1. 소켓 생성

 

int serv_sock;
serv_sock=socket(AF_INET,SOCK_STREAM,0);

이 두 문장은 소켓을 생성하는 문장인데요, socket 함수를 통해 소켓을 생성하게 됩니다. 전에 파일 디스크립터에서 설명드렸지만, serv_sock 값이 파일 디스크립터 번호에 해당하고, 운영체제가 이를 항당하게 된답니다.

 

socket( AF_INET, SOCK_STREAM, 0 )

참고로 AF_INET과 PF_INET은 동일하고, ipv4프로토콜 체계를 사용하겠다는 뜻입니다. 즉 32bit 프로토콜을 사용하겠다는 의미죠. SOCK_STREAM은 연결 지향형 소켓을 생성하겠다라는 의미인데, ipv4 에 연결 지향형은 TCP, SCTP 등등 이 존재함으로 0 (default) 이라는 인자를 통해서 무슨 프로토콜을 사용할지 구체적으로 명시해 주게 된답니다.

 

참고

AF_INET : 상대방의 응용프로그램과 통신하기 위한 조건은 Adress family_internet 즉 동일한 주소체계, 동일 프로토콜로 인터넷 프로토콜을 사용하겠다라는 의미랍니다.

 

SOCK_STREAM

소켓의 유형에 대해 기억이 나실까요? 안난다면, 윗에 참조하라는 부분을 꼭 보시길 바랍니다.

sys/socket.h에서는 소켓의 종류를 번호로 구분하는데

 

SOCK_STREAM == 1

SOCK_DGRAM == 2

SOCK_RAW == 3

SOCK_PACKET == 10

으로 정의 되어 있답니다. 2번은 비연결성 소켓 즉 UDP 서비스를 지원하는 프로토콜을 사용할 때, 3번은 어플리케이션 계층에서 네트워크 계층에 명령을 내리는 소켓을 사용하고 싶을 때, 10번은 어플리케이션 계층에서 데이터 링크 계층으로 바로 명령을 내리고 싶을 때 사용한답니다.

 

0 : 디폴트 프로토콜

응용프로그램이 Layer4에 있는 프로토콜중 어떤 것을 선택할 것인지에 대한 매개변수인데요

Stream Socket

TCP

IPPROTO_TCP == 6 

Datagram Socket

UDP

IPPROTO_UDP == 17

Raw Socket

IPPROTO_RAW == 255

Packet Socket

<linux/if_ether.h>에 정의

ETH_P_ALL // 이더넷의 모든 패킷

ETH_P_IP // 이더넷에서 IP에 관한 것

ETH_P_ARP // 이더넷에서 ARP(Address Resolution protocol)에 관한 것

ETH_P_IPV6 // 이더넷에서 IPv6에 관한 것

이렇게 정의 되어 있어요. 즉 SOCK_STREAM 에서는 TCP를 SOCK_DGREAM 에서는 UDP가 기본이 되겠지요.

socket() 파일 디스크립터

socket() 시스템콜로 소켓을 생성하면 보통 3번을 리턴하기 때문에 serv_sock에는 3번이 들어가게 된답니다.

( 물론, 다를수도 있으니 확인해 보시면 될듯합니다. 어렵지 않아요 printf( "%d\n", serv_socket) 을 해주면 되겠지요? )

2. 소켓의 주소 할당

socket() 함수를 통해 fd( 파일 디스크립터 )값을 할당받았고, 소켓의 유형까지 지정 했는데요, 이 소켓은 네트워크 어딘가의 클라이언트와 통신을 해야해요. 그럼 클라이언트가 이곳을 찾아올 수 있어야 겠네요? 그럼 무엇이 필요하냐면, 바로 IP주소와 PORT 번호랍니다. 소켓에 IP번호가 있어야 서버가 있는 컴퓨터로 들어올 수 있고, PORT 번호가 있어야 프로세스를 특정할 수 있기 때문이죠.

struct sockaddr_in serv_addr;

구조체 변수 serv_addr에 IP와 PORT 번호가 부여되게 되는데요. sockaddr_in 구조체는 <netinet/in.h>에 정의되어 있고 아래와 같이 정의되어 있어요.

struct sockaddr_in{
    sa_family_t sin_family; //16비트
    in_port_t sin_port; // 16비트 포트번호
    struct in_addr sin_addr;// 32비트 IP 주소
    char sin_zero[8];// 전체 크기를 16 비트로 맞추기 위한 dummy
};

sin_family 는 주소 체계를 포함하기 위해, sin_port는 port 번호를 부여하기 위해, sin_addr는 ip주소를 담기 위해, sin_zero는 8비트 모두 0으로 채워지는 부분이랍니다. sin_port는 __uint16_t로 선어되어 있으며 16비트임을, ip 주소는 32비트로 선언되어 있습니다. 즉 이말은 ipv4를 사용하겠다는 뜻이 되지요.

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=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,sizedof(serv_addr))==-1)
	error_handling("bind() error.\n");

//sockaddr 구조체
struct sockadddr{
    __uint8_t sa_len;
    sa_family_t sa_family;
    char sa_data[14];
};

 

첫 번쨰 문장 

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

이 문장을 보기 전에 bind 에서 sockaddr 형으로 형변환됨을 주목해야 하는데요. sockaddr 구조체를 보면, sa_data를 통해 ip주소와 port 번호를 특정할 수 있어야하는데, ipv4의 경우 14바이트를 채우지 못합니다. 따라서 IPv4 의 포트 번호 16비트 + ip주소 32비트를 채우고 나머지 8바이트는 sin_zero로 채워 준것이랍니다.

 

그아래로 3 문장은 구조체 변수에 따라 채워 넣어준 것이지요.

 

serv_addr.sin_family=AF_INET;

  IPv4에 해당하는 주소체계를 채워 준 것입니다.


serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

  ip를 할당하는 문장으로, htonl함수는 host to network(long)으로 해석되는데요, long 자료형의 변수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환해 주는 함수랍니다. 바이트 순서이는 리틀엔디안과 빅엔디안이 있는데요. 네트워크 표준은 빅엔디안 임으로 리틀엔디안으로 사용할 수도 있는 순서를 빅 엔디안으로 변경해주는 것이랍니다.

 

INADDR_ANY

  서버에는 여러 개의 랜카드가 있을 수 있고, 그 랜카드 마다 할당된 이더넷 주소가 여러 개 있을 겁니다. 또한 그 여러 개의 이더넷 주소에 해당하는 아이피 주소 역시 여러 개일겁니다. 즉 한 컴퓨터가 여러 개의 아이프 주소를 가질 수 있는데, 내가 가진 어떤 아이피 주소로 패킷이 들어오더라도 그것을 다 처리하겠다는 의미로 사용합니다. 조금만 더 자세히 설명드리면 <netinet/in.h>에 #define INADDR_ANY 0x00000000 로 정의 되어 있고 16진수 1비트는 2진수 4비트로 치환되는 데 4*8 = 32비트 = 4바이트가 되고 이 모든 것이  0으로 정의 되어 있다는 겁니다. IPv4에서 ip를 4바이트로 나타내죠? 4바이트가 모두 0인경우 자신의 아이피 주소를 나타낸답니다. 결론, 내가 가진 아이피주소가 여러개가 있을 수 있는데 그 중 아무거나 쓰겠다는 뜻이에요.

 

참고

리틀 엔디안 : MSB가 맨 왼쪽

빅 엔디안 : MSB가 맨 오른쪽

 

serv_addr.sin_port=htons(atoi(argv[1]));

  port 번호를 할당하는 문장인데요. htons는 host to nestwork (short)로 해석되고, 즉 short형( 2바이트 )의 port 번호를 host 바이트 순서에서 network 바이트 순서로 변환해주겠다는 뜻입니다. 

 

생각보다 글이 길어지네요. 원래 한편에다 다 끝내려고 했는데 나눠서 올리도록 하겠습니다.

다음 편은 아마 몇 줄 안되지만, bind, listen, accept, close에 대해서 다루도록 하겠습니다.

읽어주셔서 감사합니다. 이상 WH였습니다.

 

728x90
반응형