본문 바로가기
Network

C언어로 서버 만들기

by 뇌 속의 통 2024. 11. 8.

1. 통신이란?

옛날에 불을 이용하여 통신.말 그대로 서로 의사소통하는 것.

 

어떤 신호를 보내면 이 신호를 다른 곳에서 알아들으면 통신 되는 것이다.

양측 서로 신호를 보낼 수 있다면 양방향 통신, 한쪽에서만 보낼 수 있다면 단방향 통신이다.

 

통신에서는 신호를 보내는 것도 중요하지만 그 신호의 의미를 알고 있어야 제대로 전달되기 때문에

의미도 매우 중요한 요소 중 하나이다.

 

이런 신호에 대한 의미를 보통 약속하는데 이 약속이 바로 프로토콜이다.

또한, 통신을 하기 위해선 물리적인 연결이 필요하며 이게 바로 랜선이다.

Wifi 같은 무선도 결국 랜선을 이용해서 통신이 된다.

 

기본 통신을 하기 위해선 반드시 물리적인 연결이 있어야 한다.

 

프로토콜(통신 규약)의 종류

TCP : 신호를 보내고 신호를 잘 받았는지 확인까지 받는 절차가 포함된 프로토콜이 TCP이다.

안정성이 높고 속도가 느리다. 인터넷에서 자료를 다운 받거나 할 때 주로 사용된다.

 

UDP : 신호를 보내기만 한다.(신호를 받았는지 확인철차가 없다.)

안정성이 TCP에 비해 낮으나 속도가 빠르다. 채팅, 게임, 영화 스트리밍 등과 같이 일부 데이터가 손실되어도 상관 없는 곳에 주로 사용되며 실제로 손실될 확률도 이더넷 기준 0.01%이하로 매우 낮다.

 

IP Version

IP : Internet Protocol의 약자로 인터넷 통신 규약을 뜻한다. 2가지 버전이 있다.

 

version 4 : 127.36.95.2 이런 형태로 사용되는 것.

version 6 : 사실 잘 안 쓴다. 인터넷이 엄청 흥하게 되면서 기존 4보다 더 많은 IP들이 필요할 것이라 추측해서 만들었다. 그래서 주소 범위가 훨씬 넓고 경우의 수가 엄청 많다. 그러나 공유기가 등장하면서 필요 없게 되버렸다.

 

공유기 : 대표 IP 주소 하나를 가지고 여러개의 기기에 공유해 주는 기기. 즉, IP 하나의 주소만 가지고 여러개의 기기들과 통신하게 된 것이다.

 

 

2. 소켓이란?

추상화된 개념으로 충전기 핀을 생각하면 쉽다.

내부가 어떻게 구현되었는지 알 지 모르지만 C타입 충전기에는 모든 C타입 구멍이 있는 장치들이 충전이 되는 것처럼 어떤 하드웨어건 소켓을 맞춰주면 통신을 시켜주는 것이다.

 

TCP 통신에서 소켓은 두가지가 있는데 하나는 서버고 다른 하나는 클라이언트다.

서버는 소켓을 열고 Listen하고 있는 상태, 클라이언트는 소켓을 열고 IP주소를 통해 서버로 접속을 시도하는 주체다.

 

서버에서는 빈 소켓을 만들고(함수 호출) Bind 과정을 통해 소켓과 특정 IP Port, 프로토콜(TCP, UDP 등)을 묶어준다. 그 다음 Listen을 한다.(고객을 기다린다.)

접속되면 Listen이 반환되고, Accept(수용) 단계가 된다. 그 다음 Send, Recive가 반복된다.

 

클라이언트에서는 빈 소켓을 만든다. Bind 과정과 비슷하게 IP Port, 프로토콜 등을 넣어준다.그 다음 Connect 단계에서 서버에 접속을 시도하게 된다. 접속되면 Send와 Receive를 반복하게 된다.

클라이언트 소켓은 클라이언트 당 하나씩 생성되기 때문에 클라이언트 수가 증가하면 소켓 수도 증가한다.

 

서버 소켓 하나를 만들고 클라이언트가 서버 소켓에 접속하면 클라이언트 소켓이 하나 생성된다.

클라이언트(접속한 컴퓨터)와 데이터(패킷)를 주고 받는 것은 서버 소켓이 아닌 생성된 클라이언트 소켓으로 통신하게 되는 것이다.

 

* 엔디안(Endian)

메모리에 데이터를 저장할 때 그 데이터를 쪼개서 저장하는데 이때 낮은 주소에 낮은 데이터를 넣는가? 또는 낮은 주소에 높은 데이터를 넣는가가 다르다.

 

운영체체(OS)에 따라 엔디안이 다르기 때문에 통신 시 운영체제가 다르면 같은 데이터가 다르게 읽힐 수 있다.

그래서 네트워크 통신 시 반드시 네트워크 오더를 사용하기로 약속함. 즉, 네트워크 통신에는 Big Endian 방식만 사용한다.

 

빅 엔디안(big endian) : 낮은 주소에 높은 바이트를 저장(선형, 우리가 숫자를 읽는 방식) 1 → 2 → 3 → 4

리틀 엔디안(little endian) : 낮은 주소에 낮은 바이트를 저장(선형의 반대) 4 → 3 → 2 → 1

 

##C언어로 실제 서버 만들어보기

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

//서버를 만들기 위해 필요한 Header file
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define CLNT_MAX 10

