공부 (100)

01
27

ODBC 관리자

ODBC 관리자

ODBC 관리자에 해당 사용할 DBMS 드라이버가 있다면

ODBC로 데이터베이스를 불러온 준비가 되었다.

 

 

본인은 MS사의 Access를 사용할 것이므로 MS 드라이버를 다운로드하였다.

설치 방법은 바로 이전 포스팅 참고하면 된다.

 

ODBC 핸들 종류 및 할당 및 해제

ODBC핸들에는 종류가 4가지가 있다.

 

핸들 종류

 

환경 핸들, 연결(접속) 핸들, 명령 핸들, 설명 핸들이 그것이다.

 

할당 함수

1
2
3
4
SQLRETURN SQLAllocHandle(  
      SQLSMALLINT   HandleType,//할당하고자 핸들 타입
      SQLHANDLE     InputHandle,//생성할 부모 핸들 지정
      SQLHANDLE *   OutputHandlePtr);// 생성할 핸들의 주소
cs

 

파라미터에 부모 핸들 지정이 있는데, 

환경 핸들을 만들고 환경 핸들 갖고 접속 핸들을 만들고

접속핸들을 가지고 접속에 성공하면 명령 핸들을 상속한다.

상속관계를 가지고 있다.

 

반환 값이 SQLRETURN 인데, 성공하면 SQL_SUCCESS를 반환한다. 

SQL_ERROR 또는 SQL_SUCCESS_WITH_INFO 플래그가 뜬다면

에러 처리를 하기 위한. SQLError() 함수가 있다.

 

해제 함수

1
2
3
SQLRETURN SQLFreeHandle(  
     SQLSMALLINT   HandleType, //해제할 핸들 ㅏ입
     SQLHANDLE     Handle);  // 해제 
cs

 


환경 핸들, 연결(접속) 핸들, 명령 핸들을 사용해서

MS Access 파일인 User_DB.accdb 파일을 불러와보자.

파일

1. ODBC 헤더 파일, 라이브러리를 추가한다.

ODBC Core functions을 하는 헤더와 라이브러리 추가

#include <sql.h>, #include <sqlext.h>

링크 : obdc32.lib

라이브러리는 기본으로 프로젝트에 링크가 되어있다.

 

2. 환경 핸들 할당

핸들 선언

우선 환경 핸들을 SQLAllocHandle()함수로 할당한다.

 

 

그런 후에 환경 버전 처리를 하는 SQLSetEnvAttr()함수를 호출한다.

 

 

3. 연결 핸들 할당 및 연결 시도 

환경 핸들을 부모로 두어 접속 핸들 할당한다.

 

 

그런 다음 접속 시도인 SQLDriverConnect()함수를 호출한다.

SQLDriverConnect()를 하기 위해서 드라이버 경로를 입력해줘야 한다.

 

하드코딩

 

위 방법처럼 직접 드라이버를 입력하는 건 좋지 않다.

.dsn 파일의 규격의 텍스트 파일을 읽어서 연결해야 한다. 

 

아래는 해당 프로젝트 안에 있는. dsn 파일을 읽어온 코드이다.

 

4. 명령 핸들 할당 및 명령

드라이버의 연결이 완료가 되었다면 명령 핸들을 할당해서 명령을 한다.

연결이 되었다면 SQLAllocHandle 함수로 명령 핸들 할당한다. 

 

 

명령문(쿼리문)부터 보자

 

위 사진은 유저 테이블 안에 모든 값을 조회해달라는 쿼리문이다.

SQLExecDirect 바로 명령어 처리하는 함수이다.

1
2
3
4
SQLRETURN SQLExecDirect(  
     SQLHSTMT     StatementHandle, // 명령 핸들
     SQLCHAR *    StatementText,  // 실행할 SQL문
     SQLINTEGER   TextLength);  // SQL문의 문자열 
cs

 

Select문, Update문, Insert문, Delete문 모두 가능하다.

다루지는 않겠다. 

Select 문으로 그 값을 개발자가 조회해야 하는데,

조회된 값을 얻어오기 위해서는

c언어 변수와 테이블 변수와 연동을 해야 한다.  (Bind)

 

SQLBindCol - 변수 Binding

1
2
3
4
5
6
7
SQLRETURN SQLBindCol(  
      SQLHSTMT       StatementHandle, //명령 핸들 
      SQLUSMALLINT   ColumnNumber,  //바인딩될 컬럼의 번호
      SQLSMALLINT    TargetType,  //바인딩 되는 변수의 데이터 타입
      SQLPOINTER     TargetValuePtr,  // 결과값 저장할 버퍼
      SQLLEN         BufferLength,  //버퍼의 길이
      SQLLEN *       StrLen_or_IndPtr);  //컬럼의 길이나 상태를 리턴
cs

 

반복문으로 결과 집합에서 다음 데이터 행 집합을 가져오고

바인딩된 모든 열에 대한 데이터를 반환하는 함수 호출.

SQLFetch() 함수 - 바인딩된 모든 열 데이터 반환

1
2
SQLRETURN SQLFetch(  
     SQLHSTMT     StatementHandle);  
cs

 

데이터 SQL_NO_DATA 데이터가 없을 때까지 커서를 통해서 조회를 한다.

시작 하기 전의 커서 상태 블록 커서는 결과 집합의 시작 부분 앞에 배치 됩니다. 새 행 집합의 첫 번째 행이 결과 집합의 시작 앞에 있으면 Sqlfetch 는 SQL_NO_DATA을 반환 합니다.
종료 후의 커서 상태 블록 커서는 결과 집합의 끝 뒤에 배치 됩니다. 새 행 집합의 첫 번째 행이 결과 집합의 끝 뒤에 있으면 Sqlfetch 는 SQL_NO_DATA을 반환 합니다.

SQL_NO_DATA는 데이터가 커서의 위치에서 데이터가 없을때 뜬다.

while (SQLFetch(handle_HSTMT) != SQL_NO_DATA)로 

커서 위치에 데이터가 없을 때까지 출력하도록 한다.

만약 커서를 다 사용했다면 SQLCloseCursor(명령핸들); 로 커서를 초기화한다.

 

 

 

결과 

성공적으로 DB에 있는 데이터를 Select로 전체 조회를 했다. 

 

전체 코드

