Select 함수

제일 먼저 등장한 입출력 다중화 모델.

예전에는 하나의 포트당 프로그램을 하나씩 동작하여 처리함. 하나의 프로세스에서 여러개의 디바이스 처리를 위해 등장

  1. 블록킹 소켓 사용 효과

    • 소켓 함수 호출시, 조건 만족이 되지 않아서 블록되는 상황을 막는다.
  2. 넌블록킹 소켓 사용 효과

    • 조건이 만족되지 않아서, 나중에 다시 호출하지 않아도 된다.

Select 동작

1
2
3
4
5
6
7
8
Select 모델을 사용하려면 소켓 셋 3개를 준비한다.

소켓 셋은 읽기 셋, 쓰기 셋, 예외 셋으로 이루어져 있다.

호출할 함수의 종류에 따라 소켓을 적당한 셋에 넣는다.
소켓 셋 준비후, Select 호출하면, Select 함수는 소켓 셋에 포함된 소켓이 입출력을 위한 준비가 될때까지 대기한다.

적어도 하나의 소켓이 준비되면 리턴, IO가 가능한 소켓만이 남는다.

IO완료를 통지받지 않고, Event Loop을 통해 소켓 상태를 검사하므로 동기식 입출력

타임아웃을 설정한 경우, 넘어가므로 논블록킹 소켓

  • 장점

    • 모든 OS에서 지원하므로 이식성이 좋다.
  • 단점

    • 스레드당 처리가능한 소켓의 갯수가 정해져 있다. ( FD_SETSIZE로 처리가능한 수를 늘릴수 있음)
    • 하위 호환성을 위해 존재한다. 소켓 입출력 모델중 가장 느리다. (소켓 상태를 다 검사해야된다.)

