winsock2 라이브러리를 이용한 채팅 구현
1. Winsock2이란?
- Windows Sockets API(WSA)
- Windows 운영 체제에서 네트워크 프로그래밍을 수행하는데 사용되는 API
- 인터넷 네트워크 및 소켓 관련 함수들 제공
- 버전1과 버전2가 있음. 보통 2를 사용함
- 레퍼런스 문서
- 헤더 및 함수 정보
2. Winsock2 장점 및 특징
- 윈도우 환경에서 효율적으로 자원을 사용할 수 있음
- 다양한 네트워크 프로토콜, 소켓 유형, 주소 체계를 지원
3. Winsock2의 구성 및 사용법
- 전체 코드는 최하단에 있음 -C winsock2를 이용한 소켓 통신 구현를 참조하여 작성
서버측 코드 리뷰
1
2
3
4
5
6
7
8
9
10
서버측의 로직
1. Winsock를 사용할 수 있도록 초기화(Initialize)
2. 리스닝 소켓을 생성(Create)
3. IP 주소와 PORT 번호 같은 정보를 소켓에 묶어(Bind)준다
4. 서버가 클라이언트의 요청을 받을 수 있는 상태(Listen)가 되도록한다
5. 클라이언트의 요청이 들어오면 받는다(Accept)
1. 1개의 클라이언트만 받는다면, listening 중인 서버측 소켓을 닫는다(Close)
6. send/recv와 같은 함수를 통해 클라이언트와 데이터를 주고 받는다
7. 통신이 종료된다면, 클라이언트와 연결된 소켓을 닫는다(Close)
8. Winsock의 사용으로 종료한다(Clean Up, terminate)
1
2
3
4
5
6
7
8
#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리
#include <iostream>
#include <winsock2.h>
#include <thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크
ws2_32.lib
라이브러리에 대한 종속성 설정 필요_inet_ntoa()_
함수 사용 시, Deprecated된 함수이므로 에러 발생할 수 있음#define _WINSOCK_DEPRECATED_NO_WARNINGS
으로 경고 제거
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
WSADATA wsa; // Windows 소켓 구현에 대한 구조체 생성
// WSAStartup을 통해 wsadata 초기화
// MAKEWORD(2,2) 매개 변수는 시스템에서 Winsock 버전 2.2를 요청
// WSAStartup은 성공시 0, 실패시 SOCKET_ERROR를 리턴하므로
// 리턴값이 0인지 검사하여 에러 여부 확인
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
std::cout << "WSAStartup failed" << endl;
return 1;
}
……
WSACleanup();
return 0;
}
WSDATA
: 라이브러리를 초기화하고 네트워크 통신을 설정하기 위한 구조체WSAStartup
: Winsock2 라이브러리 초기화 함수.- 라이브러리 사용 전 반드시 호출해야한다고 함.
- 다양한 초기화 매개변수와 WSADATA 구조체를 전달하여, Winsock2를 초기화 하고, 초기화 정보를 WSADATA 구조체에 저장함
1
2
3
4
5
6
7
8
// 서버가 클라이언트 연결을 수신 대기할 수 있도록 소켓 생성
server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 주소 패밀리, IP 주소 및 포트 번호에 대한 정보를 보유할 sockaddr 구조체 생성
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET; // IPv4 기반의 TCP, UDP 프로토콜 사용
addr.sin_port = htons(4444);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
- server_socket을 socket 함수의 반환값으로 초기화
- PF_INET, SOCK_STREAM_ IPPROTO_TCP가 순서대로 들어감
- 순서대로, 네트워크 주소 체계로 IPv4를 사용
- 소켓 타입으로 Stream을 사용(TCP 사용 방식)
- TCP 프로토콜을 사용하는 것을 의미함
- PF_INET, SOCK_STREAM_ IPPROTO_TCP가 순서대로 들어감
- sockaddr_in을 통해 인터넷 프로콜을 사용하는 ‘소켓 주소 객체’
addr
생성- 해당 객체에 대해 IPv4, Port 번호 ‘4444’를 넣어줌
htons()
함수를 사용하여 호스트 바이트 순서에서 네트워크 바이트 순서로 변환함- 엔디안 시스템 간의 호환성 유지를 위해 필요하다고 함
- IPv4 주소는 INADDR_ANY를 사용하였는데, 사용할 수 있는 랜카드의 IP 주소 중, 현재 사용 가능한 IP 주소를 선택한다
1
2
3
4
5
6
7
8
9
10
// bind 함수를 통해 생성된 소켓 및 sockaddr 구조를 매개 변수로 전달
bind(server_socket, (SOCKADDR*)&addr, sizeof(addr));
listen(server_socket, SOMAXCONN); // 소켓에서 수신 대기
// SOMAXCONN은 Winsock 공급자에게 큐에 있는 최대 적정 수의 보류 중인 연결을 허용하도록 지시하는 특수 상수
SOCKADDR_IN client = {};
int client_size = sizeof(client);
// 클라이언트에서 연결을 수락하기 위해 ClientSocket이라는 임시 소켓 개체 생성
client_socket = accept(server_socket, (SOCKADDR*)&client, &client_size);
bind
함수를 이용해 서버 소켓에 필요한 정보를 할당함- 이후
listen
함수를 이용해, 서버 소켓이 클라이언트로 접속 요청이 들어오는 것을 대기함 - 클라이언트의 연결을 수락하기 위해
client_socket
이라는 임시 소켓 개체 생성- 클라이언트 주소 정보를 담을 client_addr 객체를 생성하여, 그 크기를 accept 함수로 넘기게 되면, 클라이언트와 연결을 진행함
1
2
3
4
5
if (!WSAGetLastError()) {
std::cout << "연결 완료" << endl;
std::cout << "Client IP: " << inet_ntoa(client.sin_addr) << endl;
std::cout << "Port: " << ntohs(client.sin_port) << endl;
}
- 연결이 완료된 경우, 클라이언트의 IP와 Port 번호를 출력
1
2
3
4
5
6
char msg[PACKET_SIZE] = { 0 };
while (!WSAGetLastError()) {
ZeroMemory(&msg, PACKET_SIZE);
cin >> msg;
send(client_socket, msg, strlen(msg), 0);
}
- 서버가 클라이언트에게 메세지를 보내기 위해, 메세지를 입력받을 버퍼인
msg[]
생성하고, 입력 받아- 서버측에서 client_socket을 향해
send()
함수를 사용하여 메시지를 전송함
- 서버측에서 client_socket을 향해
ZeroMemory()
는 메모리를 0으로 채울때 쓰는 함수.cstring.h
의memset
함수와 유사함
1
2
3
4
5
6
int WSAAPI send(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags
);
send()
함수 구성- 매개 변수로 (연결된 소켓, 전송할 데이터를 포함하는 버퍼, 버퍼에 대한 데이터의 길이, 특수 옵션값인 플래그)를 받음
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
void proc_recvs() {
char buffer[PACKET_SIZE] = { 0 };
while (!WSAGetLastError()) {
ZeroMemory(&buffer, PACKET_SIZE); // 버퍼 초기화
recv(client_socket, buffer, PACKET_SIZE, 0); // winsock2에 정의된 recv함수
// client_socekt으로부터, 버퍼에 패킷 사이즈만큼 받아 저장한다
cout << "받은 메세지: " << buffer << endl;
}
}
int main() {
……
thread proc2(proc_recvs);
char msg[PACKET_SIZE] = { 0 };
while (!WSAGetLastError()) {
ZeroMemory(&msg, PACKET_SIZE);
cin >> msg;
send(client_socket, msg, strlen(msg), 0);
}
proc2.join();
……
}
- 클라이언트로부터 데이터를 수신하는
proc_recvs()
함수 - 메세지가 담길 버퍼 생성 후, winsock2에 정의된
recv()
를 통해 해당 버퍼에 데이터를 반환 받아 출력함 - 해당 함수를 thread를 통해 실행하여, 클라이언트에게 메세지를 전송하는 구문과 동기적으로 작업이 일어남
1
2
3
4
5
6
7
8
9
int main() {
……
closesocket(client_socket);
closesocket(server_socket);
WSACleanup();
return 0;
}
- closesocket() 함수를 통해, 클라이언트와 서버 소켓을 닫고
- WSACleanup() 함수를 통해 설정된 리소스를 초기화 함
클라이언트 코드 리뷰
1
2
3
4
5
6
7
클라이언트 측의 로직
1. Winsock을 초기화(Initialize)
2. 소켓을 생성(Create)
3. 소켓, IP 주소, PORT 번호와 함께 서버에 연결(Connect)
4. send/recv와 같은 함수를 통해 서버와 데이터를 주고 받음
5. 클라이언트의 소켓을 닫는다(Close)
6. Winsock의 사용을 종료(Clean Up)
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
#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리
#include <iostream>
#include <winsock2.h>
#include <thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크
#define PACKET_SIZE 1024 // 송수신 버퍼 사이즈 1024로 설정
SOCKET server_socket;
int main() {
WSADATA wsa; // Windows 소켓 구현에 대한 구조체 생성
// WSAStartup을 통해 wsadata 초기화
// MAKEWORD(2,2) 매개 변수는 시스템에서 Winsock 버전 2.2를 요청
// WSAStartup은 성공시 0, 실패시 SOCKET_ERROR를 리턴하므로
// 리턴값이 0인지 검사하여 에러 여부 확인
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
std::cout << "WSAStartup failed" << endl;
return 1;
}
……
WSACleanup();
return 0;
}
- 선언 및 초기화 부분은 서버측과 동일함
- 리소스 해제하는 부분도 동일
1
2
3
4
5
6
7
8
9
// 클라이언트에서 접속할 서버 소켓 생성
server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
// 주소 패밀리, IP 주소 및 포트 번호에 대한 정보를 보유할 sockaddr 구조체 생성
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 서버의 IP 설정. 서버가 클라이언트와 동일한 IP에서 실행되므로 여기서는 loopback 설정
- 서버 소켓을 생성하고 초기화하는 부분도 같음
- inet_addr에서 서버의 IP를 입력해주어야하는 부분은 다르므로 주의할 것
1
2
3
while (1) { // 서버 소켓에 대해 반복 연결 시도, 연결 성공 시 break
if (!connect(server_socket, (SOCKADDR*)&addr, sizeof(addr))) break;
}
- 클라이언트에서 생성된 서버 소켓 및 sockaddr 구조를 매개변수로 connect() 함수에 전달하여 연결 시도
connect()
함수는 (연결이 필요한 소켓, 연결을 설정해야하는 sockaddr 구조체, 해당 구조체의 길이)를 매개변수로 전달하여, 지정된 소켓에 대한 연결을 설정하는 함수
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
void proc_recv() {
char buffer[PACKET_SIZE] = {}; //char 생성
while (!WSAGetLastError()) {
ZeroMemory(&buffer, PACKET_SIZE); //buffer 비우기
recv(server_socket, buffer, PACKET_SIZE, 0); //데이터받아오기
cout << "받은 메세지: " << buffer << endl;
}
}
int main() {
……
// server로부터 메세지를 수신하는 함수를 thread에 등록
thread proc1(proc_recv);
char msg[PACKET_SIZE] = { 0 }; // 메세지 입력받을 버퍼 생성
while (!WSAGetLastError()) {
ZeroMemory(&msg, PACKET_SIZE); // 버퍼 초기화하고, 입력받은 메세지를 서버에게 send
cin >> msg;
send(server_socket, msg, strlen(msg), 0);
}
proc1.join(); // 실행중인 thread의 작업 완료될때까지 대기
……
}
proc_recv()
함수를 통해 서버로부터 데이터를 수신- 메시지가 담길 버퍼 생성 후 ,
recv()
함수로 해당 버퍼에 데이터를 반환받아 출력 - 해당 함수를 thread를 통해 실행하여, main()에서 서버에게 메시지를 전송하는 구문과 동기적으로 작업이 이루어짐
1
2
3
4
5
6
7
8
int main() {
……
closesocket(server_socket);
WSACleanup();
return 0;
}
- closesocket()을 통해 서버를 닫고, WSACleanup() 함수를 통해 리소스를 해제함
Winsock2 단점
- 보안 취약성 주의.
- 네트워크를 다루는 라이브러리이므로, 버퍼 오버플로우 같은 공격에 취약함
- 교차 플랫폼 호환..?
- 리눅스에서 윈소켓을쓸 수는 있음. 사용할 수는 있지만,
sys/socket.h
을 쓰는게 편함
- 리눅스에서 윈소켓을쓸 수는 있음. 사용할 수는 있지만,
유사 라이브러리
boost::asio
: C++ 표준 라이브러리 기반의 네트워킹 라이브러리- Qt Network : QT 프레임 워크에서 제공하는 네트워킹 모듈. GUI 애플리케이션에서 네트워크 프로그래밍 가능
참조 :
Winsock2 라이브러리를 이용하여 간단한 TCP/IP 서버와 클라이언트를 구현해보자 | C++ C winsock2를 이용한 소켓 통신 구현
- 서버측 소스코드 전체
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
#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리 #include <iostream> #include <winsock2.h> #include <thread> using namespace std; #pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크 #define PACKET_SIZE 1024 // 송수신 버퍼 사이즈 1024로 설정 // 서버가 클라이언트 연결에 대해 수신 대기하는데 사용할 서버 소켓과,클라이언트와 연결을 맺는데 사용할 클라이언트 소켓 생성 SOCKET server_socket, client_socket; void proc_recvs() { char buffer[PACKET_SIZE] = { 0 }; while (!WSAGetLastError()) { ZeroMemory(&buffer, PACKET_SIZE); // 버퍼 초기화 recv(client_socket, buffer, PACKET_SIZE, 0); cout << "받은 메세지: " << buffer << endl; } } int main() { WSADATA wsa; // Windows 소켓 구현에 대한 구조체 생성 // WSAStartup을 통해 wsadata 초기화 // MAKEWORD(2,2) 매개 변수는 시스템에서 Winsock 버전 2.2를 요청 // WSAStartup은 성공시 0, 실패시 SOCKET_ERROR를 리턴하므로 // 리턴값이 0인지 검사하여 에러 여부 확인 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { std::cout << "WSAStartup failed" << endl; return 1; } // 서버가 클라이언트 연결을 수신 대기할 수 있도록 소켓 생성 server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 주소 패밀리, IP 주소 및 포트 번호에 대한 정보를 보유할 sockaddr 구조체 생성 SOCKADDR_IN addr = {}; addr.sin_family = AF_INET; // IPv4 기반의 TCP, UDP 프로토콜 사용 addr.sin_port = htons(4444); addr.sin_addr.s_addr = htonl(INADDR_ANY); // bind 함수를 통해 생성된 소켓 및 sockaddr 구조를 매개 변수로 전달 bind(server_socket, (SOCKADDR*)&addr, sizeof(addr)); listen(server_socket, SOMAXCONN); // 소켓에서 수신 대기 // SOMAXCONN은 Winsock 공급자에게 큐에 있는 최대 적정 수의 보류 중인 연결을 허용하도록 지시하는 특수 상수 SOCKADDR_IN client = {}; int client_size = sizeof(client); //ZeroMemory(&client, client_size); // 클라이언트에서 연결을 수락하기 위해 ClientSocket이라는 임시 소켓 개체 생성 client_socket = accept(server_socket, (SOCKADDR*)&client, &client_size); // 연결 시 클라이언트 정보 출력 if (!WSAGetLastError()) { std::cout << "연결 완료" << endl; std::cout << "Client IP: " << inet_ntoa(client.sin_addr) << endl; std::cout << "Port: " << ntohs(client.sin_port) << endl; } // client로부터 메세지를 수신하는 함수를 thread에 등록 thread proc(proc_recvs); char msg[PACKET_SIZE] = { 0 }; // 메세지 입력받을 버퍼 생성 while (!WSAGetLastError()) { ZeroMemory(&msg, PACKET_SIZE); // 버퍼 초기화하고, 입력받은 메세지를 클라이언트에게 send cin >> msg; send(client_socket, msg, strlen(msg), 0); } proc.join(); // 실행중인 thread의 작업 완료될때까지 대기 closesocket(client_socket); // client, server socket 닫기 closesocket(server_socket); WSACleanup(); // 리소스 해제 return 0; }
- 클라이언트 측 소스코드
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
#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리 #include <iostream> #include <winsock2.h> #include <thread> using namespace std; #pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크 #define PACKET_SIZE 1024 // 송수신 버퍼 사이즈 1024로 설정 SOCKET server_socket; void proc_recv() { char buffer[PACKET_SIZE] = {}; //char 생성 while (!WSAGetLastError()) { ZeroMemory(&buffer, PACKET_SIZE); //buffer 비우기 recv(server_socket, buffer, PACKET_SIZE, 0); //데이터받아오기 cout << "받은 메세지: " << buffer << endl; } } int main() { WSADATA wsa; // Windows 소켓 구현에 대한 구조체 생성 // WSAStartup을 통해 wsadata 초기화 // MAKEWORD(2,2) 매개 변수는 시스템에서 Winsock 버전 2.2를 요청 // WSAStartup은 성공시 0, 실패시 SOCKET_ERROR를 리턴하므로 // 리턴값이 0인지 검사하여 에러 여부 확인 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { std::cout << "WSAStartup failed" << endl; return 1; } // 클라이언트에서 접속할 서버 소켓 생성 server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 주소 패밀리, IP 주소 및 포트 번호에 대한 정보를 보유할 sockaddr 구조체 생성 SOCKADDR_IN addr = {}; addr.sin_family = AF_INET; addr.sin_port = htons(4444); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 서버의 IP 설정. 서버가 클라이언트와 동일한 IP에서 실행되므로 여기서는 loopback 설정 while (1) { // 서버 소켓에 대해 반복 연결 시도, 연결 성공 시 break if (!connect(server_socket, (SOCKADDR*)&addr, sizeof(addr))) break; } // server로부터 메세지를 수신하는 함수를 thread에 등록 thread proc1(proc_recv); char msg[PACKET_SIZE] = { 0 }; // 메세지 입력받을 버퍼 생성 while (!WSAGetLastError()) { ZeroMemory(&msg, PACKET_SIZE); // 버퍼 초기화하고, 입력받은 메세지를 서버에게 send cin >> msg; send(server_socket, msg, strlen(msg), 0); } proc1.join(); // 실행중인 thread의 작업 완료될때까지 대기 closesocket(server_socket); // client socket 닫기 WSACleanup(); // 리소스 해제 return 0; }
- 윈도우 환경에서 C++을 이용한 소켓 통신 구현
- 직접 짜보면서 복습해볼 것!
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.