더보기
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
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <tchar.h>
#include <string>
#include <sql.h>
#include <sqlext.h>
#include <iostream>
//SQLHANDLE = void* 임 이름만 다름
SQLHENV handle_HENV; // 환경핸들
SQLHDBC handle_HDBC; // 접속핸들
SQLHSTMT handle_HSTMT; // 명령핸들
void Check();
void main()
{
    setlocale(LC_ALL, "");
    if (SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &handle_HENV) != SQL_SUCCESS)
    {
        return;
    }
    //환경 설정 환경 버젼 선택
    if (SQLSetEnvAttr(handle_HENV, SQL_ATTR_ODBC_VERSION,
         (SQLPOINTER)SQL_OV_ODBC3_80, SQL_IS_INTEGER) != SQL_SUCCESS)//버젼처리
    {
        return;
    }
    //환경 핸들 갖고 접속핸들 만듬 (환경핸들이 부모)
    if (SQLAllocHandle(SQL_HANDLE_DBC, handle_HENV, &handle_HDBC) != SQL_SUCCESS)
    {
        return;
    }
 
    //접속을 해야함 유니코드 버젼으로 하겠다. 버젼선택 mdb, accdb 파일을 읽어온다.
    //파일 경로 스트링
    SQLWCHAR dir[MAX_PATH] = { 0, };
    GetCurrentDirectory(MAX_PATH, dir);
    std::wstring dbpath = dir;
    dbpath += L"\\User_DB.dsn";
 
    TCHAR inCon[256= { 0, };
    TCHAR outCon[256= { 0, };
 
    //연결 Connect. _countof 문자열의 길이
    //원래는 .dsn 파일의 규격의 텍스트 파일을 읽어서 연결해야한다. 
    _stprintf(inCon, _T("FileDsn=%s"), dbpath.c_str());
    
    SQLSMALLINT cbOut_Len;
    SQLRETURN ret = SQLDriverConnect(handle_HDBC, NULL, inCon,_countof(inCon),
                    outCon, _countof(outCon), &cbOut_Len, SQL_DRIVER_NOPROMPT);
 
    //접속이 성공하면 success가 뜨는데 완벽한 성공, 두번째는 부족한대 성공 
    if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO)
    {
        Check();
        return;
    }
 
    if (SQLAllocHandle(SQL_HANDLE_STMT, handle_HDBC, &handle_HSTMT) != SQL_SUCCESS)
    {
        return;
    }
 
    SQLLEN lid = SQL_NTS;
    SQLLEN lname = SQL_NTS;
    SQLLEN llevel = SQL_NTS;
    int user_id = 0;
    int user_level= 0;
    TCHAR user_name[20= { 0, };
 
    //결과를 저장 바인드
    //첫번째 필드로 바인드 각각의 레코드를 반환해준다. 그중에 1번 필드
    //명령 핸들에 사용자 c++ 변수를 바인딩해줌 메모리 연결
    ret = SQLBindCol(handle_HSTMT, 1, SQL_C_ULONG, &user_id, 0&lid);
    ret = SQLBindCol(handle_HSTMT, 2, SQL_UNICODE, user_name, sizeof(user_name), &lname);
    ret = SQLBindCol(handle_HSTMT, 3, SQL_C_ULONG, &user_level, 0&llevel);
 
    //쿼리문 SQL문 demogame 테이블에서 모든 값을 출력
    TCHAR sql[MAX_PATH] = L"select * from user_table";
    ret = SQLExecDirect(handle_HSTMT, (SQLTCHAR*)&sql, SQL_NTS);
 
    // 행을 반환할 때 바인딩된 각 열에 대한 데이터를 
    // 해당 열에 바인딩된 버퍼에 넣는다
    while (SQLFetch(handle_HSTMT) != SQL_NO_DATA)
    {
        std::wcout << L"번호 : " << user_id << L"\t이름 : " <<
            user_name << L"\t레벨 : " << user_level<< std::endl;
    }
    SQLCloseCursor(handle_HSTMT);
 
    SQLFreeHandle(SQL_HANDLE_STMT, handle_HSTMT);
    SQLDisconnect(handle_HDBC);
    SQLFreeHandle(SQL_HANDLE_DBC, handle_HDBC);
    SQLFreeHandle(SQL_HANDLE_ENV, handle_HENV);
}
void Check()
{
    SQLTCHAR szSQLState[SQL_SQLSTATE_SIZE + 1];
    SQLTCHAR errorBuffer[SQL_MAX_MESSAGE_LENGTH + 1];
    SQLINTEGER iSQLCode;
    SQLSMALLINT length;
    SQLError(handle_HENV, handle_HDBC,
        handle_HSTMT,
        szSQLState,
        &iSQLCode,
        errorBuffer,
        sizeof(errorBuffer),
        &length);
    MessageBox(NULL, errorBuffer, szSQLState, MB_OK);
}
cs

 

COMMENT
 
01
27

https://docs.microsoft.com/ko-kr/sql/odbc/microsoft-open-database-connectivity-odbc?view=sql-server-ver15

 

Microsoft ODBC (Open Database Connectivity) - Open Database Connectivity (ODBC)

Microsoft ODBC(Open Database Connectivity)

docs.microsoft.com

 

Oracle
MySQL
MSSQL
MariaDB
- 오라클에서 만들어 판매중인 상업용 데이터베이스
- 윈도우, 리눅스, 유닉스 등 다양한 운영체제(OS)에서 설치 가능
- MySQL, MSSQL보다 대량의 데이터 처리 용이
- 대기업에서 주로 사용하며, 글로벌 DB 시장 점유율 1위
- 비공개 소스, 폐쇄적인 운영
- 가장 널리 사용되는 RDBMS
- MySQL사에서 개발, 썬마이크로시스템즈를 거쳐 현재 오라클에 인수합병
- 윈도우, 리눅스, 유닉스 등 다양한 운영체제(OS)에서 설치 가능
- 오픈소스로 이루어져있는 무료 프로그램(상업적 사용 시 비용 발생)
- 가격 등의 장점을 앞세워 다수의 중소기업에서 사용중
- RDBMS
- 마이크로소프트(MS)사에서 개발한 상업용 데이터베이스
- 다른 운영체제에서도 사용가능하지만 윈도우에 특화됨
- 비공개 소스로 폐쇄적인 운영(리눅스 버전은 오픈소스)
- 중소기업에서 주로 사용중
- RDBMS
- MySQL이 오라클에 인수합병된 후 불확실한 라이선스 문제를 해결하려고 나온 오픈소스 RDBMS
- 구현언어 : C++
- MySQL과 동일한 소스 코드 기반
- MySQL과 비교해 애플리케이션 부분 속도가 약 4~5천배 정도 빠름

출처 : https://m.blog.naver.com/sundooedu/221301384166

 

위는 DBMS 데이터베이스 관리 프로그램 종류이다. 

응용프로그램에서는 DBMS와 데이터를 주고받는 통신을 한다.

근데 DBMS의 종류가 여러개이고, 각자 프로토콜이 다르다.

또한 DBMS사에서 프로토콜을 공개하지 않는다. 

 

Vender API

 

프로토콜을 공개하지 않는데, 어떻게 프로그램이랑 DB랑 통신을 하냐면

Vender API를 사용한다.

DBMS 마다 서로 다른 Vender API로 DBMS에 종속적인 문제가 있다. 

 

ODBC API

 

MS가 최초로 시도한 데이터베이스 연결을 위한 표준이다.

앞서 말한 문제점을 가지고 MS사에서 ODBC API 규격으로

모든 DBMS에 통합된 접근 방법을 만들었다.

 

DBMS는 본인사의 ODBC Driver를 만들어 제공한다. 

 

ODBC API <-> 해당DBMS해당 DBMS Driver <-> 해당 DBMS Vender API <-> 해당 DBMS

 

이런 식으로 통신한다. 비종속적으로 ODBC은 여러 DBMS 기능을 가지고 있음

단점으로는 Vender API를 쓰는 것보다 여러 인터페이스를 거치기 때문에

직접 이용하는 것보다는 속도가 느리다. 

 

OCBC 데이터 타입에는 Sql문과 C언어형 두가지 형태의 데이터 타입을 지원한다.

예를 들어 long int 타입은 [C타입 - SQL_C_ULONG , SQL - SQLINTEGER ] 

두 가지 모두 사용할 수 있다. 이렇게 두 가지를 지원하는 이유는 

JAVA나 c#과 C언어의 데이터 형이 다르기 때문이다. 

 

ODBC, DAO, RDO, OLE DB, ADO

 

이름 내용
ODBC 데이터 엑세스를 위해 C언어에서 사용하도록 설계된 표준 하위 애플리케이션 프로그래밍 인터페이스 (API)이다. SQL 쿼리문으로 그 데이터베이스 언어로 사용한다.  
DAO ODBC 이후 발표된 것, Jet Database Engine에 접근하기 위해 프론트 엔드 애플리케이션 개발에 필요한 c++ 인터페이스
로컬 DB용으로 많이 쓰임
RDO 서버에 기반을 둔 데이터베이스 기술
OLE DB OLE라고 불리는데, COM인터페이스의 집합체이다. 객체 연결 삽입 데이터 베이스로 ODBC를 높은 수준으로 대체하면서 그 뒤를 잇는 설계, 마이크로소프트 데이터 엑세스 구성 요소(MDAC)스택의 일부로 파일 시스템, 스프레트시트,ASP 같은 다양한 문서를 사용가능
ADO ActiveX Data Objects로 웹 기반의 애플리케이션이나 데이터 기반에서 쓰일 수 있는 객체 기반 인터페이스 

 

설치

 

앞으로 MS Access Database 데이터베이스 응용프로그램으로

Visual Studio에 연결하여 데이터베이스를 관리할 것이다.

 

ODBC를 사용해여 DBMS(MS Access Database)와 Visual studio 통신하려면 드라이버를 설치해야 한다.  

Microsoft Access Database Engine 2010 재배포 가능 패키지

https://www.microsoft.com/ko-kr/download/details.aspx?id=13255 

 

Download Microsoft Access Database Engine 2010 재배포 가능 패키지 from Official Microsoft Download Center

중요! 아래에서 언어를 선택하면 전체 페이지 내용이 해당 언어로 신속하게 변경됩니다. 독일어스페인어영어이탈리아어일본어중국어(간체)중국어(번체)프랑스어한국어 다운로드 이 다운로드

www.microsoft.com

 

설치 완료해서 ODBC 데이터 관리자에서 확인이 되면 설치 완료된 것,

 

COMMENT
 
01
26
모델 내용
Select 모델 Select 모델은 동기 입출력을 수행하기 위해 필요한 경우 대기 중인 하나 이상의 소켓 상태를 결정한다.
소켓을 만들고, Select함수를 통해 FD_SET으로 해당 소켓을 등록하여 FD_ISSET으로 관찰할 수 있는 방식이다.
 
fd_set -> Readfds – 수신할 데이터가 있을 때
         Writefds – 쓰기 가능한 상태일 때, 연결 성공했을 때
         Exceptfds – 연결이 실패했을 때
 
소켓모드와 상관없이 한 스레드에서 여러 소켓을 처리할 수 있고, 윈도우, 유닉스 등 여러 운영체제에서 사용할 수 있어서 이식성이 좋지만, 성능은 여섯 가지 모델 중 가장 좋지 않다.
 64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야한다.
WSAAsyncSelect 모델 Async는 비동기적이라는 의미로 윈속의 소켓이벤트를 윈도우 메시지를 통해서 비동기적으로 받는다. 윈도우 메시지를 통해 성공 여부를 판단한다. 윈도우 메시지 큐에 의존해 소켓의 처리가 이루어져 많은 수의 소켓 이벤트를 처리해야 하는 경우 큐가 고갈될 수 있다. 윈도우 메시지를 사용자 정의하여 원하는 네트워크 이벤트를 발생시킨다.
 
네트워크 이벤트
FD_WRITE – WSAConnect 함수 호출하고 소켓이 처음 연결되었을 때, WSAAccept 호출 뒤 연결 수락되었을 때 send, WSASend, sendto, WSASendTO 함수 호출 후에 WSAEWOULDBLOCK이 리턴이 돼서 전송 버퍼가 비워졌을 때 이벤트 발생한다. , 보낼 수 있는 상태가 되면 발생한다.
FD_ACCEPT – 클라이언트가 접속했을 때
FD_READ – 수신 가능할 때
FD_CLOSE – 접속 종료할 때
FD_CONNECT – 접속 완료될 때
FD_OBB – 데이터가 도착할 때
 
소켓 이벤트를 윈도우 메시지 형태로 처리해서 윈도우 친화적이다. 서버보다 클라이언트에 유리하다. 하지만 하나의 윈도우 프로시저에서 일반 윈도우 메시지와 소켓 메시지를 처리해야 해서 성능저하의 요인이 된다. 또한 윈도우여야만 적용이 가능하다는 단점이 있다.
WSAEventSelect 모델 두개 이상의 스레드가 협력해서 작업을 하는 경우에 상황에 따라 동기화가 필요하다. 그때 동기화 객체가 쓰이는데, event객체는 그 중 하나이며, 커널 오브젝트이기도 하다.
소켓과 WSACreateEvent()로 이벤트 객체를 생성하여 짝지어서 네트워크 이벤트가 발생함을 애플리케이션에서 알 수 있게 되는 방식이다.
 
WaitForMultipleEvent() 함수로 신호 상태가 된 커널 오브젝트에 네트워크 이벤트가 발생했다는 사실을 알 수 있기 때문이다. 반환 값은 신호 상태로 변화된 이벤트 배열의 인덱스이다.
또한, WSAEnumNetworkEvents()로 구체적인 네트워크 이벤트를 알려주는 함수도 있다.
 
Select 모델과 WSAAsyncSelect 모델의 특성을 혼합한 형태로 비교적 뛰어난 성능을 가짐에도 WSAAsyncSelect와 달리 윈도우 메시지가 아닌 이벤트 오브젝트를 사용해 윈도우가 아니어도 구현 가능하다.
단점으로는64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야한다.
Overlapped 모델 I
(Overlapped Event)
Overlapped은 중첩 입출력으로 응용 프로그램은 입출력 함수를 호출한 후에도 입출력 작업의 완료 여부와 무관하게 다른 작업을 하는 비동기 입출력을 지원한다.
IO가 여러개 중첩되는 것, 요청하고 기다릴 필요없이 리턴하고
다른 일을 수행하는 것이 Overlapped IO이고, 통지하는 방법에 따라 종류가 나뉜다.
 



 
WSAEventSelect 모델과 비슷하게 이벤트 객체를 사용한다. 소켓 입출력 작업이 완료되면, 운영체제는 응용 프로그램이 등록한 이벤트 객체를 신호 상태로 바꾼다. 이벤트 객체를 관찰함으로써 입출력 작업 완료를 감지할 수 있다.
 
비동기 입출력을 지원하는 소켓을 생성하고 WSACreateEvent() 함수로 대응하는 이벤트 객체도 같이 생성한다. 비동기 입출력을 지원하는 소켓 함수를 호출할 때 WSAOVERLAPPED 구조체의 hEvent변수에 이벤트 개체 핸들 값을 넣어 전달한다.
동기 입출력이 완료가 되지 않았다면 오류를 리턴 하며. 오류 코드는 WSA_IO_PENDING으로 된다. 입출력이 완료가 되면 운영체제는 이벤트 객체를 신호 상태로 만들어 통보한다.
WSAWaitForMultipleEvent() 함수가 리턴 되면 WSAGetOverlappedResult()함수로 비동기 입출력 결과를 확인하고 데이터를 처리한다.
 
비동기 입출력을 통해 성능이 뛰어나지만, 이벤트 객체로 64개 이상의 소켓을 처리하려면 여러 개의 스레드를 사용해야 하는 단점이 있다.
Overlapped 모델 II
(Overlapped Callback)
소켓 입출력이 완료되면 등록해둔 함수를 자동으로 호출한다. 일반적으로 운영체제가 호출하는 응용 프로그램 함수를 콜백함수라고 하는데, Overlapped 모델에서는 완료 루틴(completion routine) 이라고 한다.
 
비동기 입출력을 지원하는 소켓을 생성한다. 비동기 입출력 함수(WSASend, WSARecv)를 호출할 때 완료 루틴의 시작 주소를 함수 인자로 전달한다. 비동기 입출력 작업이 완료되지 않으면 WSA_IO_PENDING이 된다. 운영체제에 입출력 작업요청을 한다. WaitForSingleObjectEx(), WaitForMultipleObjectEx(), SleepEx(), WSAWaitForMultipleEvent() 등 함수로 해당 스레드는 완료 루틴이 호출될 수 있는 상태인 Alertable wait 상태로 진입하게 한다.
비동기 입출력 작업이 완료되면, 운영체제는 스레드의 APC큐에 결과를 저장하고 완료 루틴을 호출한다.
 
APC : asynchronous procedure call queue는 비동기 입출력 결과를 저장을 위해 운영체제가 각 스레드에 할당하는 메모리 영역이다.
 
비동기 입출력 함수를 호출한 스레드가 Alertable wait 상태에 있으면 운영체제는 APC 큐에 저장된 정보를 참조하여 완료 루틴을 호출한다. 완료 루틴 호출이 끝나면, 스레드는 Alertable wait 상태에서 빠져나온다. 스레드가 비동기 입출력 결과를 계속 처리하기 위해서 다시 Alertable wait 상태에 진입하는 동작 원리를 갖는다.
 
장점으로는 비동기 입출력을 통해 뛰어난 성능을 갖는다. 단점으로는 모든 비동기 소켓 함수에 대해 완료 루틴을 사용할 수 있는 건 아니다. 콜백 함수의 결과를 해당 콜백 함수를 수행한 스레드 만이 알 수 있다.
IOCP
Completion Port
비동기 입출력으로 입출력을 요청하고도 다른 일을 수행하는 것이 Overlapped IO이다. Overlapped IO를 이벤트와 콜백을 사용해 작업완료 여부를 체크하는 방법 중 하나로 윈도우에서 제공하는 일반적인 I/O 모델 중 최고 성능을 갖는 IOCP이다.
 
커널 객체(운영체제의 핵심적인 기능 프로세스, 스레드 메모리 관리 기능 제공하는 객체)이다. IOCP 시스템을 관리하는 객체를 커널이 만들어서 우리에게 HANDLE를 넘겨주는 것이다.
 

IOCP 특징 중 하나가, 쓰레드 풀로 쓰레드를 재사용하는 것인데,
IOCP 본인의 스레드 큐를 가지고 작업을 하고 완료된 IO가 있다면 알려준다.
재사용이 가능한 스레드를 유지하는 스레드 풀링은
WSASend/ WSARecv같은 비동기 입출력 함수를 쓰면
스레드 내부적으로 APC 큐가 생성이 된다. 여기에 완료된 결과를 저장하는데
APC큐는 자동으로 생성되고 파괴된다. 그리고 그 큐는 본인 스레드에서만
확인이 가능하는데, 반면에 관리자 역할을 하는 IOCP는 모든 걸 확인이 가능하며, CloseHandle()함수 호출하여 파괴한다. 입출력 완료 포트를 접근하는 스레드를 별도로 두는데 이를 작업 스레드(WorkerThread라고 한다.)
 
예를 들어 관리자(IOCP)가 일꾼(1번 입출력), 일꾼(2번 입출력)
1번 일꾼의 IO작업이 끝났 다하면 이를 처리하기 위해 Waiting Thread Queue
가장 최근에 들어온 스레드를 깨운다. I/O가 완료될 때마다 기다리고 쓰레드가 알아서 I/O작업을 해 성능이 좋다. -> 스레드 풀
 
또한, 특정 스레드에서 작업을 하던 간에IOCP 큐자체가 기타 스레드 결과가 저장이 된다.
, A 스레드에서 작업해도 B스레드에서도 확인이 가능하다.
 
IOCP 자료구조 설명
Device list hDevice, Completion Key로 구성되어 키 값으로 값을 얻을 수 있게 됨
IO Completion Queue FIFO로 앞에서부터 자료를 빼서 처리, I/O가 완료 되면 정보(이벤트)를 저장함
Waiting Thread Queue LIFO로 마지막에 사용된 스레드를 다시 사용함, 스레드 ID가 저장됨  스레드 풀 역할
Released Thread List Waiting Thread Queue(스레드 풀)에서 꺼내 온 쓰레드 정보 
Paused Thread List Release쓰레드가 Suspend상태되면 저장되고, Suspend상태가 해제되면 다시 Released Thread List로 올라감

 

소켓 입출력 모델 (Select, WSAAsyncSelect, WSAEventSelect는 동기 입출력과 비동기 통지를 결합한 형태

비동기 소켓 입출력 모델은 (Overlapped, Completion Port)는 비동기 입출력과 비동기 통지를 결합한 형태

COMMENT
 
01
21

std::fucnction

c의 함수포인터를 대체하는 c++11부터 추가된 기능이다.

용도 : 함수포인터 반환, 함수를 다른 함수에 전달

 

전역 함수를 function로 받아옴

 

전역으로 선언된 Func 함수를 std::function 기능으로 받아온 것이다. 

전역함수가 아니라 클래스의 멤버함수의 경우에는

 

클래스의 맴버함수를 function로 받아옴

 

클래스 객체를 하나 만들어서 함수의 주소를 전달해서 사용해야한다.

또한 그렇게 만든 함수를 사용할때도 객체를 전달해야한다. 


std::bind

Functional과 마찬가지로 c++11부터 표준이다.

용도 : 함수의 일부 매개변수를 고정 값으로 정해, 사용할 수 있게 한다. 

 

 

위 코드는 bind로 1, 2.0f, false로 함수의 매개변수를 고정시킨 것이다.

FuncA(5,6,false) 로 넣든간에 120의 결과로 나오는 걸 볼 수 있다.  

bind시 인자를 변수로 변경하고 싶으면 std::placeholders를 사용해야한다. 

 

 

 function을 사용하지않고 간편하게 auto로 bind를 할 수 있다.  

 

  1) function Class를 사용

    std::function<함수의 형태> 변수명  = bind(함수명, 인자1, 인자 2,...);


  2) auto 키워드 사용

    auto 변수명  = bind(함수명, 인자 1, 인자 2,...);

