kakasoo

[TCP/IP] iteractive 기반의 서버, 클라이언트 구현 (code) 본문

프로그래밍/네트워크

[TCP/IP] iteractive 기반의 서버, 클라이언트 구현 (code)

카카수(kakasoo) 2020. 7. 17. 14:58
반응형

클라이언트에서 입력한 것을, 서버가 받아서, 동일한 값을 되돌려 주는 에코 서버, 클라이언트이다.

자세한 내용은 모두 주석으로 처리하였으니, 읽고 기억을 되새겨보도록 하자.

echo_server.c

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
60
61
62
63
64
65
66
67
68
69
70
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
 
#define BUF_SIZE 1024
 
void ErrorHandling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
 
int main(int argc, char* argv[]) {
    WSADATA wsaData;
    SOCKET hServSock, hClntSock; // 호스트 서버 소켓, 호스트 클라이언트 소켓
    char message[BUF_SIZE]; // 이번에는 "hello world" 가 아니라 직접 입력을 받는다.
    int strLen;
 
    SOCKADDR_IN servAdr, clntAdr; // 서버 주소를 담을 변수 두 개를 선언하였다.
    int clntAdrSize = sizeof(clntAdr);
 
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]); // PORT 정보가 없을 시에 종료시킨다.
        exit(1);
    }
 
    if(WSAStartup(MAKEWORD(2,2),&wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
 
    hServSock = socket(PF_INET, SOCK_STREAM, 0); // IPv4, 연결지향형 socket 생성
    if (hServSock == INVALID_SOCKET) // 유효하지 않은 소켓이라면 (-1 값이지만 매크로 상수로 하는 것을 권장.)
        ErrorHandling("Socket() error!");
    
    /* 여기는 서버 소켓의 정보를 구성하는 부분이다 */
    {
        memset(&servAdr, 0sizeof(servAdr)); // servAdr를 모두 0으로 초기화한다.
        servAdr.sin_family = AF_INET;
        // AF_INET과 PF_INET은 둘 다 2의 값을 가지는 상수이지만, 주소일 때는 AF, 프로토콜일 때는 PF로 나타낸다.
        servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
        // INADDR_ANY는 서버의 IP 주소를 자동으로 할당하는 것이기 때문에, 직접 입력할 필요를 줄인다.
        servAdr.sin_port = htons(atoi(argv[1])); // 입력받은 PORT 값을 서버 측 PORT로 지정한다, PORT는 데이터 송수신 창구.
    }
    if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR) // 이렇게 만든 정보를 bind 한다.
        ErrorHandling("bind() error!");
    
    if (listen(hServSock, 5== SOCKET_ERROR)
        ErrorHandling("listen() error!");
 
    for (int i = 0; i < 5; i++) {
        hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize); // clntAdrSize는 값을 반환받기 받는다.
        if (hClntSock == -1)
            ErrorHandling("accept() Error!");
        else
            printf("Connected client %d \n", i + 1);
 
        while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0
            // 수신대상 hClntSock에 메세지, 수신 가능한 길이를 매개변수로 준다.
            // 성공 시 strLen은 수신한 바이트 수가 저장된다.
// echo 하기 위해서 만들어진 반복문.

            send(hClntSock, message, strLen, 0);
            // while() 내부의 send() 함수는 hClntSock으로 message의 strLen만큼의 길이를 전달하고, 전송한다.
        
        closesocket(hClntSock); // 전송 후에 종료한다.
    }
 
    closesocket(hServSock); // 모든 클라이언트의 전송이 종료되면 서버 소켓도 종료시킨다.
    WSACleanup(); // 소켓 관련 함수들을 반환
    return 0;
}
cs

 

echo_client.c

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
 
#define BUF_SIZE 1024
 
void ErrorHandling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
 
