티스토리 뷰

서버-클라이언트 환경을 만들기 위한 과정


서버

 : Socket 생성 → Socket에 이름연결(bind) → 클라이언트의 연결을 기다림(listen) → 클라이언트 받아들임(accept) → 클라이언트의 명령을 받아서 적절한 서비스를 수행


클라이언트

 : Socket 생성 → 서버에 연결 시도(connect) → 서버에 각종 명령을 전달







소켓 (위키백과 : https://ko.wikipedia.org/wiki/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC_%EC%86%8C%EC%BC%93)

 - 컴퓨터가 통신을 하기 위한 도구이다. 이 소켓을 이용해서 컴퓨터간에 데이터를 주고받을 수 있다.

 - 소켓은 모든 운영체제에서 지원해주는 것이다. 

 - 패킷이란 소켓이 주고받는 데이터를 정형화 해놓은 것이다.

 - 소켓은 다음과 같은 요소들로 구성되어있다.

· 인터넷 프로토콜 (TCP, UDP, raw IP)

· 로컬 IP 주소

· 로컬 포트

· 원격 IP 주소

· 원격 포트

 - 소켓에는 크게 2가지 유형이 있다. 연결지향형, 비연결 지향형이다.






연결지향형

 : 각 소켓끼리 서로 연결된 상태에서 통신을 하는 것을 말한다. 

일반적으로 말하는 TCP/IP 가 바로 이 연결지향형 소켓에 해당된다.

연결된 상태에서 통신하기 때문에 연결된 대상 외에 다른 대상과는 통신이 불가능하고 

만약 다른 대상과 통신을 하고 싶다면 그 대상과 연결되는 새로운 소켓을 하나 더 만들어 주어야 한다.

연결지향형 소켓은 데이터를 보내두고 제대로 다 받았는지 중간중간 확인작업을 하게 된다. 

그러므로 안정적으로 데이터를 모두 보낼 수 있다. 

상대적으로 비연결지향형에 비해 속도가 느리지만, 안정적으로 데이터를 전송해줄 수있다. 

데이터가 절대 소실되어서는 안되는 경우 무조건 TCP를 활용한다.

따라서 대다수의 경우 TCP를 활용하고있다.



비연결지향형

 : 연결되지 않은 상태에서 내가 원하는 주소에 데이터를 보낼 수 있는 통신방법을 말한다. 

UDP가 비연결지향형 소켓에 해당된다. 

데이터를 보내고 난 후에 확인작업을 안해주어서 데이터가 다 수신을 했는지 확인이 불가능하다. 

상대적으로 연결지향형에 비해 속도가 빠르지만 중간에 데이터가 소실될 수 있다. 

예시로는 동영상 스트리밍 서비스를 생각해 볼 수 있다.






<서버>


# 소켓을 사용하기위한 헤더 include 및 기본설정 지정


#

#include <WinSock2.h> & #pragma comment(lib, "ws2_32")

소켓을 사용하기 위해선 라이브러리를 링크 걸어줘야 한다. 

ws2_32 는 lib파일이다. lib파일은 실제 구현부분을 바이너리화 시킨 파일이다.

보통은 헤더에 선언한 기능들을 cpp에 구현하고, 이 cpp에 구현된 내용들이 라이브러리화 된다.

즉, WinSock2.h 에 선언한 기능들을 사용하겠다고, ws2_32 라이브러리를 링크걸어주는 것이다.

참고로 stdio.h를 사용하는 이유는 C++에서 C문법을 가져다 쓰며 구현하려하기 때문이다.


#

#define PORT 4578 & #define PACKET_SIZE 1024

PORT를 사용할때는 예약된 포트를 제외하고 사용하여야 한다. (ex) 21 : FTP포트, 80 : HTTP포트, 8080 : HTTPS포트)

따라서 4자리 포트중 임의의 숫자를 할당하였다.

또한 패킷사이즈를 정의해주었다.