auto가 편하니 auto를 쓰자


std::placeholders

bind시 인자를 변수로 변경하고 싶으면 std::placeholders를 사용해야한다. 

 

 

std::placeholders::_1 의 숫자는 인자의 순서를 뜻한다.

고로 첫번째인자에는 _1이 들어가고, 두번째인자에는 _2가 들어간다.

bind 마지막 인자에 _1를 넣었는데, 이렇게 되면 첫번째 인자가 불값으로 들어간다.

결과 121이 나오는 걸 확인 할 수 있음

 

 

COMMENT
 
01
19

 

이전 포스팅에서 Overlapped 구조체를 사용해서 

비동기 파일 입출력을 구현했었다.

WriteFile함수와 ReadFile 함수에 Overlapped 구조체를 넣고 IO를 중첩시키고,

WSASend 하면 바로 리턴됨 (ERROR_IO_PENDING)

GetOverlappedResult함수로 해당 IO의 결과를 얻을 수 있게 해주는 함수도 있었다.

 

IOCP는 Overlapped IO로 바탕으로 결과를 받아 처리하는 하나의 방식으로

Overlapped IO를 기본적으로 알고 있어야 한다.  

IO가 여러개 중첩되는 것, 요청하고 기다릴 필요없이 리턴하고

다른 일을 수행하는 것이 Overlapped IO이고, 

 

중첩

 

이 작업 완료 여부를 체크하는 방법 중 하나가 IOCP인 것이다.

Overlapped구조체, 이벤트 객체멤버 등 다른 방법들이 있지만

IOCP는 입출력 요청한 스레드는 계속 다른 작업이 가능하다.

 

IOCP 개요

 

IOCP는 윈도우에서 제공하는 일반적인 I/O 모델 중 최고 성능을 갖고 있다 한다.

커널 객체(운영체제의 핵심적인 기능 프로세스, 스레드 메모리 관리 기능 제공하는 객체)이다. 

https://docs.microsoft.com/ko-kr/windows/win32/sysinfo/kernel-objects

 

커널 개체 - Win32 apps

커널 개체 핸들은 프로세스 마다 다릅니다.

docs.microsoft.com

 

위 MSDN에 IOCP가 있는 것을 볼 수 있다.

IOCP 시스템을 관리하는 객체를 커널이 만들어서 우리에게 HANDLE를 넘겨주는 것이다.

 

IOCP 자료구조 설명
Device list hDevice, CompletionKey로 구성되어 키값으로 값을 얻을 수 있게됨
IO Completion Queue FIFO로 앞에서 부터 자료를 빼서 처리, I/O가 완료 되면 정보(이벤트)를 저장함
Waiting Thread Queue LIFO로 마지막에 사용된 스레드를 다시 사용함, 스레드 ID가 저장됨  스레드 풀 역활
Released Thread List Waiting Thread Queue(스레드 풀)에서 꺼내온 쓰레드 정보 
Paused Thread List Release쓰레드가 Suspend상태되면 저장되고, Suspend상태가 해제되면 다시 Released Thread List로 올라감

 

IOCP 특징 중 하나가, 쓰레드 풀로 쓰레드를 재사용하는 것인데,

IOCP 본인의 스레드 큐를 가지고 작업을 하고 완료된 IO가 있다면 알려준다.

 

재사용이 가능한 스레드를 유지하는 스레드 풀링은

WSASend/ WSARecv같은 비동기 입출력 함수를 쓰면

스레드 내부적으로 APC 큐가 생성이된다. 여기에 완료된 결과를 저장하는데

APC큐는 자동으로 생성되고 파괴된다. 그리고 그 큐는 본인 스레드에서만

확인이 가능하는데, 관리자 역활을 하는 IOCP는 모든걸 확인이 가능하다.

 

예를들어 관리자(IOCP)가 일꾼(1번 입출력), 일꾼(2번 입출력) 중

1번 일꾼의 IO작업이 끝났다하면 이를 처리하기 위해 Waiting Thread Queue에

가장 최근에 들어온 스레드를 깨운다. I/O가 완료될때마다 기다리고 쓰레드가 알아서 I/O작업을 해

성능이 좋다. -> 스레드 풀

 

또한, 특정 스레드에서 작업을 하던간에

IOCP 큐자체가 기타 스레드 결과가 저장이 된다.

즉, A 스레드에서 작업해도 B스레드에서도 확인이 가능하다.

 

1. IOCP 객체 핸들 생성

1
2
3
4
5
6
HANDLE WINAPI CreateIoCompletionPort(
  _In_      HANDLE FileHandle,
  _In_opt_  HANDLE ExistingCompletionPort,
  _In_      ULONG_PTR CompletionKey,
  _In_      DWORD NumberOfConcurrentThreads
);
cs

 

CreateIoCompletionPort 함수는 세 가지의 역활을 한다.

 - IOCP 포트만 생성

 - 기존 IOCP 포트와 파일,소켓과 연결

-  생성 및 연결 모두 수행

 

처음 IOCP 객체 핸들 생성할때는 이런 식으로 생성한다.