int main(int argc, char* argv[]) {
    WSADATA wsaData;
    SOCKET hSocket; // 호스트 클라이언트 소켓
    char message[BUF_SIZE]; // 이번에는 "hello world" 가 아니라 직접 입력을 받는다.
    int strLen;
 
    SOCKADDR_IN servAdr; // 서버 주소를 담을 변수를 선언하였다 (클라이언트 소켓의 주소는 필요없다.)
 
    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]); // IP나 PORT 정보가 없을 시에 종료시킨다.
        exit(1);
        // 여담인데, connect() 함수는 자동으로 IP와 PORT를 할당하기 때문에 클라이언트는 bind()가 필요없다.
        // 명시적으로 할 필요가 없다는 것이지, 쓰임이 없다는 것은 아니다.
    }
 
    if (WSAStartup(MAKEWORD(22), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
 
    hSocket = socket(PF_INET, SOCK_STREAM, 0); // IPv4, 연결지향형 socket 생성
    if (hSocket == INVALID_SOCKET) // 유효하지 않은 소켓이라면 (-1 값이지만 매크로 상수로 하는 것을 권장.)
        ErrorHandling("Socket() error!");
 
    /* 여기는 서버 소켓의 정보를 구성하는 부분이다 */
    {
        memset(&servAdr, 0sizeof(servAdr)); // servAdr를 모두 0으로 초기화한다.
        servAdr.sin_family = AF_INET;
        // AF_INET과 PF_INET은 둘 다 2의 값을 가지는 상수이지만, 주소일 때는 AF, 프로토콜일 때는 PF로 나타낸다.
        servAdr.sin_addr.s_addr = inet_addr(argv[1]); // 서버와 클라이언트의 IP가 같은 경우는 거의 없을 것.
        // INADDR_ANY는 서버의 IP 주소를 자동으로 할당하는 것이기 때문에, 직접 입력할 필요를 줄인다.
        servAdr.sin_port = htons(atoi(argv[2]));
        // 입력받은 PORT 값을 서버 측 PORT로 지정한다, PORT는 데이터 송수신 창구.
        // 서버 측과 바뀐 것은, argv[1]이 argv[2]로 변경되었다는 것 하나 뿐이다.
    }
//    if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR) // 이렇게 만든 정보를 bind() 한다.
//        ErrorHandling("bind() error!");
//  위에서 말했다시피, bind()는 클라이언트 측에는 필요가 없다, connect() 할 때 자동생성되기 때문이다.
 
    if (connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
        ErrorHandling("connect() error!");
    else
        puts("Connected!");
 
    while (1) {
        fputs("Input message (Q to quit) : ", stdout); // stderr는 디버깅, stdout은 출력과 관련된 표준 스트림이다.
        fgets(message, BUF_SIZE, stdin); // 입력 받은 data를 message에 저장.
        
        if (!strcmp(message, "q\n"|| !strcmp(message, "Q\n"))
            break// q or Q를 입력한 경우에는 전송 종료.
        
        send(hSocket, message, strlen(message), 0); // 그 외인 경우에는 서버 측으로 전송
        strLen = recv(hSocket, message, BUF_SIZE - 10); // 서버 측으로부터 에코된 게 있는지 확인 (문자열 길이를 반환)
 
        message[strLen] = 0// 마지막 값은 0으로 지정 (문자열의 마지막)
        printf("message from server : %s", message); // 정상적이라면 message에 client에서 입력한 것이 동일하게 돌아와야 한다.
    }
 
    closesocket(hSocket); // 모든 클라이언트의 전송이 종료되면 서버 소켓도 종료시킨다.
    WSACleanup(); // 소켓 관련 함수들을 반환
    return 0;
}
cs

 

이 코드에는 문제점이 있다.

]client에서 받아들이는 문자열의 크기가 1024를 초과하는 경우, 즉 BUF_SIZE를 초과하는 경우이다.

초과할 경우에는 운영체제가 알아서 정보를 잘게 잘라서 전송해주겠지만, 이는 운영체제의 이야기일 뿐이고,

코드에서는 while 문 내에서, 이미 정보가 돌아왔음으로 판단하고 재전송하는 단계로 넘어갈 수가 있다.

다음에는 이를 해결해보자.

 

여담이지만, Q를 누르면 코드가 종료되는 것은, 어플리케이션 프로토콜에 해당한다.

반응형