Coding & Programming/Network(TCP, IP, UDP)

[C/C++] 윈도우 소켓(Winsock) 1:N 채팅/통신 프로그램 구현하기 #3:: TCP 서버 구현하기(소스코드)

mainCodes 2021. 4. 18. 08:49

안녕하세요 JollyTree입니다 (•̀ᴗ•́)و

 

이번에는 채팅 서버 기능을 구현해 보았습니다. 1:N 서버는 기본적으로 N개의 클라이언트(사용자)의 접속이 필요하므로  N개의 소켓을 관리하고 각 클라이언트들로부터 보내진 메시지를 다수의 클라이언트로 재전송하는 기능을 포함합니다.

서버 주요 기능

  - 소켓 초기화 및 클라이언트 접속 대기
  - 소켓으로부터 클라이언트 접속, 채팅 메시지와 서비스 종료 메시지를 수신하기 위한 FD_ACCEPT, FD_READ, 
    FD_CLOSE 이벤트를 등록

  - 소켓을 이용한 채팅 메시지 수신과 전달
  - 클라이언트 연결/접속 종료 상태와 공지사항 전달

   
다수의 클라이언트들은 소켓, 이벤트, 별명, IP주소 정보가 포함된 SOCK_INFO 구조체를 통해 관리되며 SOCK_INFO 변수는 최대 클라이언트 개수에 +1을 더한 수만큼으로 배열 변수로 선언되어 있습니다.

typedef struct sock_info
{
    SOCKET s;
    HANDLE ev;
    char nick[50];
    char ipaddr[50];
}SOCK_INFO;

...
SOCK_INFO sock_array[client_count + 1];
...

 
main() 함수는 클라이언트와 유사한 구조를 가지고 있습니다. 서버 프로그램도 명령행 인자(argv [1])로부터 포트 정보를 입력받습니다. 포트 정보 입력은 필수가 아니므로 입력이 없으면 디폴트 포트인 9999 포트로 소켓을 생성합니다.

서버는 2개의 쓰레드를 생성하여 병렬로 기능을 수행합니다. do_chat_service() 함수는 서버 소켓을 초기화하고 지속적으로 클라이언트들로부터 수시 할 메시지가 있는지 이벤트를 모니터 합니다. 서버가 모니터 하는 소켓 이벤트는 클라이언트 접속(FD_ACCEPT),  메시지 수신(FD_READ), 클라이언트 접속 종료(FD_CLOSE) 이벤트입니다.

index = WSAWaitForMultipleEvents(total_socket_count, 
    handle_array, false, INFINITE, false);

WSAEnumNetworkEvents(sock_array[index].s, sock_array[index].ev, &ev);
if (ev.lNetworkEvents == FD_ACCEPT)
    add_client(index);
else if (ev.lNetworkEvents == FD_READ)
    read_client(index);
else if (ev.lNetworkEvents == FD_CLOSE)
    remove_client(index);

add_client() 함수는 accept() 함수를 실행한 후 소켓, 이벤트, IP 주소를 SOCK_INFO sock_array에 추가함과 동시에 accept() 함수의 반환 값인 accept_sock 소켓에 클라이언트로부터 소켓 이벤트를 모니터 하기 위해 FD_READ,  FD_CLOSE 이벤트를 등록합니다.

read_client() 함수는 쓰레드(recv_and_forward())를 추가적으로 생성하여 클라이언트가 송신한 메시지를 수신하고 수신한 메시지를 각 클라이언트들에게 재전송합니다.

remove_client() 함수는 FD_CLOSE 이벤트가 감지된 클라이언트 정보를 SOCK_INFO sock_array로부터 삭제하고 모든 클라이언트들에 삭제된 클라이언트 정보를 전송합니다.

여기까지가 서버 코드에 대한 설명으로 다음은 채팅 서버 전체 소스입니다.

채팅 서버 전체 소스코드:

 

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
 
#include <stdio.h>
#include <WinSock2.h>
#include <process.h>
#include <string.h>
 
int server_init();
int server_close();
unsigned int WINAPI do_chat_service(void* param);
unsigned int WINAPI recv_and_forward(void* param);
int add_client(int index);
int read_client(int index);
void remove_client(int index);
int notify_client(char *message);
char* get_client_ip(int index);
 