1
2
    g_hIOCP = CreateIoCompletionPort(
        INVALID_HANDLE_VALUE, 000);
cs

 

마지막 인자 NumberOfConcurrentThreads는 0이면 시스템 프로세서에 있는 만큼

스레드를 허용하는 것이다. CPU 갯수보다 일부로 낮게 넣지 않는 이상, 0을 넣으면 된다.

 

2. IOCP와 소켓(파일) 연결

위의 CreateIoCompletionPort()는 세가지 역활을 할 수 있다고 위에 있다.

1
CreateIoCompletionPort((HANDLE)clientSock, m_hIOCP, (ULONG_PTR)user, 0);
cs

 

 첫번째 인자 FileHandle에 소켓을 넣고 기존에 생성한 IOCP 핸들을 넣는다.

그리고 세번째 인자는 키값으로 입출력 완료가 되었을때 어떤 소켓이 완료

되었는지 알 수 있다.

 

3. WorkerThread 생성

IOCP는 스레드가 사실 없어도 동작을 한다. 하지만 IOCP가 여러 스레드를

동시에 컨트롤 한다는 이점으로 성능을 위해서는 필수이다.

 

 쓰레드 갯수는 MSDN에서 CPU*2+1이란 숫자를 추천하는데,

이는 스레드가 Suspend되어 대기상태에 빠졌을때, 새로운 스레드를 꺼내기

위함이다라고 이해했다.

Iocp 기본 구조 이해 (slideshare.net)

 

Iocp 기본 구조 이해

IOCP IO Completion Port NHN NEXT 남현욱

www.slideshare.net

 

 

4. GetQueuedCompletionStatus()

 