int g_clnt_socks[CLNT_MAX];
//클라이언트용 소켓 전역 변수로 선언
int g_clnt_count = 0;
//접속한 클라이언트의 총 수를 저장할 변수 선언

int main(int argc, char ** argv)
{
	int serv_sock;
	//서버용 소켓 선언
	int clnt_sock;
	//클라이언트 소켓 선언
	//서버용 소켓에 접속한 클라이언트가 accept 되면 생성되는 소켓임.
	
	struct sockaddr_in clnt_addr;
	int clnt_addr_size;
	//서버 소켓도 port 등 정보를 갖는 구조체와 그 사이즈를 선언하듯이
	//추후 생성될 클라이언트 소켓의 정보를 저장하기 위해 구조체와 사이즈를 선언해둔다.
	
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	//소켓을 만들어서 반환해주는 함수 socket();
	//첫번째인자는 IP version을 정해준다.
	//AF(PF)_INET : 숫자 2, IP version 4 //address에서는 AF, Protocol에서는 PF로 표기 상관은 X
	//AF(PF)_INET6 :  숫자 23, IP version 6 //address에서는 AF, Protocol에서는 PF
	
	//두번째 인자는 소켓 전송 방식을 지정한다.
	//SOCK_STREAM : 연결 지향적인 방식(TCP)
	//SOCK_DGRAM : 비연결 지향적인 방식(UDP)
	//SOCK_RAW : 가공하지 않은 소켓(TCP나 UDP가 아닌 그 아래 단계 소켓)
	
	//세번째 인자는 프로토콜을 지정한다.
	// 0 : 자동으로 첫번째, 두번째 매개 변수를 기준으로 인자값을 지정해준다.
	//IPPROTO_TCP : TCP 프로토콜을 사용
	//IPPROTO_UDP : UDP 프로토콜을 사용
	//IPPROTO_HOPOPTS : 1, 2번 매개 변수을 기준으로 자동으로 인자값 지정
	
	//두번째 인자와 세번째 인자가 매우 비슷하지만 사실 다른 것이다.
	//두번째 인자는 우편을 보낸다고 하면 직접 전달할지 우편으로 보낼지 등을 정하는 것이고
	//세번째 인자는 우체국, 또는 편의점 등 어떤 우편 서비스를 사용할지 선택하는 것이다.
	
	struct sokaddr_in serv_addr;
	//Bind를 해주기 위해 Bind할 Port 주소 등을 저장할 구조체 변수 하나를 선언
	
	serv_addr.sin_family = AF_INET;
	//IPv4로 설정
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	//소켓 주소를 어떤 주소로 Bind할 지 설정
	//htonl : host to network long 네트워크 통신을 할때 Host의 오더 방식을 network 오더 방식으로
	//변경하는 의미다.
	//INADDR_ANY : 모든 네트워크 인터페이스를 의미
	//즉, 모든 네트워크 인터페이스를 통해 들어오는 데이터들을 받아오는데 호스트의 오더 방식을
	//네트워크 오더 방식으로 변경하여 데이터를 올바르게 받아온다는 뜻이다.
	
	serv_addr.sin_port = htons(atoi(argv[1]);
	//포트를 설정하는 함수, 사용할 포트 번호를 넣어준다. ex) 7989
	//단 여기서는 입력받는 포트를 언제든지 편하게 바꾸기 위해 변수로 주었다.
	//argv[1] : 배열의 첫번째 인덱스를 포트 번호로 받아오고
	//atoi : integer로 변환하고
	//htons : 네트워크 오더로 변환하여 sin_port로 넣어준다
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1)
	{
		printf("bind error\n");
	}
	//bind 해주는 함수
	//bind가 안될 수 있으니, if문을 통해 예외 처리하도록 한다.
	//첫번째 인자로 bind 할 sock을 가져온다.
	//두번째 인자로 bind 할 Port, 주소 등의 정보를 갖고 있는 구조체를 가져온다.
	//이때 이 구조체를 sockaddr*로 형변환하고, 원본 데이터를 참조하도록 해준다.
	//세번째 인자로 구조체의 size를 가져온다.
	
	if(listen(serv_sock, 5) == -1)
	{
		printf("listen error");
	}
	//이제 연결을 기다리는 listen 함수를 호출한다.
	//첫번째 인자는 열고 기다리고 있는 소켓
	//두번째 인자는 listen 상태에서 대기할 최대 클라이언트 수다.
	//즉, accept가 늦어져서 5명을 넘어가게 되면 error가 발생한다.
	
	while(true)
	{
		clnt_addr_size = sizeof(clnt_addr);
		//앞서 선언한 클라이언트 소켓 사이즈에 접속한 클라이언트 소켓 사이즈를 넣어준다.
		clnt_sock = accept(serv_sock,(struct sockaddr*) &clnt_addr, &clnt_addr_size);
		//반복문을 돌면서 클라이언트를 수용한다.
		//accept 함수를 통해 소켓에 접속한 클라이언트의 소켓을 생성(port 등 정보를 저장) 및 반환해준다.
		g_clnt_socks[g_clnt_count++] = clnt_sock;
		//클라이언트 소켓 배열에 클라이언트 소켓을 저장한다.
		
	}

}

 

 

 

 

'Network' 카테고리의 다른 글

Network 기초 2  (0) 2024.11.17
Network 기초  (1) 2024.11.09