typedef struct sock_info
{
    SOCKET s;
    HANDLE ev;
    char nick[50];
    char ipaddr[50];
}SOCK_INFO;
 
int         port_number = 9999;
const   int client_count = 10;
SOCK_INFO   sock_array[client_count + 1];
int         total_socket_count = 0;
 
int main(int argc, char* argv[])
{
    unsigned int tid;
    char message[MAXBYTE] = "";
    HANDLE mainthread;
 
    printf("\n사용법 : mcodes_server [포트번호]\n");
    printf("         ex) mcodes_server.exe 9999\n");
    printf("         ex) mcodes_server.exe \n\n");
 
    if(argv[1!= NULL)
        port_number = atoi(argv[1]);
 
    mainthread = (HANDLE)_beginthreadex(NULL0, do_chat_service, (void*)00&tid);
    if (mainthread)
    {
        while (1)
        {
            gets_s(message, MAXBYTE);
            if (strcmp(message, "/x"== 0)
                break;
 
            notify_client(message);
        }
        server_close();
        WSACleanup();
        CloseHandle(mainthread);
    }
 
    return 0;
}
 
int server_init()
{
    WSADATA wsadata;
    SOCKET s;
    SOCKADDR_IN server_address;
 
    memset(&sock_array, 0sizeof(sock_array));
    total_socket_count = 0;
    if (WSAStartup(MAKEWORD(22), &wsadata) != 0)
    {
        puts("WSAStartup 에러.");
        return -1;
    }
 
    if ((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    {
        puts("socket 에러.");
        return -1;
    }
 
    memset(&server_address, 0sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(port_number);
 
    if (bind(s, (struct sockaddr*)&server_address, sizeof(server_address)) < 0)
    {
        puts("bind 에러");
        return -2;
    }
 
    if (listen(s, SOMAXCONN) < 0
    {
        puts("listen 에러");
        return -3;
    }
 
    return s;
}
 
int server_close()
{
    for (int i = 1; i < total_socket_count; i++)
    {
        closesocket(sock_array[i].s);
        WSACloseEvent(sock_array[i].ev);
    }
 
    return 0;
}
 
unsigned int WINAPI do_chat_service(void* param)
 
{
    SOCKET  server_socket;
    WSANETWORKEVENTS ev;
    int index;
    WSAEVENT handle_array[client_count + 1];
 
    server_socket = server_init();
    if (server_socket < 0)
    {
        printf("초기화 에러\n");
        exit(0);
    }
    else
    {
        printf("\n >> 서버 초기화가 완료되었습니다.(포트번호:%d)\n", port_number);
 
        HANDLE event = WSACreateEvent();
        sock_array[total_socket_count].ev = event;
        sock_array[total_socket_count].s = server_socket;
        strcpy_s(sock_array[total_socket_count].nick, "svr");
        strcpy_s(sock_array[total_socket_count].ipaddr, "0.0.0.0");
 
        WSAEventSelect(server_socket, event, FD_ACCEPT);
        total_socket_count++;
 
        while (true)
        {
            memset(&handle_array, 0sizeof(handle_array));
            for (int i = 0; i < total_socket_count; i++)
                handle_array[i] = sock_array[i].ev;
 
            index = WSAWaitForMultipleEvents(total_socket_count, 
                handle_array, false, INFINITE, false);
            if ((index != WSA_WAIT_FAILED) && (index != WSA_WAIT_TIMEOUT)) 
            {
                WSAEnumNetworkEvents(sock_array[index].s, sock_array[index].ev, &ev);
                if (ev.lNetworkEvents == FD_ACCEPT)
                    add_client(index);
                else if (ev.lNetworkEvents == FD_READ)
                    read_client(index);
                else if (ev.lNetworkEvents == FD_CLOSE)
                    remove_client(index);
            }
        }
        closesocket(server_socket);
    }
 
    WSACleanup();
    _endthreadex(0);
 
    return 0;
 
}
 
int add_client(int index)
{
    SOCKADDR_IN addr;
    int len = 0;
    SOCKET accept_sock;
        
    if (total_socket_count == FD_SETSIZE)
        return 1;
    else { 
 
        len = sizeof(addr);
        memset(&addr, 0sizeof(addr));
        accept_sock = accept(sock_array[0].s, (SOCKADDR*)&addr, &len);
 
        HANDLE event = WSACreateEvent();
        sock_array[total_socket_count].ev = event;
        sock_array[total_socket_count].s = accept_sock;
        strcpy_s(sock_array[total_socket_count].ipaddr, inet_ntoa(addr.sin_addr));
 
        WSAEventSelect(accept_sock, event, FD_READ | FD_CLOSE);
 
        total_socket_count++;
        printf(" >> 신규 클라이언트 접속(IP : %s)\n", inet_ntoa(addr.sin_addr));
 
        char msg[256];
        sprintf_s(msg, " >> 신규 클라이언트 접속(IP : %s)\n", inet_ntoa(addr.sin_addr));
        notify_client(msg);
    }
 
    return 0;
}
 
int read_client(int index)
{
    unsigned int tid;
    HANDLE mainthread = (HANDLE)_beginthreadex(NULL0, recv_and_forward, (void*)index, 0&tid);
    WaitForSingleObject(mainthread, INFINITE);
 
    CloseHandle(mainthread);
 
    return 0;
}
 
unsigned int WINAPI recv_and_forward(void* param)
{
    int index = (int)param;
    char message[MAXBYTE], share_message[MAXBYTE];
    SOCKADDR_IN client_address;
    int recv_len = 0,  addr_len  = 0;
    char* token1 = NULL;
    char* next_token = NULL;
 
    memset(&client_address, 0sizeof(client_address));
 
    if ((recv_len = recv(sock_array[index].s, message, MAXBYTE, 0)) > 0)
    {
        addr_len = sizeof(client_address);
        getpeername(sock_array[index].s, (SOCKADDR*)&client_address, &addr_len);
        strcpy_s(share_message, message);
 
        if (strlen(sock_array[index].nick) <= 0)
        {
            token1 = strtok_s(message, "]"&next_token);
            strcpy_s(sock_array[index].nick, token1 + 1);
        }
        for (int i = 1; i < total_socket_count; i++)
            send(sock_array[i].s, share_message, MAXBYTE, 0);
    }
 
    _endthreadex(0);
    return 0;
}
 
void remove_client(int index)
{
    char remove_ip[256];
    char message[MAXBYTE];
 
    strcpy_s(remove_ip, get_client_ip(index));
    printf(" >> 클라이언트 접속 종료(Index: %d, IP: %s, 별명: %s)\n", index, remove_ip, sock_array[index].nick);
    sprintf_s(message, " >> 클라이언트 접속 종료(IP: %s, 별명: %s)\n", remove_ip, sock_array[index].nick);
 
    closesocket(sock_array[index].s);
    WSACloseEvent(sock_array[index].ev);
 
    total_socket_count--;
    sock_array[index].s = sock_array[total_socket_count].s;
    sock_array[index].ev = sock_array[total_socket_count].ev;
    strcpy_s(sock_array[index].ipaddr, sock_array[total_socket_count].ipaddr);
    strcpy_s(sock_array[index].nick, sock_array[total_socket_count].nick);
 
    notify_client(message);
}
 
char* get_client_ip(int index)
{
    static char ipaddress[256];
    int    addr_len;
    struct sockaddr_in    sock;
 
    addr_len = sizeof(sock);
    if (getpeername(sock_array[index].s, (struct sockaddr*)& sock, &addr_len) < 0)
        return NULL;
 
    strcpy_s(ipaddress, inet_ntoa(sock.sin_addr));
    return ipaddress;
 }
 
 
int notify_client(char* message)
{
    for (int i = 1; i < total_socket_count; i++)
        send(sock_array[i].s, message, MAXBYTE, 0);
 
    return 0;
}
cs


채팅 서버와 클라이언트 실행결과:

 

채팅 서버실행 화면
채팅 클라이언트 #1 실행화면
채팅 클라이언트 #2 실행화면
채팅 클라이언트 #3 실행화면


읽어 주셔서 감사드리며 이상 JollyTree였습니다 (•̀ᴗ•́)و