본문 바로가기

소켓 프로그래밍/서버와 클라이언트

1) Hello World 서버

네트워크 프로그래밍이란 "멀리 떨어져 있는 호스트들이 서로 데이터를 주고받을 수 있도록 프로그램을 구현하는 것이다. 소프트웨어 차원에서 호스트들 간에 연결을 위한 장치가 필요한데 이러한 기능을 해주는 장치가 소켓(socket)이다. 그래서 일반적으로 네트워크 프로그래밍과 소켓 프로그래밍은 같은 의미로 사용된다.

소켓은 다소 추상적인 개념으로 사용되는 단어이다. 하지만 전화기를 빗대어 생각하자. 전화기도 멀리 떨어져 있는 두 사람이 통신을 하기 위한 도구이다. 소켓도 소프트웨어적으로 멀리 떨어져 있는 두 개의 호스트(host)를 연결해주는 매개체이다. 실생활에서 전화기가 필요하듯이 네트워크 프로그래밍에서도 소켓이 필요하다.

이제부터 간단하게 "Hello World!"를 출력하는 프로그램을 만들 것이다. 서버를 실행시킨 후 클라이언트를 실행시켜서 서버에게서 받은 "Hello World!" 메시지를 클라이언트에 출력한 후 종료되는 프로그램을 만들어보자.

서버 프로그램의 구현 과정

먼저 전화기를 장만하여야 한다. 아래의 함수를 이용하여 전화기를 한 대 장만할 수 있다.

1) 다음은 소켓을 생성하는 함수의 선언이다.
socket(_In_ int af, _In_ int type, _In_ int protocol);
1인자 : 생성할 소켓이 통신을 하기 위해 사용할 프로토콜 체계(Protocol Family)를 설정
2인자 : 소켓을 전송함에 있어서 사용하게 되는 전송 타입을 설정
3인자 : 두 호스트들간의  통신을 하는데 있어서 특정 프로토콜을 지정하기 위함

걸려오는 전화를 받으려면 전화기를 장만한 후에 전화번호를 할당받는 것이다. 소켓도 이와 마찬가지이다. 전화기에 전화번호를 할당받듯이 소켓도 IP주소를 할당받아야 한다.

2) 다음은 소켓에 IP주소를 할당받는 함수의 선언이다.
bind(_In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen);
1인자 : 서버 소켓
2인자 : IP체계 및 IP, port번호가 저장되어있는 SOCKADDR 구조체의 주소
3인자 : 2번째 인자의 크기

이제 전화기도 한 대가 준비되었고 전화번호도 할당을 받았다. 하지만 전화기가 케이블에 연결되어 있지 않는다면 전화를 받을 수 없다. 전화기를 케이블에 연결하는 과정이 필요하다. 소켓도 연결 요청이 가능한 상태로 바꾸어 주어야 한다.

3) 소켓에 연결 요청이 가능한 상태로 만드는 함수의 선언이다.
listen(_In_ SOCKET s, _In_ int backlog);
1인자 : 서버 소켓
2인자 : 클라이언트 연결 대기열의 크기이다. 여러 클라이언트가 한 번에 들어올 수 있다. 보통 5로 설정한다고 하지만, 윈속2부터 SOMAXCONN이라는 상수값을 지정할 수 있다.

위의 과정이 끝나면 전화벨이 울린 후 수화기를 들어 올리면 대화를 시작할 수 있다. 이때 수화기를 들어 올린다는 것은 전화를 하기 위해 누군가 전화 요청을 한 것이고 이를 수락하는 것이다. 

4) 연결 요청을 수락하는 함수의 선언이다.
accept(_In_ SOCKET s, _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, _Inout_opt_ int FAR * addrlen);
1인자 : 서버 소켓
2인자 : sockaddr 구조체에 대한 포인터이다. 연결이 성공되면 이 구조체 값이 채워지며 우리는 이구조체의 정보를 이용해서 연결된 클라이언트의 인터넷 정보를 알아낼수 있다.
3인자 : 2번째 인자의 크기

모든 과정을 살펴보았다. 모든 과정을 정리해보면 "전화기를 구입한 후 전화번호를 할당받고 전화기를 케이블에 연결한 후에 전화가 오기를 기다리고 수화기를 받는다" 이 순서대로 정의를 할 수 있다. 이 순서를 항상 기억해두자.

위의 과정을 코드로 작성해보자.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")
 
using namespace std;
 