#

WSADATA wsaData; & WSAStartup(MAKEWORD(2, 2), &wsaData); & WSACleanup();


WSADATA 구조체

 : Windows의 소켓 초기화 정보를 저장하기위한 구조체. 이미 선언되어있는 구조체이다.


WSAStartup(소켓버전, WSADATA 구조체 주소);

 : 이 함수를 가장 먼저 호출해준다. 이 함수를 호출해서 윈도우즈에 어느 소켓을 활용할 것인지 알려준다. 

첫번째 인자는 소켓 버전이 들어간다. 2.2 버전을 활용할건데 WORD 타입으로 들어가게 된다.

WORD는 unsigned short 타입을 typedef 해놓은 것이다. 그런데 2.2 버전은 실수이므로, 

2.2라는 실수를 정수값으로 변환하여 넣어줄 수 있어야 한다. MAKEWORD 매크로를 이용해서 만들어준다.

2번째 인자는 WSADATA 구조체의 포인터타입이 들어간다. 


WSACleanup();

 : 소켓을 활용하는것은 WSAStartup 함수와 WSACleanup 함수 사이에 작성해야 한다. 생성자와 소멸자 같은 개념이다.

WSACleanup 함수는 WSAStartup 을 하면서 지정한 내용을 지워준다. 






# 소켓생성


#

SOCKET hListen; // hListen = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

SOCKET 은 핸들이다. 핸들이란 운영체제가 관리하는 커널오브젝트의 한 종류이다. 

커널오브젝트는 운영체제가 관리하는 커널이라는 특수한 영역에 존재하는 오브젝트이다. 

윈도우를 생성해도 해당 윈도우의 핸들이 생성되고 운영체제가 그 핸들을 이용해서 

어떤 프로그램인지를 구분한다던지 하는 기능을 제공한다.


TCP 소켓은 크게 2가지로 나뉘게 된다. 

첫번째는 다른 컴퓨터로부터 들어오는 접속 승인 요청을 수락해주는 소켓이 존재해야 한다. 

두번째는 다른 컴퓨터와 연결된 소켓이 있어야 한다.


컴퓨터는 IP 주소라는 개념을 이용하여 해당 컴퓨터의 주소정보를 만들어낸다. 

IP 주소는 크게 2가지로 나뉘게 된다. IPV4타입 IPV6타입이다. 

IPV4타입은 32bit 주소 체계로 일반적으로 사용하는 주소이다.

그런데 32bit를 이용하여 표현하는 값의 한계가 있어서 IPV6 타입이 만들어지게 되었다. 

IPV6 타입은 16바이트 주소 체게이다. 천문학적인 값의 표현이 가능하므로 엄청나게 많은 컴퓨터에 IP주소를 할당해 줄 수 있다. 

PF_INET 을 넣어주면 IPV4 타입을 사용한다는 것이다.

SOCK_STREAM 을 넣어주면 연결지향형 소켓을 만들겠다는 의미이다.

세번째 인자는 protocol이 들어간다. protocol은 통신규약을 말한다.

IPPROTO_TCP 는 TCP를 사용하겠다고 지정해주는것이다.






# 소켓의 구성요소를 담을 구조체 생성 및 값 할당

위에 언급한 소켓의 구성요소들을 지정해준다.

주소정보를 만들어낸다. 주소를 구성하는것은 IP와 PORT 2가지로 구성된다. 

IP는 주소이고 PORT는 일종의 구멍을 만들어서 해당 구멍을 이용하여 통신하도록 해준다. 

이런 PORT를 정확하게 구성을 해 주어야 해당포트를 이용하여 

각각의 프로그램중 어느 프로그램인지를 인식하여 통신할 수 있도록 해준다.


#

SOCKADDR_IN tListenAddr = {};

SOCKADDR_IN 구조체