Example

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
```cpp

Client
{
ID, X, Y
SOCKET // 초기값 INVALID_SOCKET으로 초기화
}

Client g_client[30];
SOCKET g_ListenSocket; // 여기로 Accept 해주면 된다.
ID = x(특정값); // 세션마다 고유한 번호를 매겨야 된다. -> 쓰레드환경이면 중복값이 나올수 있다.
// 특정값부터 + 1씩 해나간다. (Unique한 값으로)


main()
{
// 서버 준비
// Startup, Bind, 등등.. 네트워크 초기화 작업

while()
{
NetworkProcess()
Draw()
}
}

void NetworkProcess()
{
// 이 소켓이 접속자인지 확인뒤 FD_SET에 넣는다.
FD_SET ReadSet;
Timeval time; // 0,0
FD_ZERO(&ReadSet);
FD_SET(g_ListenSocket, &ReadSet);

for(0~29)
{
if(접속자[n])
{
FD_SET(접속자[n]소켓, &ReadSet);
re = Select(0, &ReadSet, NULL, NULL, &time);
if(re <= 0)
return;

if(FD_ISSET(g_ListenSocket, &ReadSet))
{
Socket = Accept(g_ListenSocket, addr .... );
신규접속자처리(Socket);
}
}
}

// Listen
for(0 ~ 29)
{
if(접속여부 확인[N] && FD_ISSET(소켓, &ReadSet))
{
int packet[4];
ret = recvn(소켓, packet, 16, 0);
if(ret == 0 || ret == SOCKET_ERROR)
접속해제(N);
패킷처리(N, Packet);
}
}
}

1. 함수
신규접속자(Socket)
{
빈 클라이언트 구조체 탐색
Client[N].Socket = Socket;
[N].ID = g_ID++;
.X = ?
.Y = ?

Packet[1].g_ID;
Packet[0] = 0;
ret = Send(Socket, Packet, 16 , 0); // ID 할당 파트
if(ret == 0 || ret == SOCKET_ERROR)
접속해제(N);
// 신규 접속 파트
for(0 ~ 29)
{
if(Client[N] 확인)
{
Packet[0] = 1;
Packet[1] = g_ID;
Packet[2] = X
Packet[3] = Y;

ret = Send(Socket, Packet, 16 , 0); // ID 할당 파트
if(ret == 0 || ret == SOCKET_ERROR)
접속해제(N);
}
}
}
접속해제(index)
{
// 접속 해제 감지 방법 recv, send
CloseSocket(client[index].Socket);
Client[index] 초기화
}
패킷처리(Index, packet)

ASyncSelect

  • 소켓 함수 호출시 성공할수 있는 시점을 윈도우 메세지 수신으로 알수있다.

    멀티스레드를 사용하지 않아도, 여러개의 소켓 처리가 가능(멀티스레드가 쓰기가 애매해진다.)

윈도우 메세지를 통해, 비동기적으로 소켓활용 가능

  • 장점
    • 소켓 이벤트를 윈도우 메세지 형태로 처리하므로, GUI와 결합이 가능.
  • 단점

    • 하나의 윈도우 프로시저에서 일반 윈도우 메세지와 소켓 메세지를 처리해야하므로, 성능저하의 원인이 된다.
    • 윈도우 큐 메세지에는 한도가 있다. 같이 쓰다보니, 오버플로우 될수 있다.
    • 멀티스레드를 쓰기 힘든 구조
  • 유의할점

    • WSAASyncSelect을 사용하면 해당 소켓은 자동으로 넌블럭킹 소켓
    • accpet 함수가 리턴하는 소켓은 연결대기 소켓과 동일한 속성을 갖게된다.
      • 연결대기 소켓은 데이터송수신 하지않으므로, FD_READ, FD_WIRTE를 처리하지 않는다.
      • 다시 WSAASyncSelect 호출하여 이벤트를 등록해야된다.
    • 윈도우메세지를 받았을때, 소켓함수를 호출하지않으면, 다음번에는 윈도우메세지가 발생하지않는다.
      • 대응함수를 호출하거나, 직접 메세지를 발생시켜야 된다.

Example

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
WinMain()
{
Dialog IP 받기
// Connect 하기전에 AsyncSelect를 해야된다.
// 1. 소켓 생성
// 2. 윈도우 생성
WSAASyncSelect(socket, hWnd, UM_MESSAGE, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);
err = connect();
if(WSAGetLastError() != WSAWOULDBLOCK)
{
// 진짜 에러 -> 종료
}

// FD_CONNECT 신호가 오면 진짜로 연결된거다!. bConnect 플래그가 필요하다. FD_CONNECT가 뜨면 이 플래그를 TRUE로 전환한다.

// 메세지 루프
if(bConnect)
{

}
}

WndProc()
{
switch()
{
case WM_MOUSEMOVE:
if 마우스 클릭중이라면
SendDraw(위치 전송);
break;
case UM_NETWORK:
에러 체크
switch(이벤트 lParam)
{
case FD_CONNECT:
// 진짜 연결 플래그 전환
bConnect = true;
break;
case FD_CLOSE:
// 종료.
break;
case FD_READ:
RecvEvent();
break;
case FD_WRITE: // 첫 접속시, WSAWouldblock상태에서 전송가능상태로 전환된경우(버퍼가 비어질때)
SendFlag = true;
SendEvent();
break;
}
break;
}
}

SendDraw(Sx, Sy, Ex, Ey)
{
SendQ에 넣기 작업
SendPacket(); // SendQ에 있는 버퍼를 보내는 함수, FD_WRITE가 떳을때에도 이 함수를 호출한다.
}

SendEvent() == SendEvent()
{
if(SendFlag == false)
return;

WSABUF wsabuf[2]
// 셋팅
// sendSize = 0으로 초기화
ret = WSAEvent(Socket, &wsabuf, bufcount, &sendSize, &flag, NULL, NULL);

if(ret == SOCKET_ERROR)
{
if(WSAGetLastError() == WSAWOULDBLOCK)
{
SendFlag = false;
return;
}
}


SendQ.Remove(SendSize);

}

RecvEvent()
{
int bufcount = 1;
WSABUF wsabuf[2];
wsabuf[0].len = 한방에 안끊기고 받을수 있는 사이즈
wsabuf[0].buff = 쓰기에 대한 포인터

if(RecvQ.GetNotBrokenPutSize() < RecvQ.GetFreeSize())
{
// 아직 공간이 남았다면
wsabuf[1].len = 길이
wsabuf[1].buff = RecvQ의 시작 포인터
bufcount++;
}

DWORD RecvSize = 0; // Out
DWORD Flag = 0; // In
ret = WSARecv(sock, &wsabuf, 2, &RecvSize, &Flag, NULL, NULL);

// 방법 1.
if(ret == SOCKET_ERROR)
{
// WSAGetLastError 체크
DWORD Error = WSAGetLastError();

if(Error != WSAWOULDBLOCK)
{
끊기
}
}
// 방법 2.
if(RecvSize == 0)
{
// 끊기
}

RecvSize 크기만큼 m_rear 위치 변경
RecvQ.MoveWritePos(RecvSize);
PacketProc(); // 패킷 처리 -> 그리기
}

PacketProc()
{
unsigned short len;
while(1)
{
if(RecvQ.GetUseSize() < 2) // 최소한 헤더 크기 이상
break;

RecvQ.Peek(&len, 2); // 헤더만큼 가져온다.

if(RecvQ.GetUseSize() < len + 2) // 사용한 데이터 < 헤더 크기 + 2 라면 완성이 안되어있다.
break;

RecvQ.RemoveData(2); // 헤더만큼 큐에서 지운다.
RecvQ.Get(&Packet, len);
Draw(-);
}
}