int main()
{
    // 윈속 초기화
    WSADATA wsaData = {};
    if (WSAStartup(MAKEWORD(22), &wsaData) != 0)
    {
        cout << "WSAStartup() Error" << endl;
        return -1;
    }
 
    // 서버 소켓 생성
    SOCKET servSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (servSocket == INVALID_SOCKET)
    {
        cout << "socket() Error" << endl;
        return -1;
    }
 
    // 소켓에 주소 할당
    SOCKADDR_IN sockAddr = {};
    sockAddr.sin_family = AF_INET;
    sockAddr.sin_port = htons(4000);
    sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(servSocket, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR_IN)) == -1)
    {
        cout << "bind() Error" << endl;
        return -1;
    }
 
    // 연결 요청 대기 상태
    listen(servSocket, SOMAXCONN);
 
    // 연결 요청 수락
    SOCKADDR_IN tClntAddr = {};
    int size = sizeof(SOCKADDR_IN);
    SOCKET clntSocket = accept(servSocket, (SOCKADDR*)&tClntAddr, &size);
    if (clntSocket == SOCKET_ERROR)
    {
        closesocket(servSocket);
        WSACleanup();
    }
 
    // 데이터 전송
    char message[] = "Hello World! \n";
    send(clntSocket, message, sizeof(message), 0);
 
    // 연결 종료
    closesocket(clntSocket);
    closesocket(servSocket);
 
    // 윈도우 소켓 해제
    WSACleanup();
}
cs

위의 코드로 작성된 서버 프로그램을 디버깅 모드로 실행시키면 accept() 함수에서 블로킹이 되는데 블로킹이란 요청이 올 때까지 대기 중인 상태를 말한다. 클라이언트의 connect() 함수로 연결 요청이 성공적으로 될 경우 0보다 큰 값을 반환하여 블로킹을 빠져나온다. 클라이언트가 접속되면 send() 함수를 통해서 "Hello World!"라는 메시지를 전송한 후 종료가 된다.

클라이언트 프로그램의 구현 과정

클라이언트 프로그램은 서버 프로그램보다 훨씬 간단하다. 일반적으로 서버는 걸려오는 전화를 받는 역할이며 클라이언트는 전화를 거는 역할이다. 클라이언트는 먼저 전화기를 한 대 장만하여 전화번호를 눌러서 상대방에게 전화를 걸기만 하면 된다.

1) 소켓을 생성하는 함수의 선언이다.
socket(_In_ int af, _In_ int type, _In_ int protocol);

2) 서버에게 연결 요청하는 함수의 선언이다.
connect(_In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen);

이제 접속 요청을 위한 클라이언트 코드를 보자.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <Winsock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
 
using namespace std;
 
int main()
{
    // 윈속 초기화
    WSADATA wsaData = {};
    if (WSAStartup(MAKEWORD(22), &wsaData) != 0)
    {
        cout << "WSAStartup() Error" << endl;
        return -1;
    }
 
    // 서버로의 접속을 위한 소켓 생성
    SOCKET clntSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clntSocket == INVALID_SOCKET)
    {
        cout << "socket() Error" << endl;
        return -1;
    }
 
    // 서버로의 연결 요청
    SOCKADDR_IN sockAddr = {};
    sockAddr.sin_family = AF_INET;
    sockAddr.sin_port = htons(4000);
    inet_pton(AF_INET, "127.0.0.1"&sockAddr.sin_addr);
    if (connect(clntSocket, (SOCKADDR*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR)
    {
        cout << "connect() Error" << endl;
        return -1;
    }
 
    // 데이터 수신
    char message[30= {};
    recv(clntSocket, message, sizeof(message), 0);
    cout << "Message from server : " << message << endl;
 
    // 연결 종료
    closesocket(clntSocket);
 
    // 윈도우 소켓 해제
    WSACleanup();
}
cs

30번째 줄에 보면 접속하고자하는 서버의 IP와 port의 정보를 설정한 후 connect() 함수를 통하여 서버에게 접속 요청을 한다. 이때 서버 프로그램에서는 accept() 함수에서 블로킹을 빠져나오게 되며 접속한 클라이언트에게 "Hello World!"라는 메시지를 보내어 준다. 클라이언트는 recv() 함수를 통하여 서버에서 보내준 메시지를 출력한 후 종료가 된다.

 

클라이언트 프로그램 결과 이미지
클라, 서버의 흐름도

위의 내용만 가지고는 이해하기 어려운 부분들이 많을 것이다. 필자도 이해가 안된 상태로 코드를 한줄한줄 따라치며 실행을 해보며 함수 하나 하나의 내용을 익혔다. 아래의 세부적인 포스팅을 작성하였기 때문에 참고하면 좋을 것 같은 포스트들을 링크에 달아두었다.

들어가기에 앞서 참고하면 좋을 포스팅
* 소켓의 생성 : https://developer-jun.tistory.com/12 
* 주소 체계와 데이터 설정 : https://developer-jun.tistory.com/13

'소켓 프로그래밍 > 서버와 클라이언트' 카테고리의 다른 글

* Iterative 서버  (0) 2020.07.14
* TCP / IP 4계층  (0) 2020.07.13
* 바이트 순서  (0) 2020.07.09
* 주소 체계와 데이터 설정  (0) 2020.07.08
* 소켓의 생성과 프로토콜  (0) 2020.07.08