티스토리 뷰
- 문제 상황 )
- 수신 버퍼에 데이터가 없는데, **read**
- 송신 버퍼가 꽉 찼는데, **write**
- Select 모델(selec함수가 핵심)
- 소켓 함수 호출이 성공할 시점을 미리 알 수 있다.
- 블로킹 소켓 : 조건이 만족되지 않아서 블로킹되는 상황 예방
- 논 블로킹 소켓 : 조건이 만족되지 않아서 불필요하게 반복 체크하는 상황을 예방
실행 순서
- 읽기[ ] 쓰기[ ] 예외(00B)[ ] 관찰 대상 등록
단) OutOfBrand는 send() 마지막 인자 MSG_OOB로 보내는 특별한 데이터 받는 쪽에서도 recv 00B 세팅을 해야 읽을 수 있음. - select(readSet, writeSet, exceptSet); -> 관찰 시작
- 적어도 하나의 소켓이 준비되면 리턴 -> 낙오자는 알아서 제거됨
- 남은 소켓 체크해서 진행
- 사용 함수
- fd_set set
- FD_ZERO : 비운다 ex) FD_ZERO(set);
- FD_SET : 소켓 s를 넣는다.( 최대 크기 64개 ex) FD_SET(s, &set)
- FD_CLR : 소켓 s를 제거 ex) FD_CLR(s, &set);
- FD_ISSET : 소켓 s가 set안에 들어있으면 0이 아닌 값을 리턴한다.
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
#include<stdio.h>
#include<Winsock2.h>
#include<stdlib.h>
#define PORT 9000
#define BUFSIZE 512
//소켓 정보 저장을 위한 구조체와 변수
struct SOCKETINFO {
SOCKET sock;
char buf[BUFSIZE + 1];
int recvBytes;
int sendBytes;
};
int nTotalSockets = 0;
SOCKETINFO* SocketInfoArray[FD_SETSIZE];
//소켓 관리 함수
BOOL AddSocketInfo(SOCKET sock);
void RemoveSocketInfo(int nIndex);
//오류 출력 함수
void err_quit(const char* msg);
int main()
{
...
if (bind(listen_s, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR) err_quit("Bind");
if (listen(listen_s, SOMAXCONN) == SOCKET_ERROR) err_quit("listen");
//비동기 소켓으로 전환 하는 과정
u_long on = 1;
retVal = ioctlsocket(listen_s, FIONBIO, &on);
if (retVal == SOCKET_ERROR) err_quit("ioctlsocket()");
//데이터 통신에 사용할 변수
FD_SET rset, wset;
SOCKET clientsock;
SOCKADDR_IN clientaddr;
int addrlen, i;
while (1) {
//소켓 초기화
FD_ZERO(&rset);
FD_ZERO(&wset);
FD_SET(listen_s, &rset);
for (int i = 0; i < nTotalSockets; i++) {
if (SocketInfoArray[i]->recvBytes > SocketInfoArray[i]->sendBytes)
FD_SET(SocketInfoArray[i]->sock, &wset);
else
FD_SET(SocketInfoArray[i]->sock, &rset);
}
//select()
retVal = select(0, &rset, &wset, nullptr, nullptr);
if (retVal == SOCKET_ERROR) err_quit("select()");
//소켓 셋 검사(1) : 클라이언트 접속 수용
if (FD_ISSET(listen_s, &rset)) {
addrlen = sizeof(clientaddr);
clientsock = accept(listen_s, (SOCKADDR*)&clientaddr, &addrlen);
if (clientsock == INVALID_SOCKET) {
err_quit("accept()");
}
else {
printf("[TCP서버] 클라이언트 접속 IP : %s %d\r\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
AddSocketInfo(clientsock);
}
}
//소켓 셋 검사(2) : 데이터 통신
for (i = 0; i < nTotalSockets; i++) {
SOCKETINFO* ptr = SocketInfoArray[i];
if (FD_ISSET(ptr->sock, &rset)) {
//데이터 받기
retVal = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
if (retVal == SOCKET_ERROR) {
err_quit("Recv()");
RemoveSocketInfo(i);
continue;
}
else if (retVal == 1) {
RemoveSocketInfo(i);
continue;
}
ptr->recvBytes = retVal;
addrlen = sizeof(clientaddr);
getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);
ptr->buf[retVal] = '\0';
printf("[TCP/%s : %d] %s\r\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), ptr->buf);
}
if (FD_ISSET(ptr->sock, &wset)) {
//데이터 전달
retVal = send(ptr->sock, ptr->buf + ptr->sendBytes, ptr->recvBytes - ptr->sendBytes, 0);
if (retVal == SOCKET_ERROR) {
err_quit("send()");
RemoveSocketInfo(i);
continue;
}
ptr->sendBytes += retVal;
if (ptr->recvBytes == ptr->sendBytes) {
ptr->recvBytes = ptr->sendBytes = 0;
}
}
}
}
closesocket(listen_s);
WSACleanup();
return 0;
}
//소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock) {
if (nTotalSockets >= FD_SETSIZE) {
printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");
return FALSE;
}
SOCKETINFO* ptr = new SOCKETINFO;
if (ptr == NULL) {
printf("[오류] 메모리가 부족합니다.\n");
return FALSE;
}
ptr->sock = sock;
ptr->recvBytes = 0;
ptr->sendBytes = 0;
SocketInfoArray[nTotalSockets++] = ptr;
return TRUE;
}
//소켓 정보 삭제
void RemoveSocketInfo(int nIndex) {
SOCKETINFO* ptr = SocketInfoArray[nIndex];
//클라 정보 얻기
SOCKADDR_IN clientaddr;
int addrlen = sizeof(clientaddr);
getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);
printf("[TCP 서버] 클라이언트 종료 : IP 주소 = %s, 포트번호 = %d\r\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
closesocket(ptr->sock);
delete ptr;
if (nIndex != (nTotalSockets - 1))
SocketInfoArray[nIndex] = SocketInfoArray[nTotalSockets - 1];
--nTotalSockets;
}
void err_quit(const char* msg) {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
printf("[%s] %s\n", msg, (char*)lpMsgBuf);
LocalFree(lpMsgBuf);
exit(1);
}
아래는 WSAAsyncSelect를 활용하여 만들어진 Select 모델입니다.
- 순서 방식
- WSAAsyncSelect() 함수를 호출하여, 소켓 이벤트를 알려줄 윈도우 메시지와 관심 있는 네트워크 이벤트를 등록한다.
- 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하여 윈도우 프로시저가 호출된다.
- 윈도우 프로시저에서는 받은 메시지의 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
#include<stdio.h>
#include<Winsock2.h>
#include<stdlib.h>
#define PORT 9000
#define BUFSIZE 512
#define WM_SOCKET (WM_USER+1)
//소켓 정보 저장을 위한 구조체와 변수
struct SOCKETINFO {
SOCKET sock;
char buf[BUFSIZE + 1];
int recvBytes;
int sendBytes;
BOOL recvdelayed;
SOCKETINFO* next;
};
SOCKETINFO* SocketInfoArray;
//윈도우 메시지 처리 함수
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ProcessSocketMessage(HWND, UINT, WPARAM, LPARAM);
//소켓 관리 함수
BOOL AddSocketInfo(SOCKET sock);
SOCKETINFO* GetSocketInfo(SOCKET sock);
void RemoveSocketInfo(SOCKET sock);
//오류 출력 함수
void err_quit(const char* msg);
void err_display(int errcode);
int main()
{
WNDCLASS wndclass;
int retVal;
//윈도우 초기화
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = NULL;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = "MyWndClass";
if (!RegisterClass(&wndclass)) return 1;
//윈도우 생성
HWND hWnd = CreateWindow("MyWndClass", "TCP 서버", WS_OVERLAPPEDWINDOW, 0, 0, 600, 200, NULL, NULL, NULL, NULL);
if (hWnd == NULL) return 1;
ShowWindow(hWnd, SW_SHOWNORMAL);
UpdateWindow(hWnd);
WSADATA wsaData;
SOCKET listen_s;
SOCKADDR_IN sock_addr;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
err_quit("WSAStartup");
//소켓 생성
listen_s = socket(AF_INET, SOCK_STREAM, 0);
if (listen_s == INVALID_SOCKET)
err_quit("socket");
ZeroMemory(&sock_addr, sizeof(struct sockaddr_in));
sock_addr.sin_family = PF_INET;
sock_addr.sin_port = htons(PORT);
sock_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (bind(listen_s, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR) err_quit("Bind");
if (listen(listen_s, SOMAXCONN) == SOCKET_ERROR) err_quit("listen");
//WSAAsyncSelect()
retVal = WSAAsyncSelect(listen_s, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
if (retVal == SOCKET_ERROR) err_quit("WSAAsyncSelect()");
//메시지 루프
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
WSACleanup();
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParm, LPARAM lParm) {
switch (uMsg)
{
case WM_SOCKET:
ProcessSocketMessage(hWnd, uMsg, wParm, lParm);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, uMsg, wParm, lParm);
}
void ProcessSocketMessage(HWND hWnd, UINT uMsg, WPARAM wParm, LPARAM lParm) {
//데이터 통신에 사용할 변수
SOCKETINFO* ptr;
SOCKET clientsock;
SOCKADDR_IN clientaddr;
int addrlen, retVal;
//오류 발생 여부
if (WSAGETSELECTERROR(lParm)) {
err_display(WSAGETSELECTERROR(lParm));
RemoveSocketInfo(wParm);
return;
}
//메시지 처리
switch (WSAGETSELECTEVENT(lParm)) {
case FD_ACCEPT:
addrlen = sizeof(clientaddr);
clientsock = accept(wParm, (SOCKADDR*)&clientaddr, &addrlen);
if (clientsock == INVALID_SOCKET) {
err_quit("accept()");
}
printf("[TCP서버] 클라이언트 접속 IP : %s %d\r\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
AddSocketInfo(clientsock);
retVal = WSAAsyncSelect(clientsock, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
if (retVal == SOCKET_ERROR) {
err_quit("WSAAsyncSelecT()");
return;
}
break;
case FD_READ:
ptr = GetSocketInfo(wParm);
if (ptr->recvBytes > 0) {
ptr->recvdelayed = TRUE;
return;
}
//데이터 받기
retVal = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
if (retVal == SOCKET_ERROR) {
err_quit("recv()");
RemoveSocketInfo(wParm);
return;
}
ptr->recvBytes = retVal;
//받은 데이터 출력
addrlen = sizeof(clientaddr);
getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);
ptr->buf[retVal] = '\0';
printf("[TCP/%s : %d] %s\r\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port), ptr->buf);
case FD_WRITE:
ptr = GetSocketInfo(wParm);
if (ptr->recvBytes <= ptr->sendBytes)
return;
//데이터 보내기
retVal = send(ptr->sock, ptr->buf + ptr->sendBytes, ptr->recvBytes - ptr->sendBytes, 0);
if (retVal == SOCKET_ERROR) {
err_quit("send()");
RemoveSocketInfo(wParm);
return;
}
ptr->sendBytes += retVal;
//받은 데이터 전부 보냈는지 체크
if (ptr->recvBytes == ptr->sendBytes) {
ptr->recvBytes = ptr->sendBytes = 0;
if (ptr->recvdelayed) {
ptr->recvdelayed = FALSE;
PostMessage(hWnd, uMsg, wParm, lParm);
}
}
break;
case FD_CLOSE:
RemoveSocketInfo(wParm);
break;
}
}
//소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock) {
SOCKETINFO* ptr = new SOCKETINFO;
if (ptr == NULL) {
printf("[오류] 메모리가 부족합니다.\n");
return FALSE;
}
ptr->sock = sock;
ptr->recvBytes = 0;
ptr->sendBytes = 0;
ptr->recvdelayed = FALSE;
ptr->next = SocketInfoArray;
SocketInfoArray = ptr;
return TRUE;
}
SOCKETINFO* GetSocketInfo(SOCKET sock) {
SOCKETINFO* ptr = SocketInfoArray;
while (ptr) {
if (ptr->sock == sock)
return ptr;
ptr = ptr->next;
}
return NULL;
}
//소켓 정보 삭제
void RemoveSocketInfo(SOCKET sock) {
//클라 정보 얻기
SOCKADDR_IN clientaddr;
int addrlen = sizeof(clientaddr);
getpeername(sock, (SOCKADDR*)&clientaddr, &addrlen);
printf("[TCP 서버] 클라이언트 종료 : IP 주소 = %s, 포트번호 = %d\r\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
SOCKETINFO* curr = SocketInfoArray;
SOCKETINFO* prev = NULL;
while (curr) {
if (curr->sock == sock) {
if (prev)
prev->next = curr->next;
else
SocketInfoArray = curr->next;
closesocket(curr->sock);
delete curr;
return;
}
prev = curr;
curr = curr->next;
}
}
void err_quit(const char* msg) {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
printf("[%s] %s\n", msg, (char*)lpMsgBuf);
LocalFree(lpMsgBuf);
exit(1);
}
void err_display(int errcode) {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, errcode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
printf("[ 오류 ] %s\n", (char*)lpMsgBuf);
LocalFree(lpMsgBuf);
exit(1);
}
'서버 공부 > 네트워크' 카테고리의 다른 글
TCP/IP 윈도우 소켓 프로그래밍 10장 연습문제(with 스레드풀) (0) | 2022.07.10 |
---|---|
TCP/IP) WSAEventSelect 모델 통신 (0) | 2022.07.09 |
이상적인 소켓 입출력 모델의 특징 (0) | 2022.07.08 |
비동기 형식 TCP/IP 서버 (0) | 2022.07.08 |
GUI 멀티쓰레드 소켓 프로그래밍(TCP) 2 - 호스트이름 구하기 추가 (0) | 2022.07.07 |
댓글