완료큐에 완료된 입출력이 있다면 WorkerThread에서 작업을 하게 할 수 있다.

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
DWORD WINAPI WorkerThread(LPVOID param)
{
    KLobbyServer* pServer = (KLobbyServer*)param;
    DWORD dwTransfer;
    ULONG_PTR KeyValue;
    OVERLAPPED* pOverlapped;
    while (1)
    {
        //지정된 킬이벤트가 신호를 받았다면 break
        if (WaitForSingleObject(pServer->m_hKillEvent, 1== WAIT_OBJECT_0)
        {
            break;
        }
        //완료큐에 데이터가 있으면 작업시작
        BOOL bRet = ::GetQueuedCompletionStatus(
            pServer->m_hIOCP, &dwTransfer, &KeyValue, &pOverlapped, 1);
 
        KNetworkUser* pUser = (KNetworkUser*)KeyValue;
        KOV* pOV = (KOV*)pOverlapped;
 
        if (bRet == TRUE && pUser && pOV)
        {
            //작업
            if (pOV->type == 1000 && dwTransfer == 0)
            {
                if(pUser->m_bConnect == true)
                pUser->m_bConnect = false;
            }
            else if(pOV->type == 1000)
            {
                pUser->Dispatch(dwTransfer, pOV);
            }
        }
        else
        {
            //오류
            if (GetLastError() != WAIT_TIMEOUT)
            {
                ::SetEvent(pServer->m_hKillEvent);
                break;
            }
        }
    }
    return TRUE;
}
cs

 

//로비서버 IOCP 적용 예시 

더보기
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
#include "KLobbyServer.h"
DWORD WINAPI WorkerThread(LPVOID param)
{
    KLobbyServer* pServer = (KLobbyServer*)param;
    DWORD dwTransfer;
    ULONG_PTR KeyValue;
    OVERLAPPED* pOverlapped;
    while (1)
    {
        //지정된 킬이벤트가 신호를 받았다면 break
        if (WaitForSingleObject(pServer->m_hKillEvent, 1== WAIT_OBJECT_0)
        {
            break;
        }
        //완료큐에 데이터가 있으면 작업시작
        BOOL bRet = ::GetQueuedCompletionStatus(
            pServer->m_hIOCP, &dwTransfer, &KeyValue, &pOverlapped, 1);
 
        KNetworkUser* pUser = (KNetworkUser*)KeyValue;
        KOV* pOV = (KOV*)pOverlapped;
 
        if (bRet == TRUE && pUser && pOV)
        {
            //작업
            if (pOV->type == 1000 && dwTransfer == 0)
            {
                if(pUser->m_bConnect == true)
                pUser->m_bConnect = false;
            }
            else if(pOV->type == 1000)
            {
                pUser->Dispatch(dwTransfer, pOV);
            }
        }
        else
        {
            //오류
            if (GetLastError() != WAIT_TIMEOUT)
            {
                ::SetEvent(pServer->m_hKillEvent);
                break;
            }
        }
    }
    return TRUE;
}
 
bool KLobbyServer::Init(int port)
{
    KServer::Init(port);
    m_hIOCP = CreateIoCompletionPort(
            INVALID_HANDLE_VALUE, 000);
 
    SYSTEM_INFO system_info;
 
    GetSystemInfo(&system_info);
 
    //쓰레드 여러개 생성
    for (int i = 0; i < system_info.dwNumberOfProcessors *2; i++)
    {
        DWORD id;
        //자기 자신의 서버 인자를 넘김. this 
        m_hWorkThread[i] = CreateThread(00, WorkerThread, this0&id);
    }
 
    return true;
}
 
//게임서버는 new delete를 막아 
//동적할당을 미리 메모리 메니져로 만들어서 해야함
bool KLobbyServer::AddUser(SOCKET clientSock, SOCKADDR_IN clientAddr)
{
    KNetworkUser* user = new KNetworkUser();
    user->Set(clientSock, clientAddr);
 
    u_long on = 1;
    ioctlsocket(clientSock, FIONBIO, &on);
 
    //유저리스트 추가
    EnterCriticalSection(&m_cs);
        m_UserList.push_back(user);
    LeaveCriticalSection(&m_cs);
 
    //비동기 작업을 해야하니까 유저가 접속이되면 리시브를 걸어놔라
    //유저에 대한 포인터를 넘긴다.
    ::CreateIoCompletionPort((HANDLE)clientSock, m_hIOCP, (ULONG_PTR)user, 0);
    //WSARecv를 건다. 
    user->Recv();
 
    //delete user;
    return true;
}
 
bool KLobbyServer::Run()
{
    while (1)
    {
        //임계구역
        EnterCriticalSection(&m_cs);
 
        //패킷 타입을 판별해서 해당 맞는 작업을 하면 됨
        std::list<KPacket>::iterator iter_packet;
        for (iter_packet = m_lPacketPool.begin();
            iter_packet != m_lPacketPool.end();)
        {
            switch ((*iter_packet).m_uPacket.ph.type)
            {
                case PACKET_USER_POSITION:
                {
                    
                }break;
                case PACKET_CHAT_MSG:
                {
 
                }break;
            }
        }
 
 
        //주기적인 동기화
        for (KNetworkUser* user : m_UserList)
        {
            if (user->m_lPacketPool.size() > 0)
            {
                Broadcast(user);
            }
        }
        //커넥트가 false면 나가는 처리까지
        std::list<KNetworkUser*>::iterator user_iter;
        for (user_iter = m_UserList.begin();
            user_iter != m_UserList.end();)
        {
            if ((*user_iter)->m_bConnect == false)
            {
                (*user_iter)->Disconnect();
                delete (*user_iter);
                user_iter = m_UserList.erase(user_iter);
                std::cout <<"\nCurrent : " << m_UserList.size() <<" 명 접속중.."<<std::endl;
            }
            else
            {
                user_iter++;
            }
        }
        LeaveCriticalSection(&m_cs);
    }
    return true;
}
 
bool KLobbyServer::Release()
{
    CloseHandle(m_hIOCP);
    KServer::Release();
    return true;
}
 
cs

 

COMMENT
 
01
17

IMGUI 한글 깨짐

Directx 11에 툴링용으로 Imgui를 적용하였다.

하지만 Imgui는 기본적으로 멀티 바이트용으로 한글이 출력이 되지 않는다.

???로 출력이 된다.  깃헙에 제시된 여러 해결방법이 있지만,

나에게 제일 간단한 방법을 찾았다.

 

1. ImGuiIO 객체의 유니코드 한글 폰트를 추가한다.

 

 
ImGuiManager::ImGuiManager()
{
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsDark();
    ImGuiIO& io = ImGui::GetIO();
    io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\malgun.ttf"18.0f, NULL, io.Fonts->GetGlyphRangesKorean());
}
 
ImGuiManager::~ImGuiManager()
{
    ImGui::DestroyContext();
}
cs

 

ImGuiIO& io = ImGui::GetIO();
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\malgun.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesKorean());

 

 

굳이 ImGui 매니져를 사용하지 않더라도, 본인이 ImGui를 초기화하는 부분에서

호출해주면 된다. 나는 폰트는 제일 흔한 마이크로소프트 맑은 고딕을 사용했다.

io.Fonts->GetGlyphRangesKorean()로 한국어의 필요한 특정 그리프 범위를 지정해준다. 

 

2. (u8"한글 출력")

 

 
    #pragma region IMGUI INTERFACE
    if (ImGui::Begin("Chatting Box"))
    {
        
        ImGui::TextColored(color,isConnect.c_str());
        //연결이 안되어 있으면 연결 재 시도
        if (!m_Net.m_bConnect)
        {
            if (ImGui::Button(u8"연결 재시도"))
            {
                if (m_Net.Connect(g_hWnd, SOCK_STREAM, 10000, IP_DD))
                {
                    m_bConnect = true;
                }
            }
        }
        else
        {
            ImGui::BeginChild(u8"채팅창", ImVec2(0-ImGui::GetItemsLineHeightWithSpacing() - 15));
            ImGui::Text(chatItems);
            ImGui::EndChild();
            ImGui::Dummy(ImVec2(0.0f, 5));
            ImGui::InputText("", buffer, sizeof(buffer));
            ImGui::SameLine();
            if (ImGui::Button("Send"))
            {
                char clear[MAX_PATH] = { 0, };
                KPacket kPacket(PACKET_CHAT_MSG);
                kPacket << 123 << "Test" << (short)12 << buffer;
 
                //리턴 값이 0보다 작으면 전송되지 않았음
                if (m_Net.SendMsg(m_Net.m_Sock, kPacket.m_uPacket) < 0)
                {
                    ZeroMemory(&chatItems, sizeof(char* 2048);
                    strcat(chatItems, "Error\n");
                    m_Net.m_bConnect = false;
                }
 
                strcpy(buffer, clear);
            }
        }
    }
    ImGui::End();
#pragma endregion
cs

 

한글 부분에 u8를 붙여서 UTF-8 (유니코드) 글자임을 명시한다.

아마도 내부적으로 MultiBytetoWideChar() <멀티 바이트-> UTF-8>을 쓰고 있지 않을까 싶다.

이렇게 간단하게 imgui에서 한글 출력을 할 수가 있다. 

 

참고로, Win32 API로 

유니코드 -> 멀티바이트 : WideCharToMultiByte()

멀티 바이트 -> 유니코드 :  MultiBytetoWideChar()이다.

https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte

 

WideCharToMultiByte function (stringapiset.h) - Win32 apps

Maps a UTF-16 (wide character) string to a new character string.

docs.microsoft.com

 

3. 결과

 

 

 

 

COMMENT
 
01
14

C언어에서는 처리할 파일을 File 구조체와 

파일을 여는 fopen(경로 및 파일명, 모드)

파일을 읽고 쓰는 fread(), fwrite() 

파일을 닫는 fclose()등 파일 입출력을 했었다.

 

WinApi에서도 파일 입출력 함수가 있는데, 

CreateFile(), ReadFile(), WriteFile(), CloseHandle()

위와 같은 역활을 한다. 다만, 윈도우 운영체제와 밀접하고

비동기 파일 입출력을 지원한다는 큰 강점이 있다.

소켓을 붙이면 소켓 입출력이된다.

 

비동기 파일 입출력의 필요성

 

유튜브 영상을 볼때, 영상을 모두 로드하고 영상을 시청할려하면,

영상을 로드하는 동안은, 영상을 볼 수가 없다. 사용자는 버근가 싶을 것이다.

그래서 비동기 I/O 사용한다. 데이터 수신과 영상 출력이 동시에 일어난다.

WINAPI의 파일입출력은 이러한 비동기 파일 입출력을 지원한다. 

 

CreateFile() 함수 원형

1
2
3
4
5
6
7
8
9
HANDLE CreateFile(
 [in]           LPCWSTR               lpFileName, //만들거나 열 파일 또는 파일의 경로 및 이름
  [in]           DWORD                 dwDesiredAccess, //원하는 접근 모드,  GENERIC_READ , GENERIC_WRITE
  [in]           DWORD                 dwShareMode, //다른 프로세스가 파일 또는 장치를 연 상태에서 공유 가능하게 할 것인지
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes, //파일의 보안속성 지정함, 자식한테 상속 가능, 옵션값임
  [in]           DWORD                 dwCreationDisposition, //생성 관련 속성, 덮어쓰기나 새로만들기 등
  [in]           DWORD                 dwFlagsAndAttributes, //장치 속성 플래그, 읽기전용, 숨김파일, 비동기 I/O등 플래그 세움
  [in, optional] HANDLE                hTemplateFile //템플릿으로 동일한 특성 새파일 만들고 싶으면 쓰임, 일반적으로 NULL
);
cs

 

WriteFile() 함수 원형

1
2
3
4
5
6
7
8
BOOL WriteFile(
  [in]                HANDLE       hFile, //CreateFile에서 할당 해 줬던 핸들을 입력 해 준다.
  [in]                LPCVOID      lpBuffer, // write할 데이터가 들어 있는 버퍼 포인터 
  [in]                DWORD        nNumberOfBytesToWrite, // write할 데이터의 사이즈
  [out, optional]     LPDWORD      lpNumberOfBytesWritten, // 작성된 바이트 수를 수신하는 변수,
                                                           // 비동기에서 잠재적으로 잘못될수 있어 Null로 설정
  [in, out, optional] LPOVERLAPPED lpOverlapped // 비동기 Overlapped 구조 포인터 자리
);
cs

 

ReadFile() 함수 원형

1
2
3
4
5
6
7
BOOL ReadFile(
  [in]                HANDLE       hFile, // 읽고자 하는 파일의 핸들 
  [out]               LPVOID       lpBuffer, // 읽어올 저장 버퍼의 포인터
  [in]                DWORD        nNumberOfBytesToRead, // 실제 읽어오는 바이트 수
  [out, optional]     LPDWORD      lpNumberOfBytesRead, // 얼마나 읽고 쓰여졌는가 결과값이 저장되는 주소값
  [in, out, optional] LPOVERLAPPED lpOverlapped // 비동기 Overlapped 구조 포인터 자리
);
cs

 

파일에 읽고 쓸 파일 HANDLE을 만들어서 함수처리하면 된다.  

주요 함수에 대해서 알았으니 파일 입출력을 해보자. 

 

동기 파일 입출력 예시

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
int main()
{
    setlocale(LC_ALL, ""); // Korean해도 되지만 공백은 시스템 지역설정으로 따르게 한다.
    HANDLE hFile = CreateFile(L"test.txt", GENERIC_WRITE
        , 0NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    //CREATE_ALWAYS : 파일 덮어씀
    if (hFile != NULL)
    {
        CHAR buffer[] = "유니코드든 싱글바이트든 해석할때가 중요하다.";
        DWORD dLength = sizeof(buffer);
        DWORD dWritten;
        bool bRet = WriteFile(hFile, buffer, dLength, &dWritten, NULL);
 
        if (bRet == true)
        {
            std::wcout << L"출력성공" << std::endl;
        }
    }
    CloseHandle(hFile);
 
 
    HANDLE hFile_Read = CreateFile(L"test.txt", GENERIC_READ
        , 0NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile_Read != NULL)
    {
        WCHAR buffer[512= { 0, };
        DWORD dLength = sizeof(buffer);
        DWORD dWritten;
        bool bRet = ReadFile(hFile_Read, buffer, dLength, &dWritten, NULL);
 
        if (bRet == true)
        {
            std::wcout << L"읽어오기 성공" << std::endl;
        }
    }
    CloseHandle(hFile_Read);
}
cs

 

1. 쓰기용 핸들을 만든다. CreateFile()

 

2. 쓰기용 핸들에 파일에 쓴다. 버퍼를 넣는다. WriteFile()

 

3. 핸들을 닫는다. 읽기 ReadFile()도 유사하다.

 

4. 결과 

 

이러한 ReadFile(), WriteFile() 입출력 함수는 일반적으로

입출력이 끝날때까지 Blocked 상태를 유지한다. 

 

만약 만화책을 보고 그림을 따라 그린다고 상상을 해보자.

만화책 한번보고, 기억하고, 도화지에 그림을 그리고

다시 만화책 한번보고(Blocked) 기억하고(CPU 사용 코드) 도화지에 그림을 그리고(Blocked)....

반복하는 작업이 있다고 생각해보자.

 

 

도화지에 만화책을 중첩시켜 그림을 그리는 작업은 어떨까!

그게 바로 Overlapped(중첩) 입출력이다. 훨씬 속도가 빠르다.

스레드를 쓰지 않고도 스레드를 쓰는 효과가 난다.

 

비동기 파일 입출력 Overlapped 입출력 예시

 

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
#define BlockSizes 2048
#include <windows.h>
#include <iostream>
 
void main()
{
    setlocale(LC_ALL, "");
    WCHAR* pwbuffer = NULL;
    LARGE_INTEGER fileSize;
    OVERLAPPED ol_Read = { 0, };
    OVERLAPPED ol_Write = { 0, };
 
    //핸들 반환 FILE_FLAG_OVERLAPPED으로 비동기 파일 입출력 
    //기존에 있는 파일을 읽어옴
    HANDLE hFile = CreateFile(L"test.JPG", GENERIC_READ | GENERIC_WRITE, 0,
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
    //실패하면 초기값이 잡힘
    if (hFile == INVALID_HANDLE_VALUE)
    {
        CloseHandle(hFile);
        return;
    }
    // 핸들 반환 FILE_FLAG_OVERLAPPED으로 비동기 파일 입출력
    // 복사하는 핸들
    HANDLE hFileCopy = CreateFile(L"test_copy.JPG", GENERIC_WRITE, 0
        NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
    //실패하면 초기값이 잡힘
    if (hFileCopy == INVALID_HANDLE_VALUE)
    {
        CloseHandle(hFileCopy);
        return;
    }
 
    //읽어온 파일 사이즈를 체크
    GetFileSizeEx(hFile, &fileSize);
 
    DWORD dwRead;
    DWORD dwWritten;
    DWORD dwTotalWrite = 0;
 
    //sector의 크기 배수 일반적으로 512
    pwbuffer = new WCHAR[BlockSizes];
    int i = 0;
    while(1)
    {
        //Temp 버퍼
        ZeroMemory(pwbuffer, BlockSizes);
        
        //비동기 로드
        //Overlapped 구조체 오프셋
        ol_Read.Offset = i * BlockSizes;
        ol_Read.OffsetHigh = 0;
 
        BOOL ret = ReadFile(hFile, pwbuffer, BlockSizes, &dwRead, &ol_Read);
        BOOL bPending = FALSE;
        if (ret == FALSE)
        {
            DWORD dwError = GetLastError();
            if (dwError == ERROR_IO_PENDING)
            {
                std::wcout << L"읽기 중" << std::endl;
            }
            else
            {
 
            }
        }
        BOOL bReturn = GetOverlappedResult(hFile, &ol_Read, &dwRead, FALSE);
        if (bReturn == TRUE)
        {
            std::wcout << L"읽기 완료" << std::endl;
            bPending = FALSE;
        }
        else
        {
            DWORD dwError = GetLastError();
            if (dwError == ERROR_IO_INCOMPLETE)
            {
                std::wcout << L".." << std::endl;
            }
            else
            {
                std::wcout << L"읽기 완료2" << std::endl;
                bPending = FALSE;
            }
        }
        //비동기 출력
        //Overlapped 구조체 오프셋
        ol_Write.Offset = i * BlockSizes;
        ol_Write.OffsetHigh = 0;
 
        ret = WriteFile(hFileCopy, pwbuffer, dwRead, &dwWritten, &ol_Write);
        if (ret == FALSE)
        {
            DWORD dwError = GetLastError();
            if (dwError == ERROR_IO_PENDING)
            {
                std::wcout << L"쓰기 중" << std::endl;
            }
            else
            {
 
            }
        }
        bReturn = GetOverlappedResult(hFileCopy, &ol_Write, &dwWritten, FALSE);
        if (bReturn == TRUE)
        {
            std::wcout << L"쓰기 완료" << std::endl;
            bPending = FALSE;
        }
        else
        {
            DWORD dwError = GetLastError();
            if (dwError == ERROR_IO_INCOMPLETE)
            {
                std::wcout << L"..";
            }
            else
            {
                std::wcout << L"쓰기 완료2" << std::endl;
                bPending = FALSE;
            }
        }
        dwTotalWrite += dwWritten;
 
        if (fileSize.LowPart == dwTotalWrite)
        {
            break;
        }
        std::wcout << (fileSize.LowPart- dwTotalWrite) << std::endl;
        i++;
    }
    CloseHandle(hFile);
    CloseHandle(hFileCopy);
}
 
cs

 

위 코드는 블록단위로 파일을 복사하는 작업을 비동기로 처리한다. 

블록단위로 읽고 복사(쓰고)를 반복한다.. 다 복사가 될때까지. 

Offset을 이용해, 블록단위로 끊어 읽고, dwRead 읽은 만큼만 Write하는 구조이다.

 

 

Overlapped 구조체를 이용하여 비동기적으로 하나 이상의 I/O 작업을 수행

할 수 있다. Overlapped 구조체 내용은 파일 포인터의 Offset과 이벤트가 있다.

 

 

오버랩 플래그를 달아준다. FILE_FLAG_OVERLAPPED

 

 

WriteFIle, ReadFile의 마지막 옵션값, 오버랩 구조체 자리에 구조체를 넣고

GetOverlappedResult의 결과를 받는다. 

 

1
2
3
4
5
6
BOOL GetOverlappedResult(
  [in]  HANDLE       hFile, // 파일 핸들
  [in]  LPOVERLAPPED lpOverlapped, // Overlapped 구조체 
  [out] LPDWORD      lpNumberOfBytesTransferred, // 전송 양
  [in]  BOOL         bWait // 기다릴지의 유무
);
cs

 

GetOverlappedResult의 마지막 인자의 이름은 bWait로 

True이면 동기작업을 하게 되고 FALSE경우 비동기 작업이된다.

비동기처리니까 당연히 예외처리를 해줘야한다. 

 

결과

 

COMMENT
 
01
11

https://dlemrcnd.tistory.com/58?category=528341 

 

Network Programming - 쓰레드(Thread)와 프로세스(Process) , 클라이언트/서버 멀티쓰레드 C++ 채팅 프로그

프로세스(Process) 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램이다. Code, Data, Stack, Heap의 구조로 되어 있는 독립된 메모리 영역이다. Code 텍스트, 코드영역 Data 전역변수, 정적변수, 배

dlemrcnd.tistory.com

 

동기화 문제

스레드 A와 스레드 B로 왔다 갔다(스위칭)하면서 경쟁하는데

둘의 조건이 안맞아 스레드끼리 서로 대기하는 교착상태(DeadLock)이 발생한다.

 

교착상태 : 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에

아무것도 완료되지 못하는 상태를 의미함,

 

위키피디아에서는 예시로 사다리에서 한 사람은 올라가려고 하고

한 사람은 내려갈라고 하는데, 서로 비켜줄 때까지 기다리는 걸로

예시를 들었다.

 

실행 순서를 제어해야 하는데, 이런 기술들을 동기화(Synchronization)라고 한다. 

 

동기화 기법의 분류

 

유저 모드는 사용자, 즉 프로그래머의 처리이다. 외부에서 관여하지 못한다.

장점으로는 프로그래밍하기 쉽고, 속도가 빠르다. 단점으로는 제한된 기능을 갖는다.

 

커널 모드는 프로세스 관리하는 운영체제가 관여해 외부 프로그램이 접근 가능하다. 

장점으로는 Deadlock 문제를 좀 더 명확히 막고, 둘 이상의 프로세스 간의 존재하는

스레드 간의 동기화도 가능하다. 운영체제가 관여하기 때문이다. 

단점으로는 실행 속도의 저하가 발생한다

 

분류 종류
유저 모드 크리티컬 섹션, 인터락 함수
커널 모드 뮤텍스(Mutex), 세마포어(Semaphore), 이벤트 (Event)

유저 모드 - 크리티컬 섹션

내부적으로 인터락 함수를 갖고 있다고 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InitializeCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);
 
// lpCriticalSection : 초기화할 크리티컬 섹션 변수의 포인터를 전달
// 크리티컬 섹션 초기화
 
void EnterCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);
// 임계영역에 들어간다. 다른 스레드는 동일 리소스 접근 못함
 
void LeaveCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);
// 임계영역에서 떠남 . leave하지 않으면 영원히 데드락임
// 대기중인 스레드가 작업이 안끝났구나 해서 무한 대기함
 
void DeleteCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);
 
// 크리티컬 섹션 해제, 
cs

Enter~ Leave로 감싸면 크리티컬 한 보호 상태가 된다. 

 

유저 모드 - 인터락(Interlocked)

멀티스레드에서 안전하게 변숫값을 조작 하는 함수를 인터락이라한다.

예를 들어 ++-- 증감 연산자는 개발자 입장에서 한 줄이지만,

기계가 해석하는 어셈블리어에서는 한 줄이 아니다. 그 사이에서 스위칭이 발생함.

그래서 별도의 증가함수가 필요하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LONG InterlockedIncrement(
  LONG volatile *Addend
);
 
LONG InterlockedDecrement(
  LONG volatile *Addend
);
 
LONG InterlockedAdd(
  LONG volatile *Addend,
  LONG          Value
);
 
LONG InterlockedExchange(
  LONG volatile *Target,
  LONG          Value
);
 
LONG InterlockedCompareExchange(
  LONG volatile *Destination,
  LONG          ExChange,
  LONG          Comperand
);
cs

 


 

커널 모드 - 뮤텍스 (Mutex)

뮤 텍스는 Mutual Exclusion의 줄임말. 상호 배제라는 뜻이다.

상호 배제는 두 스레드가 동시에 소유할 수 없다는 것을 의미한다.

 

뮤텍스는 신호상태(signaled)와 비신호상태(non-signaled)를 띤다.

신호상태는 스레드의 실행을 허가하고 비신호는 허가하지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//뮤텍스 생성함수 
HANDLE CreateMutex(
  [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes, // 보안 관련 특성 전달 (NULL)
  [in]           BOOL                  bInitialOwner, // 초기 소유된 스레드 (바로 Signaled)
  [in, optional] LPCSTR                lpName        // 객체 이름
);
 
//뮤텍스 반환 함수 Mutex 상태를 non-signaled -> signaled 로 바꿈
BOOL ReleaseMutex(
  [in] HANDLE hMutex
);
 
//뮤텍스 소멸 함수
BOOL CloseHandle(
  [in] HANDLE hObject
);
 
cs

 

대기 함수

스레드의 실행을 블록 하여 대기시킨다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
// 하나의 동기화 객체가 신호상태가 되기를 기다린다.
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds // INFINITE로 하면 무한정 기다림
);
 
//복수의 동기화 객체가 신호상태가 되길 기다림
DWORD WaitForMultipleObjects(
  [in] DWORD        nCount,
  [in] const HANDLE *lpHandles,
  [in] BOOL         bWaitAll,
  [in] DWORD        dwMilliseconds
);
cs

 

뮤텍스 예시

서버에서 유저 리스트를 업데이트할 때, 다른 스레드에서 대기 함수로 대기하는 경우이다.

 

핸들 선언

 

뮤텍스 핸들을 선언한다. 

 

뮤텍스 생성

 

두개의 스레드 생성

 

두 개의 스레드에서 전역 변수를 안전하게 접근하기 위해 뮤텍스를 사용한다.

고로 , 스레드를 생성한다.

 

아래는 스레드 시작 함수들의 내용이다. 

 

리시브 스레드
전송 스레드

 

대기 함수로 뮤텍스 핸들을 넣어 ReleaseMutex가 일어날 때까지

다른 스레드는 대기한다. 그러면, 각 스레드는 컨텍스트 스위칭을

정신없이 하는 와중에도 유저 리스트(전역 변수)에 접근할 때는

unsignaled이 돼서, 같은 변수를 접근하려는 다른 스레드는 대기를 한다. 

 

Sample.cpp

더보기
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
#include "Sample.h"
 
//서버
CRITICAL_SECTION        g_CS;
HANDLE                    g_hMutex;
std::list<KNetworkUser> g_UserList;
KNetwork                g_Net;
 
int BroadCast(KNetworkUser& user)
{
    if (user.m_lPacketPool.size() > 0)
    {
        std::list<KPacket>::iterator iter;
        for (iter = user.m_lPacketPool.begin();
            iter != user.m_lPacketPool.end();)
        {
            for (KNetworkUser& senduser : g_UserList)
            {
                int iRet = g_Net.SendMsg(senduser.m_Sock, (*iter).m_uPacket);
                if (iRet <= 0)
                {
                    senduser.m_bConnect = false;
                }
            }
            iter = user.m_lPacketPool.erase(iter);
        }
    }
    return 1;
}
DWORD WINAPI RecvThread(LPVOID param)
{
    SOCKET sock = (SOCKET)param;
    while (1)
    {
        //스레드가 특정 시그널이 발생할때까지 기다림
        WaitForSingleObject(g_hMutex, INFINITE);
 
        std::list<KNetworkUser>::iterator iter;
        for (iter = g_UserList.begin();
            iter != g_UserList.end();)
        {
            int iRet = g_Net.RecvUser(*iter);
            //0보다 작거나 같음, 받아온 데이터가 없거나, 에러일 경우 
            if (iRet <= 0)
            {
                iter = g_UserList.erase(iter);
            }
            else
            {
                iter++;
            }
        }
        //운영체제한테 제어권 넘겨줌
        ReleaseMutex(g_hMutex);
        //의도적으로 Context Switching
        Sleep(1);
    }
}
 
DWORD WINAPI SendThread(LPVOID param)
{
    SOCKET sock = (SOCKET)param;
    while (1)
    {
        //스레드가 특정 시그널이 발생할때까지 기다림
        WaitForSingleObject(g_hMutex, INFINITE);
 
        std::list<KNetworkUser>::iterator iter;
        for (iter = g_UserList.begin();
            iter != g_UserList.end();)
        {
            //전체에게 보내줌
            int iRet = BroadCast(*iter);
            //0보다 작거나 같음, 받아온 데이터가 없거나, 에러일 경우 
            if (iRet <= 0)
            {
                iter = g_UserList.erase(iter);
            }
            else
            {
                iter++;
            }
        }
        //운영체제한테 제어권 넘겨줌
        ReleaseMutex(g_hMutex);
        //의도적으로 Context Switching
        Sleep(1);
    }
}
 
void main()
{
    //유저모드 동기화 모드 크리티컬섹션
    //InitializeCriticalSection(&g_CS);
    g_hMutex = CreateMutex(NULL, FALSE, NULL);
    g_Net.InitNetwork();
    g_Net.InitServer(SOCK_STREAM,10000, nullptr);
 
    SOCKADDR_IN clientAddr;
    int iLen = sizeof(clientAddr);
 
    std::cout<< "Server Start." << std::endl;
 
    //non blocking socket 0이면 블락킹 소켓
    u_long on = 1;
    ioctlsocket(g_Net.m_Sock, FIONBIO, &on);
 
    DWORD ThreadID_Recv;
    //데이터 받는 스레드
    HANDLE hThreadRecv = ::CreateThread(
        0,
        0,
        RecvThread, // 시작함수를 지정
        (LPVOID)g_Net.m_Sock, // 시작함수 인자값
        0// 바로 시작할것인지 플래그
        &ThreadID_Recv // 스레드 아이디 반환
    );
    CloseHandle(hThreadRecv);
    
    DWORD ThreadID_Send;
    HANDLE hThreadSend = ::CreateThread(
        0,
        0,
        SendThread,
        (LPVOID)g_Net.m_Sock,
        0,
        &ThreadID_Send
    );
    CloseHandle(hThreadSend);
 
    //메인 스레드
    while (1)
    {
        SOCKET clientSock = accept(g_Net.m_Sock,
            (sockaddr*)&clientAddr, &iLen);
 
        if (clientSock == SOCKET_ERROR)
        {
            int iError = WSAGetLastError();
            if (iError != WSAEWOULDBLOCK)
            {
                std::cout << "ErrorCode=" << iError << std::endl;
                break;
            }
        }
        //클라이언트가 접속 시 시작함
        else
        {
            KNetworkUser user;
            user.set(clientSock, clientAddr);
            //메인 스레드가 특정 시그널이 발생할때까지 기다림
            WaitForSingleObject(g_hMutex, INFINITE);
            //들어온 유저 전역 리스트에 추가
            g_UserList.push_back(user);
            ReleaseMutex(g_hMutex);
            std::cout
                << "ip =" << inet_ntoa(clientAddr.sin_addr)
                << "port =" << ntohs(clientAddr.sin_port)
                << "  " << std::endl;
            u_long on = 1;
            ioctlsocket(clientSock, FIONBIO, &on);
            std::cout << std::to_string(g_UserList.size())<< " 명 접속중." << std::endl;
        }
        Sleep(1);
    }
    closesocket(g_Net.m_Sock);
    WSACleanup();
 
    CloseHandle(g_hMutex);
}
cs

 

 

 

결과

 

세마포어(Semaphore)

세마포어는 뮤텍스와 유사하지만 카운팅이 가능한 함수로

세마포어가 0이 되어야 non-signaled이 된다. 

예를 들어 크롬에서 다운로드를 할 때 3개를 동시에 다운로드 중이라면

4번째부터는 대기 상태에 들어가는 메커니즘과 유사하다.

 

이벤트 (Event)

어떤 사건이 일어났음을 알리는 동기화 객체이다.

윈도우의 메시지와 유사한 기능으로 자동리셋 이벤트와 수동 리셋 이벤트가 있다.

자동은 대기하던 스레드가 해제되면 자동으로 리셋이 되고, 수동은 자동으로 리셋이 안된다.

프로그래밍 적으로 해줘야 함. 

COMMENT
 
01
10
COMMENT
 
01
10

WSAAsyncSelect

 

윈도우(창)에서 사용하는 기본 모델이다.

Async은 비동기적 이라는 뜻이 있다.

동기적 서로 다른 주체가 하는 작업이 작업 시작, 종료시간이 연관있을때 작업 지시가 주체
비동기적 서로 다른 주체가 하는 작업이 작업 시작, 종료시간과는 관계가 없을 때
블록킹 하나의 작업이 흐름을 막는것 작업 처리가 주체
논블록킹 하나의 작업이 흐름을 막지 않는것

 

윈속의 소켓 이벤트를 윈도우 메시지를 통해서 비동기적으로 받는다.

윈도우 메세지를 통해 성공 여부를 판단해야한다. 메세지 큐에 의존하여

소켓의 처리가 이루워져 많은 수의 소켓 이벤트를 처리해야하는 경우

큐가 고갈될 수 있다. 그래서 클라이언트의 모델에 적합하다.

 

1
2
3
4
5
int WSAAsyncSelect(
            SOCKET socket,// 입출력 상황을 체크할 소켓
            HWND   hWnd, // 메시지 통지를 받을 윈도우 핸들
            unsigned int wMsg, // 이벤트가 발생하면 보낼 메시지
            long IEvent); // 통지받을 네트워크 이벤트 비트마스크 
cs

 

3번째의 인자가 이벤트가 발생하면 보낼 메세지라 되어있는데,

메세지는 윈도우에서 사용하는 메시지뿐만 아니라 사용자가 직접 등록할 수 있다.

[WM_USER + 숫자] 형식으로 윈도우 메시지와 중복되는 경우를 피할 수 있다.

 

위는 예시로 사용자 정의한 메시지와 원하는 이벤트를 기입한 코드이다.

 

네트워크 이벤트 

 

FD_WRITE

WSAConnect 함수 호출하고 소켓이 처음 연결했을떄,

WSAAccept 함수 호출하고 소켓의 연결이 수락되었을때,

Send, WSASend, send to, WSASendTO 함수 호출후 WSAEWOULDBLOCK이 리턴되서

전송버퍼가 비워졌을때 이벤트가 발생한다.

즉, 보낼수 있는 상태가 되면 이벤트가 발생한다고 생각할 수 있다. 

 

FD_ACCEPT

클라이언트가 접속했을떄

 

FD_READ

수신 가능할때

 

FD_CLOSE

접속 종료할때

 

FD_CONNECT

접속 완료될때

 

FD_OBB

데이터가 도착할때

 

재정의한 윈도우 메시지 프로시저

 

이런 식으로 구성하였다. 연결 상태를 체크하고

수신 가능하다면 연결된 socket으로부터 데이터를 수신한다. recv()함수

 

코드

KAsyncSelect.cpp

더보기
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
#include "KAsyncSelect.h"
//재정의 윈도우 메세지 프로시저
LRESULT  KAsyncSelect::Select_MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case NETWORK_MSG:
    {
        WORD wRet = WSAGETSELECTEVENT(lParam);
        switch (wRet)
        {
        case FD_CONNECT:
        {
            m_bConnect = true;
        }break;
        case FD_CLOSE:
        {
            m_bConnect = false;
        }break;
        case FD_READ:
        {
            RecvUser(m_User);
        }break;
        case FD_WRITE:
        {
            m_bConnect = false;
        }break;
        }
    }break;
    }
    return 0;
}
bool KAsyncSelect::Connect(HWND hWnd, int type, int iport, const char* ip)
{
    //소켓 구조체 채우기
    m_Sock = socket(AF_INET, type, 0);
    SOCKADDR_IN sa;
    ZeroMemory(&sa, sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(iport);
    sa.sin_addr.s_addr = inet_addr(ip);
    m_User.m_Sock = m_Sock;
 
    //윈도우 메세지를 통해서 비동기적으로 소켓 활용 가능
    if (WSAAsyncSelect(m_Sock, hWnd, NETWORK_MSG,
        FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
    {
        return false;
    }
    // 블록킹 연결시도 오류가 발생하지 않으면 0을 반환,  다른 소켓 어플리케이션에 연결
    int iRet = WSAConnect(m_Sock, (sockaddr*)&sa, sizeof(sa), NULLNULLNULLNULL);
    if (iRet == SOCKET_ERROR)
    {
        return false;
    }
    return true;
}
cs

KNetwork.cpp

더보기
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
#include "KNetwork.h"
 
bool KNetwork::InitNetwork()
{
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(22), &wsa) != 0)
    {
        return false;
    }
    return true;
}
 
bool KNetwork::InitServer(int protocol, int iport, const char* ip)
{
    m_Sock = socket(AF_INET, SOCK_STREAM, 0);
    SOCKADDR_IN sa;
    ZeroMemory(&sa, sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(iport);
    if (ip == nullptr)
    {
        sa.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    else
    {
        sa.sin_addr.s_addr = inet_addr(ip);
    }
 
    //소켓에 주소 할당
    int iRet = bind(m_Sock, (sockaddr*)&sa, sizeof(sa));
    if (iRet == SOCKET_ERROR)  return false;
 
    //클라이언트로 부터 연결 요청을 처리할수있는 상태를 만듬 
    iRet = listen(m_Sock, SOMAXCONN);
    if (iRet == SOCKET_ERROR)  return false;
    return true;
}
int KNetwork::SendMsg(SOCKET sock, char* msg, WORD type)
{
    //패킷 생성
    UPACKET    packet;
    ZeroMemory(&packet, sizeof(packet));
    packet.ph.len = strlen(msg) + PACKET_HEADER_SIZE; //ph가 헤더에는 크기와 타입을 알려줌
    packet.ph.type = type;
    memcpy(packet.msg, msg, strlen(msg));
    //운영체제 sendbuffer에 패킷 전송
    char* pMsg = (char*)&packet;
    int iSize = 0;
    do {
        //send함수
        int iSendByte = 0;
            iSendByte= send(sock, &pMsg[iSize],
            packet.ph.len - iSendByte, 0);
 
        //논블럭킹이 아니여서 이거 해줄 필요 없음
        if (iSendByte == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSAEWOULDBLOCK)
            {
                return -1;
            }
        }
        iSize += iSendByte;
    } while (iSize < packet.ph.len);
    return iSize;
}
int KNetwork::SendMsg(SOCKET sock, UPACKET& packet)
{
    char* pMsg = (char*)&packet;
    int iSize = 0;
    do {
        int iSendByte = send(sock, &pMsg[iSize],
            packet.ph.len - iSize, 0);
        if (iSendByte == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSAEWOULDBLOCK)
            {
                return -1;
            }
        }
        iSize += iSendByte;
    } while (iSize < packet.ph.len);
    //크기만큼 보내지게 반복문
    return iSize;
}
//int KNetwork::AddUser(SOCKET sock)
//{
//    SOCKADDR_IN clientAddr;
//    int iLen = sizeof(clientAddr);
//    SOCKET clientSock = accept(sock,
//        (sockaddr*)&clientAddr, &iLen);
//    if (clientSock == SOCKET_ERROR)
//    {
//        return -1;
//    }
//    else
//    {
//        KNetworkUser user;
//        user.set(clientSock, clientAddr);
//        userlist.push_back(user);
//        std::cout
//            << "ip =" << inet_ntoa(clientAddr.sin_addr)
//            << "port =" << ntohs(clientAddr.sin_port)
//            << "  " << std::endl;
//        std::cout << userlist.size() << " 명 접속중.." << std::endl;
//    }
//    return 1;
//}
int KNetwork::RecvUser(KNetworkUser& user)
{
    char szRecvBuffer[1024= { 0, };
    //recv 받기 연결된 socket으로부터 데이터를 수신합니다.
    int iRecvByte = recv(user.m_Sock, szRecvBuffer, 10240);
    if (iRecvByte == 0)
    {
        return 0;
    }
    if (iRecvByte == SOCKET_ERROR)
    {
        return -1;
    }
    user.DispatchRead(szRecvBuffer, iRecvByte);
    return 1;
}
bool KNetwork::CloseNetwork()
{
    closesocket(m_Sock);
    WSACleanup();
    return true;
}
 
cs

KNetworkUser.cpp

더보기
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
#include "KNetworkUser.h"
 
int KNetworkUser::DispatchRead(char* sRecvBuffer, int iRecvByte)
{
    //m_szRecvBuffer 2048을 넘으면 메모리 위치 바꿈, 초기화
    if (m_iWritePos + iRecvByte >= 2048)
    {
        if (m_iReadPos > 0)
        {
            memmove(&m_szRecvBuffer[0], &m_szRecvBuffer[m_iPacketPos], m_iReadPos);
        }
        m_iPacketPos = 0;
        m_iWritePos = m_iReadPos;
    }
 
    memcpy(&m_szRecvBuffer[m_iWritePos], sRecvBuffer, iRecvByte);
    m_iWritePos += iRecvByte;// 버퍼에 이전에 저장된 위치
    m_iReadPos += iRecvByte; // 패킷시작 위치로부터 받은 바이트
    
    if (m_iReadPos >= PACKET_HEADER_SIZE)
    {
        // 패킷 해석 가능
        UPACKET* pPacket = (UPACKET*)&m_szRecvBuffer[m_iPacketPos];
        // 적어도 1개의 패킷이 도착했다면
        if (pPacket->ph.len <= m_iReadPos)
        {
            do {
                KPacket kPacket(pPacket->ph.type);
                memcpy(&kPacket.m_uPacket,
                    &m_szRecvBuffer[m_iPacketPos],
                    pPacket->ph.len);
                m_lPacketPool.push_back(kPacket);
 
                // 다음패킷 처리
                m_iPacketPos += pPacket->ph.len;
                m_iReadPos -= pPacket->ph.len;
                if (m_iReadPos < PACKET_HEADER_SIZE)
                {
                    break;
                }
                pPacket = (UPACKET*)&m_szRecvBuffer[m_iPacketPos];
            } while (pPacket->ph.len <= m_iReadPos);
        }
    }
    return 1;
}
 
void KNetworkUser::set(SOCKET sock, SOCKADDR_IN addr)
{
    m_bConnect = true;
    ZeroMemory(m_szRecvBuffer, sizeof(char* 2048);
    m_iPacketPos = 0;
    m_iWritePos = 0;
    m_iReadPos = 0;
 
    m_Sock = sock;
    m_Addr = addr;
    //네트워크 주소 변환 함수 빅엔디안(32bit) ->  
    m_csName = inet_ntoa(addr.sin_addr);
    //엔디안은 메모리 연속된 대상을 배열하는 방법으로 
    // Network Byte 순서를 To Host의 Byte 순서로 바꿈
    m_iPort = ntohs(addr.sin_port);
}
 
cs

 

결과

COMMENT
 
01
07

읽을 파일이 없는데, Read()함수를 호출하면 읽을 내용이 생길때까지

대기하는 블록킹 소켓과 블록킹이 되지 않아, 조건을 만족하지 않을때

오류를 내서 일일히 관리해주는 논블로킹소켓이 있다.

 

이러한 소켓모드와 관계없이 여러 소켓을 한 스레드로 처리할 수

있는게 Select 함수이다.

 

Select() 함수

작업할 준비가 된 파일에 대해서만 작업을 하는 함수이다.

 

1
2
3
4
5
6
7
int select(
    _In_ int nfds,
    _Inout_opt_ fd_set FAR * readfds,
    _Inout_opt_ fd_set FAR * writefds,
    _Inout_opt_ fd_set FAR * exceptfds,
    _In_opt_ const struct timeval FAR * timeout
    );
cs

 

nfds 유닉스와 호환성을 위해 존재함, 윈도우에서 사용 안함, 관리할 파일의 개수를 지정하는 것이였나봄
readfds 읽기 set, 블록되지 않고 읽기가 가능한지 감시
writefds 쓰기 set, 블록되지 않고 쓰기가 가능한지 감시
exceptfds 예외 set, 예외 발생 감시
timeout 타임 아웃 값, 정해진 시간이 경과할때까지 블록

 

작업 순서

 

1, Socket Set을 비운다. (readfds, writefds)

 

 

2. 소켓셋에 소켓을 넣는다.

 

 

3. Select() 함수를 호출한다. 

 

 

4. Select 함수 리턴 후 소켓 셋에 남아 있는 모든 소켓에 대해 

맞는 소켓함수를 호출하여 처리한다.

 

 

5. 1~4 번 반복하면 된다.


timeOut 값에 5초를 넣으니까 Select() 함수에서

진짜 5초 블럭킹이 된다.

 

FD_SET으로 소켓을 넣고나서 FD_ISSET에서 해당 소켓을

넣으면 비트가 일치하니 바로 호출되는 줄 알았는데

그건 또 아닌 것 같다. 입출력이 발생해야 if문에 걸린다.

 

굉장히 편하지만, 대규모 서버에서는 적합하지 않다고 한다.

 

전체 코드

더보기
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
#include "KNetwork.h"
 
//서버 Select 모델
std::list<KNetworkUser> userlist;
int SendMsg(SOCKET sock, char* msg, WORD type)
{
    //1. 패킷을 객체 생성
    UPACKET packet;
    ZeroMemory(&packet, sizeof(packet));
    //패킷의 길이는 헤더 사이즈 + 메세지 사이즈
    packet.ph.len = strlen(msg) + PACKET_HEADER_SIZE;
    packet.ph.type = type;
    //메세지 받은걸로 memcpy로 패킷 만듬
    memcpy(packet.msg, msg, strlen(msg));
 
    // 2번 패킷 전송 : 운영체제 sendbuffer(short바이트)
    char* pMsg = (char*)&packet;
    int iSize = 0;
 
    do {
        //TCP는 연속적이지만 바이트 단위로 나눠져 보내질도 있음
        int iSendByte = send(sock, &pMsg[iSize],
            packet.ph.len - iSize, 0);
 
        if (iSendByte == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSAEWOULDBLOCK)
            {
                return -1;
            }
        }
 
        iSize += iSendByte;
    } while (iSize < packet.ph.len);
    return iSize;
}
//패킷을 받았을때.
int SendMsg(SOCKET sock, UPACKET& packet)
{
    char* pMsg = (char*)&packet;
    int iSize = 0;
    do {
        int iSendByte = send(sock, &pMsg[iSize],
            packet.ph.len - iSize, 0);
 
        if (iSendByte == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSAEWOULDBLOCK)
            {
                return -1;
            }
        }
 
        iSize += iSendByte;
    } while (iSize < packet.ph.len);
    return iSize;
}
int AddUser(SOCKET sock)
{
    SOCKADDR_IN clientAddr;
    int iLen = sizeof(clientAddr);
    SOCKET clientSock = accept(sock,
        (sockaddr*)&clientAddr, &iLen);
    if (clientSock == SOCKET_ERROR)
    {
        return -1;
    }
    else
    {
        KNetworkUser user;
        user.set(clientSock, clientAddr);
        userlist.push_back(user);
        std::cout
            << "ip =" << inet_ntoa(clientAddr.sin_addr)
            << "port =" << ntohs(clientAddr.sin_port)
            << "  " << std::endl;
        std::cout << userlist.size() << " 명 접속중.." << std::endl;
    }
    return 1;
}
int RecvUser(KNetworkUser& user)
{
    char szRecvBuffer[1024= { 0, };
    int iRecvByte = recv(user.m_Sock, szRecvBuffer, 10240);
    if (iRecvByte == 0)
    {
        return 0;
    }
    if (iRecvByte == SOCKET_ERROR)
    {
        return -1;
    }
    user.DispatchRead(szRecvBuffer, iRecvByte);
    return 1;
}
void main()
{
    KNetwork net;
    net.InitNetwork();
    net.InitServer(SOCK_STREAM, 10000);
 
    SOCKADDR_IN clientAddr;
    int iLen = sizeof(clientAddr);
 
    std::cout<< "Server Start." << std::endl;
 
    FD_SET    rSet;
    FD_SET    wSet;
    timeval timeOut;
 
    timeOut.tv_sec = 1;
    timeOut.tv_usec = 0;
 
    while (1)
    {
        //매번 초기화 구조체의 주요 변수는 배열 
        FD_ZERO(&rSet);
        FD_ZERO(&wSet);
        //리슨 소켓에 rset를 저장함
        FD_SET(net.m_ListenSocket, &rSet);
 
        std::list<KNetworkUser>::iterator useriter;
        for (useriter = userlist.begin();
            useriter != userlist.end();)
        {
            //유저 중에 연결 끊긴 사람이 있다면 리스트에서 삭제함
            if ((*useriter).m_bConnect == false)
            {
                std::cout << (*useriter).m_csName << " 접속종료됨." << std::endl;
                useriter = userlist.erase(useriter);
                continue;
            }
            //유저의 소켓을 rset에 저장함
            FD_SET((*useriter).m_Sock, &rSet);
            // 만약 user에서 보낸 패킷이 있으면 wSet 구조체에 소켓 저장
            if ((*useriter).m_lPacketPool.size() > 0)
            {
                FD_SET((*useriter).m_Sock, &wSet);
            }
            useriter++;
        }
        //select 모델을 사용
        //select 함수는 fd_set 구조체에 할당한
        // FD의 이벤트가 발생하면 이를 감지하고
        // 어떤 FD 이벤트가 발생했는지 알려준다.
        int iRet = select(0,
            &rSet,
            &wSet,
            nullptr,
            &timeOut);
 
        //아무 활동이 없으면 0을 반환한다.
        if (iRet == 0)
        {
            continue;
        }std::cout << "..";
        //fdset중 소켓 fd에 해당하는 비트가 세트되어 있으면 양수값인 fd를 리턴한다.
        if (FD_ISSET(net.m_ListenSocket, &rSet))
        {
            std::cout << "test"<< std::endl;
            if (AddUser(net.m_ListenSocket) <= 0)
            {
                break;
            }
        }
        
        //유저 리스트를 돌면서 해당 유저 소켓이 저장되어 있다면
        for (KNetworkUser& user : userlist)
        {
            //연결 끊겼는지 확인 여기서는 불값 체크만
            if (FD_ISSET(user.m_Sock, &rSet))
            {
                int iRet = RecvUser(user);
                if (iRet <= 0)
                {
                    user.m_bConnect = false;
                }
            }
        }
        for (KNetworkUser& user : userlist)
        {
            if (FD_ISSET(user.m_Sock, &wSet))
            {
                if (user.m_lPacketPool.size() > 0)
                {
                    std::list<KPacket>::iterator iter;
                    for (iter = user.m_lPacketPool.begin();
                        iter != user.m_lPacketPool.end(); )
                    {
                        for (KNetworkUser& senduser : userlist)
                        {
                            int iRet = SendMsg(senduser.m_Sock, (*iter).m_uPacket);
                            if (iRet <= 0)
                            {
                                senduser.m_bConnect = false;
                            }
                        }
                        iter = user.m_lPacketPool.erase(iter);
                    }
                }
            }
        }
    }
    net.CloseNetwork();
}
cs

 

COMMENT
 
01
06

소켓모드는 블로킹(blocking)과 넌블로킹 (non-blocking)으로 구분됨

 

블로킹 소켓 : 

소켓함수 호출 시 조건에 만족하지 않으면 함수 리턴하지

않고 해당 스레드는 대기 상태가 됨.

네트워크에서 블로킹 소켓을 사용하려면 멀티스레드를 

사용해서 해결해야함.

 

1
2
//브로킹 소켓 생성
SOCKET listen_Sock = socket(AF_INET, SOCK_STREAM, 0)
cs

 

넌블로킹 소켓 : 

소켓함수 호출 시 조건이 만족하지 않아도 함수가 리턴해서 스레드는 계속 진행됨, 다른 작업 가능

멀티스레드를 사용하지 않아도 여래개의 소켓 입출력 처리할 수 있다. 

단점으로는 소켓 함수를 호출할때마다 WSAEWOULDBLOCK 등 오류 코드 확인해야함

 

1
2
3
u_long on =1;
ret = ioctlsocket(listen_Sock, FIONBIO, &on);
if(ret==SOCKET_ERROR) err_quit(ioctlsocket());
cs

 

넌블로킹 함수 호출했을때 조건이 만족하지 않아 작업을 완료하지 못하면

소켓 함수는 오류를 리턴하는데, 이때 WSAGetLastError()함수를 호출해서

오류코드 WSAEWOULDBLOCK이되면 정상작동하게 한다.

 

이는 조건이 만족되지 않음을 나타내므로 나중에 다시 소켓 함수를 호출하면 된다.

만약 이 오류가 아닐 경우에는 예상하지 못한 오류이기 떄문에 return을 해준다.

Send()함수의 경우 전송할 버퍼공간이 없다면 이 WSAEWOULDBLOCK이 뜬다.

다시 send를 호출해서 보내지게 하면 된다. 

1
2
3
4
5
6
7
    if (iSendByte == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSAEWOULDBLOCK)
            {
                return -1;
            }
        }
cs

 

COMMENT
 
1 2 3 4 5 6 7 ··· 9