Internet Address Family에서 Windows 소켓에서 소켓을 연결할 로컬 또는 원격 주소를 지정하는 데 사용된다.

즉, 주소정보를 담아두는 구조체이다.


메모리에 데이터를 저장할때 크게 2가지 방식이 존재한다. 빅엔디안, 리틀엔디안 이라는 방식이다.

(자세한 설명은 https://ko.wikipedia.org/wiki/%EC%97%94%EB%94%94%EC%96%B8 참고)

예를 들어 16진수 0x12345678 이 있을 경우 빅엔디안과 리틀엔디안으로 어떻게 메모리 공간에 저장되는지를 비교해보자.

빅엔디안 : 12 34 56 78       //네트워크상 표준 프로토콜

리틀엔디안 : 78 56 34 12    //주로 인텔(Intel)프로세스 계열

서로 다른 방식을 사용하는 컴퓨터 간에 데이터를 주고받을 경우 문제가 발생할 수 있다.

그래서 네트워크 표준은 빅엔디안을 활용한다.


tListenAddr.sin_family = AF_INET;

: 인자들에 대한 설명중 sin_family 는 반드시 AF_INET 이어야 함을 알 수 있다.

#define AF_INET         2               // internetwork: UDP, TCP, etc.

로 기존에 정의되어있다.

tListenAddr.sin_port = htons(PORT);

 : PORT 번호를 설정한다. PORT 번호는 2바이트 안에서 표현할 수 있는 숫자로 정해야 하고 

기본으로 정해진 포트를 제외한 포트번호를 설정해야 한다. 

헤더파일을 include하는 최 상단부 하단에 #define 으로 4578로 설정해주었다.

htons : host to network short 의 약자다. 이 함수를 거치면 무조건 빅엔디안 방식으로 데이터를 변환하여 설정한다.


tListenAddr.sin_addr.s_addr = htonl(INADDR_ANY);

 : 서버는 현재 동작되는 컴퓨터의 IP 주소로 설정해주어야 한다. 우리는 따로 서버를 두지 않고 구현할 것이기 때문.

따라서 INADDR_ANY를 넣어주면 현재 동작되는 컴퓨터의 IP 주소로 설정하게 된다.

s_addr은 IPv4 Internet address를 의미한다.






# 소켓에 위에 설정한 주소정보를 묶어주고, 소켓을 접속대기상태로 만들어줌


#

bind(hListen, (SOCKADDR*)&tListenAddr, sizeof(tListenAddr));

bind(소켓, 소켓 구성요소 구조체의 주소, 그 구조체의 크기);


bind 함수는 소켓에 주소정보를 연결한다.

즉, Listen 소켓의 역할은 접속승인만 해준다. 위에서 설정한 주소 정보를 WinSock2.h 에 정의된 bind 함수를 이용하여 소켓에 묶어준다.

첫번째 인자로는 위에 선언한 소켓을 넣어준다.

두번째 인자로는 bind 될 소켓에 할당할 주소정보를 담고있는 구조체의 주소가 들어간다.(SOCKADDR* 타입 형변환)

세번째 인자로는 두번째 인자로 넣은 구조체의 크기가 들어간다.



listen(hListen, SOMAXCONN);


listen 함수는 연결을 수신하는상태로 소켓의 상태를 변경한다. 즉, 소켓을 접속 대기 상태로 만들어준다. 

SOMAXCONN은 한꺼번에 요청 가능한 최대 접속승인 수를 의미한다.






# 클라이언트 측 소켓 생성 및 정보를 담을 구조체 생성 및 값 할당, 클라이언트가 접속 요청하면 승인해주는 역할


#

SOCKET hClient = accept(hListen, (SOCKADDR*)&tClntAddr, &iClntSize);

accept(소켓, 소켓 구성요소 주소체의 주소, 그 구조체의 크기를 담고있는 변수의 주소);


accept 함수를 이용하여 접속 요청을 수락해준다. 이 함수는 동기화된 방식으로 동작된다.

동기화된 방식이란 요청을 마무리 하기 전까지 계속 대기상태에 놓이게 되는 것을 말한다.

즉 요청이 들어오기 전까지 이 함수는 안빠져나온다.

접속 요청을 승인하면 연결된 소켓이 만들어져서 리턴된다. 이렇게 만들어진 소켓을 이용해서 통신해야 한다.

첫번째 인자로는 소켓을 넣어준다.

두번째 인자로는 accept 할 클라이언트측 주소정보 구조체의 주소가 들어간다.(SOCKADDR* 타입 형변환)

세번째 인자로는 두번째 인자로 넣은 구조체의 크기를 저장해둔 변수의 주소가 들어간다.






# 클라이언트 측으로부터 정보를 받아오고 출력, 클라이언트에 정보 전송 


#

char cBuffer[PACKET_SIZE] = {};

 : 클라이언트측 정보를 수신하기위해 기존에 정의해둔 패킷 크기만큼 버퍼를 생성한다.


recv(hClient, cBuffer, PACKET_SIZE, 0);

recv(소켓, 수신 정보를 담을 배열주소, 그 배열의 크기, flag)


recv 함수는 대상 소켓으로부터 보내온 정보를 받아주는 역할을 한다. 

보내준 데이터가 없다면 여기에서 받을때까지 계속 대기상태에 있게 된다.

flag 값으로는 flag를 활성화시키지 않을것이기에 0을 지정해준다.

그 이후 수신한 정보를 C 문법으로 출력한다.


#

send(hClient, cMsg, strlen(cMsg), 0);


수신과 같은 맥락으로 서버가 메세지를 클라이언트측에 전달한다.


#

closesocket(hClient);

closesocket(hListen);

해당 소켓을 닫아준다. 


여기까지 오면 서버에서 클라이언트측으로 부터 정보를 받고 또 보낼 준비가 완료되었다.

이번엔 클라이언트측 작업을 해보자.






<클라이언트>

# 클라이언트측 코드도 대부분 동일하다. 하지만 서버 IP를 지정해줘야한다.

우리는 이 컴퓨터 내에서 서버와 클라이언트 둘다 돌리기때문에, 해당 컴퓨터의 IP주소를 입력해준다.


# IP주소는 cmd 창에서 ipconfig 입력하여 확인할 수 있다.






# 클라이언트측 코드, 소켓 구성요소 구조체에 접속할 서버의 ip를 적어준다. 클라이언트에서는 bind함수 대신 connect함수를 사용한다.


#

connect(hSocket, (SOCKADDR*)&tAddr, sizeof(tAddr));

connect(소켓, 소켓 구성요소 구조체의 주소, 그 구조체의 크기);


connect 함수는 지정된 소켓에 연결을 설정해준다. 

서버에 연결하기위해 connect 함수를 사용한다.


#

클라이언트 측에서는 생성한 소켓이 하나뿐이니 하나만 닫아주면 된다.






준비가 다 되었으면 둘다 실행시켜보자.


# 성공적으로 서버측과 클라이언트측이 정보를 주고받았다.


실행시 당연하게 서버측부터 실행시키고 클라이언트측을 실행시켜야한다.


만약 본인이 사용하는 visual studio가 2017버전이라면 SDL검사를 프로젝트 속성창에서 끄고 진행해야한다.

그 이하버전에서는 프로젝트 생성시 끄고 생성하길 권장한다.

'BASIC LANGUAGE > C++' 카테고리의 다른 글

*) DLL 추출 및 사용  (0) 2018.05.16
*) 상속, 다형성, 가상함수  (0) 2018.03.12
*) C++를 이용한 TCP 소켓통신 구현  (12) 2018.02.27
댓글
댓글쓰기 폼
공지사항
Total
290,633
Today
25
Yesterday
32
링크
TAG
more
«   2022/08   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
글 보관함