포스트

winsock2 라이브러리를 이용한 채팅 구현

1. Winsock2이란?


  • Windows Sockets API(WSA)
  • Windows 운영 체제에서 네트워크 프로그래밍을 수행하는데 사용되는 API
  • 인터넷 네트워크 및 소켓 관련 함수들 제공
  • 버전1과 버전2가 있음. 보통 2를 사용함
  • 레퍼런스 문서
  • 헤더 및 함수 정보

2. Winsock2 장점 및 특징


  • 윈도우 환경에서 효율적으로 자원을 사용할 수 있음
  • 다양한 네트워크 프로토콜, 소켓 유형, 주소 체계를 지원

3. Winsock2의 구성 및 사용법


ServerandClient

서버측 코드 리뷰

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 프로토콜을 사용하는 것을 의미함
  • 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() 함수를 사용하여 메시지를 전송함
  • ZeroMemory()는 메모리를 0으로 채울때 쓰는 함수. cstring.hmemset 함수와 유사함
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 라이센스를 따릅니다.