채팅 프로그램(메신저) 소스
[WSAAsyncSelcet 모델]
WSAAsyncSelect 함수는 핵심적인 역활을 하게 된다.
윈도우 메시지 형태로 소켓과 관련된 네트워크 이벤트를 처리할 수 있다.
Point. 모든 소켓과 관련된 메시지는 하나의 윈도우 프로시저로 전달되므로 멀티 스레드를 사용하지 않고도 여러 소켓(다중접속)을 처리 할 수 있다.
// 쓰레드를 사용안하겠다는 말이다...
* WSAAsyncSelet 모델을 이용한 소켓 입.출력 절차
1. WSAAsyncSelect() 함수를 이용하여 소켓을 위한 윈도우 메시지와 처리할 네트워크 이벤트를 등록한다.
// 소켓을 통해 데이터를 보내거나 받을수 있는 상황이 되면 특정 윈도우 메시지로 알려달라는 내용을 등록한다.
2. 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하고 윈도우 프로시저가 호출된다.
3. 윈도우 프로시저에서는 받은 메시지 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
int WSAAsyncSelet(
SOCKET s, // 처리하고자 하는 소켓
HWND hWnd, // 메시지를 받을 윈도우를 나타내는 핸들
unsigned int wMsg, // 윈도우가 받을 메시지. 소켓을 위한 메시지는 따로 정의되어 있지 않으므로 사용자 정의 메시지를 이용한다.
long IEvent // 처리할 네트워크 이벤트 종류를 마스크 조합으로 나타낸다.
) ;
* 네트워크 이벤트 상수값
* 네트워크 이벤트 상수값 사용 예
#define WM_SOCKETEVENT (WM_USER + 1) // 사용자 정의 윈도우 메시지
WSAAsyncSelect( s, hWnd, WM_SOCKETEVENT, FD_ACCEPT ) ;
- WSAAsysncSelect() 함수를 사용하면 해당 소켓은 자동으로 넌 블로킹 소켓이 된다.
- 윈도우 메시지를 받으면 적절한 함수를 반드시 호출해야만 한다. 만약 그렇지 않으면 같은 윈도우 메시지가 발생하지 않는다.
그러므로 윈도우 메시지가 발생하면 네트워크 이벤트에 대응하는 함수를 호출해야 하며 그렇지 않을 경우 직접 메시지를 발생시켜야 한다.
[소스]
#include <iostream>
#include <windows.h>
#include "resource.h" // 이전에 올라온 리소스 세팅이 끝난상태어야 한다..(이전에 올라온거 참고...)
#include <assert.h> // assert 사용
#include <list> // STL list 를 사용하기 위해서..
#include <set> // STL set 을 사용하기 위해서...
using namespace std ;
#define WM_SOCKETEVENT (WM_USER+1) // EVENT define 처리.. WM_USER 란건 컴퓨터의 EVENT 수라고 생각하면된다. 그 번호 +1
#define CHATNAMESIZE 100
// PACKETFLAG 안에 있는 값의 순서는 상관없다. 이벤트 처리시 사용할려고 변수 생성
enum PACKETFLAG{ PACKET_STRING, PACKET_CLIENTINFO, PACKET_CHATNAME_CHANGE, PACKET_CHATNAME_FIRST};
struct REQUESTINFO{
DWORD dwIp ;
int nPort ;
} ;
struct CLIENTDATA{
SOCKET hsocket;
char pChatName[CHATNAMESIZE];
REQUESTINFO RequestInfo;
};
list<CLIENTDATA*> g_ClientDataList;
list<CLIENTDATA*>::iterator g_it;
set<int> g_SerialSet;
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMSG, WPARAM wParam, LPARAM lParam);
BOOL Win_Create(HINSTANCE hInstance, int nCmdShow);
int WinRun();
BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct);
void OnPaint(HWND hWnd);
BOOL OnDlgInitDialog(HWND hwndDlg, LPARAM lParam);
void OnDlgCommand(HWND hwndDlg, WPARAM wParam);
void OnCommand(HWND hWnd, WPARAM wParam, HWND hwndCtl);
void OnDestroy(HWND hWnd);
void OnSocketEvent(HWND hWnd, SOCKET socket, LPARAM lParam);
void OnAccept(HWND hWnd, SOCKET socket);
void OnRead(HWND hWnd, SOCKET socket);
void OnClose(HWND hWnd, SOCKET socket);
int SendPacket( SOCKET socket, PACKETFLAG PacketFlag, void *pData, int nDataSize);
BOOL ReadPacket(HWND hWnd, SOCKET socket, int *pDataSize, PACKETFLAG *pPacketFlag, char **ppData);
void ProcessServerPacket( SOCKET socket, PACKETFLAG PacketFlag, char* pData, int nDataSize);
void ProcessClientPacket( HWND hWnd, SOCKET socket, PACKETFLAG PacketFlag, char *pData, int nDataSize);
void AddStringToList(HWND g_hwndList, char *szItem);
HINSTANCE g_hlnst;
char dwIP[20];
SOCKET g_ServSocket = NULL ;
SOCKET g_ClientSocket = NULL ;
char *servPort = "9000";
HWND g_hWnd;
char g_pChatName[20];
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// WinMain... WinCreate 윈도우 모드 생성. API 를 아직 안봤다면 그냥 하얀창을 만들어준다고 생각하면 된다..
// 동기화후 윈도우 모드를 생성하고 WinRun 을 실행한다.. WinRun 이 프로그램이 돌아가는 것이라고 생각하면 된다.. 쉽게 쉽게~~~
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevinstance,
LPSTR IpCmdLine, int nCmdShow)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
Win_Create(hinstance, nCmdShow);
return WinRun();
WSACleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CALLBACK 함수는 메시지 처리함수이다. 어떤 메시지(message) 와 메시지에 대한 부가정보(wParam, lParam) 에 맞게
// 각각의 함수들을 호출해 주고 있다. 윈도우 모드에 대해서 발생하는 메시지라고 생각하면 된다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch ( message )
{
case WM_SOCKETEVENT: OnSocketEvent(hwnd, (SOCKET)wParam, lParam); return 0 ;
case WM_CREATE: OnCreate( hwnd,(LPCREATESTRUCT)lParam);
case WM_PAINT: OnPaint(hwnd); return 0 ;
case WM_COMMAND: OnCommand(hwnd, wParam, (HWND)lParam); return 0 ;
case WM_DESTROY: OnDestroy(hwnd); return 0;
}
return DefWindowProc (hwnd, message, wParam, lParam); // Wndproc 에서 처리하지 않는 기본적인 메시지에 대한 처리
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 실제 윈도우(창) 가 돌아가는 곳이다. WinMain 에서 호출해줬었다. while 문은 TRUE 일때 계속 실행하는 것이다.
// 메시지가 GetMessage 로 들어오게 되는데 프로그램을 종료하라는 WM_QUIT 메시지가 들어오면 FALSE 를 리턴. 나머지는 TRUE 를 리턴한다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int WinRun()
{
MSG msg; // 메시지 구조체
while ( GetMessage( &msg, 0, 0, 0 ) ) // GetMessage 는 메시지큐(메시지 임시 저장지역) 에서 메시지를 읽어 들인다.
{
TranslateMessage( &msg ); // 키보드에 값이 들어왔을때 그 메시지를 만드는 역활
DispatchMessage ( &msg ); // 메시지 큐에서 꺼낸 메시지를 메시지 처리함수 WndProc 로 전달
}
return 0 ;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 윈도우를 만드는 곳이다. WinMain 에서 호출하고 있다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BOOL Win_Create(HINSTANCE hInstance, int nCmdShow)
{
g_hlnst = hInstance; // hInstance : 프로그램의 인스턴스 핸들.. 핸들을 다른곳에서 사용하니 전역...
// Instance 란 클래스가 메모리에 실제로 구현된 실체를 의미한다. 그냥 쉽게 프로그램 실행되는 것..
//------------------------------------------------------------------
// 윈도우 클래스(구조체) 제작. 초기화 . API 를 공부하자....
//------------------------------------------------------------------
WNDCLASSEX wnd;
wnd.cbSize = sizeof( wnd ); // 윈도우의 사이즈
wnd.style = CS_HREDRAW | CS_VREDRAW; // 윈도우의 스타일
wnd.lpfnWndProc = WndProc; // 메시지 처리함수 지정.
wnd.cbClsExtra = 0 ; // 일종의 예약 영역, 특수한 목적에 사용되는 여분의 공간. 보통 0 으로 지정
wnd.cbWndExtra = 0 ; // 위와 동일..;;
wnd.hInstance = hInstance; // 윈도우 클래스로 등록하는 프로그램의 번호
wnd.hIcon = LoadIcon(NULL,IDI_APPLICATION); // 윈도우가 사용할 아이콘 지정
wnd.hCursor = LoadCursor( NULL, IDC_ARROW ); // 윈도우가 사용할 커서 지정
wnd.hbrBackground = CreateSolidBrush( RGB( 173, 205, 216)); // 윈도우 배경색상
wnd.lpszMenuName = MAKEINTRESOURCE(IDR_CHATTINGMENU); // 메뉴. ( 세팅에서 만든 IDR_CHATTINGMENU )
wnd.lpszClassName = "Hello"; // 클래스 이름
wnd.hIconSm = NULL;
//------------------------------------------------------------------
// 운영체제에 클래스 등록
//------------------------------------------------------------------
RegisterClassEx( &wnd );
//------------------------------------------------------------------
// 윈도우 만들기. 창의 사이즈 . 메뉴 스타일. 크기등... 인자들을 확인하면 이해할수 있다.
//------------------------------------------------------------------
g_hWnd = CreateWindow( "Hello", "Hello",
WS_SYSMENU | WS_VISIBLE | WS_MINIMIZEBOX,
200, 200, 320, 480, NULL, NULL, hInstance, NULL );
if( !g_hWnd )
return FALSE;
//------------------------------------------------------------------
// 윈도우 화면에 출력
//------------------------------------------------------------------
ShowWindow( g_hWnd, SW_SHOW );
return 0 ;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 대화 상자 실행( 서버 / 클라이언트 선택 ) 서버와 클라이언트 선택에 맞게 세팅을 해주는 곳이다.
// 하나의 프로그램에서 서버역활과 클라이언트 역활을 할수 있기에 각각에 맞게 설정( if 검사 )
// 여기서 한가지... IDC_CHAT . IDC_SAVE. IDC_SEND 를 define 해주어야 한다. 위에서 안되어잇지만.. 영역별로 메시지 처리를 위하여
// 임의로 설정할려고 만든것이니 숫자가 안겹치게 define 해주면 된다. resourse.h 파일에 가서 다른 IDC_~~ 밑에 그 다음 숫자로 추가해준다. 3개 모두...
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BOOL OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)
{
DialogBoxParam(g_hlnst, MAKEINTRESOURCE(IDD_SELECT), hWnd, DialogProc, IDD_SELECT);
CreateWindow("listbox", NULL,
WS_CHILD | WS_VISIBLE | WS_VSCROLL,
7, 54, 300, 315, hWnd, (HMENU)IDC_SAVE, g_hlnst, NULL);
CreateWindow("edit", NULL,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL | ES_MULTILINE,
7, 375, 307-78, 43, hWnd, (HMENU)IDC_CHAT, g_hlnst, NULL);
CreateWindow("button", "보내기",
WS_CHILD | WS_VISIBLE | WS_DISABLED,
237, 385, 68, 40, hWnd, (HMENU)IDC_SEND, g_hlnst, NULL);
HFONT font = CreateFont( 12, 0, 0, 0, FW_NORMAL, 0, 0, 0, HANGEUL_CHARSET, 3, 2, 1, VARIABLE_PITCH | FF_ROMAN, "돋움체" );
SendMessage(GetDlgItem(hWnd, IDC_CHAT), WM_SETFONT, (WPARAM)font, (LPARAM)TRUE);
SendMessage(GetDlgItem(hWnd, IDC_SEND), WM_SETFONT, (WPARAM)font, (LPARAM)TRUE);
SendMessage(GetDlgItem(hWnd, IDC_SAVE), WM_SETFONT, (WPARAM)font, (LPARAM)TRUE);
// 서버인 경우
if( !strcmp(dwIP, "127.0.0.1" ) )
{
g_ServSocket = socket( PF_INET, SOCK_STREAM, 0 );
SOCKADDR_IN servAddr;
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = INADDR_ANY;
servAddr.sin_port = htons(atoi(servPort));
bind( g_ServSocket , (SOCKADDR*)&servAddr, sizeof(servAddr));
listen( g_ServSocket, SOMAXCONN );
// 서버 소켓을 넌 블로킹 소켓으로 지정
WSAAsyncSelect( g_ServSocket, hWnd, WM_SOCKETEVENT, FD_ACCEPT) ;
}
g_ClientSocket = socket( PF_INET, SOCK_STREAM, 0);
// 자기 자신도 연결이 가능하다..
SOCKADDR_IN servAddr;
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(dwIP);
servAddr.sin_port = htons(atoi(servPort));
if( connect( g_ClientSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
MessageBox(hWnd, "클라이언트 실행 오류", "클라이언트 실행 확인", MB_OK );
// 클라이언트 소켓을 넌 블로킹 소켓으로 전환
if( WSAAsyncSelect( g_ClientSocket, hWnd, WM_SOCKETEVENT, FD_READ | FD_CLOSE ) == SOCKET_ERROR)
MessageBox(hWnd, "클라이언트 실행 오류", "클라이언트 실행 확인", MB_OK );
return TRUE;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 대화상자 메시지 처리함수이다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMSG, WPARAM wParam, LPARAM lParam)
{
switch(uMSG)
{
case WM_INITDIALOG : OnDlgInitDialog(hwndDlg, lParam); return 0 ;
case WM_COMMAND : OnDlgCommand(hwndDlg, wParam); return 0;
}
return FALSE;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 초기화 함수. 위에 CALLBACK 함수에서 실행
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BOOL OnDlgInitDialog(HWND hwndDlg, LPARAM lParam)
{
// 처음에 서버와 클라이언트 를 선택하는 대화상자 = IDD_SELECT
if( lParam == IDD_SELECT)
{
CheckRadioButton(hwndDlg, IDC_SERVER, IDC_CLIENT, IDC_SERVER ); // 라디오 버튼 설정
SetDlgItemText(hwndDlg, IDC_IPADDRESS, "127.0.0.1" ); // 기본적으로 IP 127.0.0. 로 세팅
EnableWindow(GetDlgItem(hwndDlg, IDC_IPADDRESS), FALSE ); // Enablewindow : 윈도우의 활성화 / 비활성화
}
return TRUE;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 대화상자 커멘드에 관련된 처리 함수.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnDlgCommand(HWND hwndDlg, WPARAM wParam)
{
//-----------------------------------------------------------------------------------
// 서버 / 클라이언트 선택 대화상자.. IDD_SELECT
//-----------------------------------------------------------------------------------
if( wParam == IDC_SERVER) // IDC_SERVER 를 선택했을때
{
if( HIWORD(wParam) == BN_CLICKED) // 클릭되었을때
{
SetDlgItemText(hwndDlg, IDC_IPADDRESS, "127.0.0.1" ); // IP 입력창에 127.0.0.1 로 세팅
EnableWindow(GetDlgItem(hwndDlg, IDC_IPADDRESS), FALSE); // 비활성화
}
}
else if( wParam == IDC_CLIENT) // IDC_CLIENT 를 선택했을때
{
if( HIWORD(wParam) == BN_CLICKED ) // 클릭되었을때
{
HWND hwndIP = GetDlgItem(hwndDlg, IDC_IPADDRESS); // IP 주소를 얻는다.
SetDlgItemText(hwndDlg, IDC_IPADDRESS, ""); // 127.0.0.1 로 되어있는것 삭제.
EnableWindow(hwndIP, TRUE); // 활성화. IP 를 입력할수 있다.
SetFocus(hwndIP);
}
}
else if( wParam == IDC_SELECTOK) // 처음 대화상자 OK 버튼 눌렀을시 처리
{
GetDlgItemText( hwndDlg, IDC_IPADDRESS, dwIP, 15);
if( IsDlgButtonChecked(hwndDlg, IDC_CLIENT) != FALSE )
{
if( !strcmp("", dwIP) )
{
MessageBox(hwndDlg, "IP 주소를 잘못 입력하셨습니다.", "에러", MB_OK );
return;
}
}
EndDialog(hwndDlg, 0); // 대화상자 종료
}
//-----------------------------------------------------------------------------------
// 대화명 변경 대화상자 IDD_CHATNAME 메뉴에서 실행시킬수 있다.
//-----------------------------------------------------------------------------------
if( wParam == IDC_CHATNAMEOK ) // OK 버튼을 눌렀을때
{
char pChatName[CHATNAMESIZE];
GetDlgItemText( hwndDlg, IDC_CHATNAME, pChatName, sizeof( pChatName ) );
//비어 있는지 확인
if( lstrlen( pChatName ) == 0 )
{
MessageBox( hwndDlg, "대화명 없음", "에러", MB_OK );
return;
}
//이전 대화명과 비교
if( lstrcmp( pChatName, g_pChatName ) == 0 )
{
if( MessageBox( hwndDlg, "현재 대화명과 동일합니다.", "에러", MB_YESNO | MB_ICONERROR ) == IDYES )
EndDialog( hwndDlg, 0 );
return;
}
if( pChatName[0] == '*' )
{
MessageBox( hwndDlg, "대화명 *로 시작할수 없습니다. 오류", "오류", MB_OK );
return;
}
// 데이터를 보낸다.
SendPacket( g_ClientSocket, PACKET_CHATNAME_CHANGE, pChatName, lstrlen(pChatName) );
EndDialog( hwndDlg, 0 );
}
else if( wParam == IDC_CHATNAMECANCEL ) // Cancel 을 선택했을때 대화 상자 종료
{
EndDialog( hwndDlg, 0 );
}
//-----------------------------------------------------------------------------------
// 클라이언트 정보 대화상자 IDD_CLIENTINFO
//-----------------------------------------------------------------------------------
if( wParam == IDC_CLIENTOK ) // OK 선택시
{
char pClientName[CHATNAMESIZE];
GetDlgItemText( hwndDlg, IDC_CLIENTCHATNAME, pClientName, sizeof(pClientName) );
if( lstrlen( pClientName ) == 0 )
{
MessageBox( hwndDlg, "대화명을 입력하지 않았군요", "에러", MB_OK );
return;
}
// 데이터를 보낸다.
SendPacket( g_ClientSocket, PACKET_CLIENTINFO, pClientName, lstrlen(pClientName) );
EndDialog( hwndDlg, 0 );
}
else if( wParam == IDC_CLIENTCANCEL ) // Cancel 선택시 대화상자 종료
{
EndDialog( hwndDlg, 0 );
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 채팅프로그램(메신저) 를 꾸며주는 곳이다. API 를 배웠다면 좀더 멋지게 꾸밀수 있다. 비트맵을 이용하여 그림을 넣는다던지...
// 여기선 간단히 형태만...^^ 이 내용의 설명은 Pass ~ 만약 API 를 안배웠다면 여기서는 대충 주석잡으면서 눈으로 확인을 해서 이해하면 된다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnPaint(HWND hWnd)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT rt;
HRGN hRgn;
HBRUSH hBrush;
HFONT font, oldfont;
{
GetClientRect(hWnd, &rt);
RoundRect(hdc, (rt.left+2), (rt.top+2), (rt.right-2), (rt.bottom-385), 10, 10);
RoundRect(hdc, (rt.left+2), (rt.top+52), (rt.right-2), (rt.bottom-70), 10, 10);
RoundRect(hdc, (rt.left+2), (rt.top+370), (rt.right-2), (rt.bottom), 10, 10);
hBrush = CreateSolidBrush(RGB( 214, 223, 247));
hRgn = CreateRoundRectRgn((rt.left+3), (rt.top+3), (rt.right-2), (rt.bottom-385), 10, 10);
FillRgn(hdc, hRgn, hBrush);
font = CreateFont(15, 0, 0, 0, FW_NORMAL, 0, 0, 0, HANGEUL_CHARSET, 3, 2, 1,
VARIABLE_PITCH | FF_ROMAN, "돋움체" );
oldfont = (HFONT)SelectObject(hdc, font);
SetBkColor(hdc, RGB(214, 223, 247));
SetTextColor(hdc, RGB(0, 0, 0));
TextOut(hdc, 78, 8, "GameD 채팅 프로그램", 20);
SetTextColor(hdc, RGB(255, 0, 0));
TextOut(hdc, 42, 25, "http://blog.naver.com/gamed21", 30);
DeleteObject(oldfont);
}
SetFocus(GetDlgItem(hWnd, IDC_CHAT));
EndPaint(hWnd, &ps);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 윈도우창의 커맨드 관련된 처리
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnCommand(HWND hWnd, WPARAM wParam, HWND hwndCtl)
{
switch(LOWORD(wParam))
{
//-----------------------------------------------------------------------------------
// 채팅창 (글씨 입력하는 곳)
//-----------------------------------------------------------------------------------
case IDC_CHAT:
if( HIWORD( wParam) == EN_CHANGE) // 변할때 CHANGE .. 글씨가 써질때마다라고 생각하면 된다...
{
char pChatString[128];
GetDlgItemText(hWnd, IDC_CHAT, pChatString, sizeof(pChatString));
int nLen = lstrlen( pChatString );
HWND hwndSend = GetDlgItem( hWnd, IDC_SEND );
EnableWindow(hwndSend, nLen); // 글이 입력되면 (보내기 버튼) 이 활성화 된다.
// 그냥 엔터를 쳤을때의 처리.... 엔터만 쳐보면 알겠지만 메시지가 전송되면서 다음줄로 내려간다...
if( strstr( pChatString, "\r\n" ) != NULL )
{
pChatString[nLen-2] = '\0';
SetWindowText( hwndCtl, pChatString);
SendMessage(hWnd, WM_COMMAND, (WPARAM)IDC_SEND, (LPARAM)hwndSend);
}
}
break;
//-----------------------------------------------------------------------------------
// 보내기 IDC_SEND
//-----------------------------------------------------------------------------------
case IDC_SEND:
if( HIWORD(wParam) != BN_CLICKED )
return ;
char pChatStr[512];
GetDlgItemText(hWnd, IDC_CHAT, pChatStr, sizeof(pChatStr));
char pSendStr[576];
wsprintf( pSendStr, "[%s]: %s", g_pChatName, pChatStr); // sprintf 와 동일하다. Windows......
SendPacket( g_ClientSocket, PACKET_STRING, pSendStr, lstrlen(pSendStr)); // 데이터 전송
SetDlgItemText(hWnd, IDC_CHAT, "");
SetFocus(GetDlgItem(hWnd, IDC_CHAT));
EnableWindow(hwndCtl, FALSE);
break;
//-----------------------------------------------------------------------------------
// 메뉴에서 종료를 선택시
//-----------------------------------------------------------------------------------
case ID_APPEXIT:
OnDestroy(hWnd);
break;
//-----------------------------------------------------------------------------------
// 메뉴에서 대화명 변경 선택시
//-----------------------------------------------------------------------------------
case ID_CHANGECHATNAME:
DialogBoxParam(g_hlnst, MAKEINTRESOURCE(IDD_CHATNAME), hWnd, DialogProc, IDD_CHATNAME);
break;
//-----------------------------------------------------------------------------------
// 메뉴에서 클라이언트 정보 선택시
//-----------------------------------------------------------------------------------
case ID_CLIENTINFO:
DialogBoxParam(g_hlnst, MAKEINTRESOURCE(IDD_CLIENTINFO), hWnd, DialogProc, IDD_CLIENTINFO);
break;
} // switch(LOWORD(wParam))
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 종료
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnDestroy(HWND hWnd)
{
PostQuitMessage(0);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 패킷 전송(데이터 전송)
// 패킷 구성을 잘 살펴보면 int + PacketFlag의 사이즈 + 글자수
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int SendPacket( SOCKET socket, PACKETFLAG PacketFlag, void *pData, int nDataSize)
{
static char pPacket[1024];
int nPacketSize = sizeof(int) + sizeof(PacketFlag) + nDataSize;
if( lstrlen(pPacket) >= sizeof(pPacket))
return 0;
// memmove 는 지정한 크기만큼 메모리를 옮기는 함수이다.
memmove( pPacket, &nPacketSize, sizeof(int));
memmove(pPacket+ sizeof(int), &PacketFlag, sizeof(PacketFlag));
memmove(pPacket+sizeof(int)+ sizeof(PacketFlag), pData, nDataSize);
pPacket[nPacketSize] = '\0';
return send(socket, pPacket, nPacketSize, 0);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// WndProc 에서 WM_SOCKETEVENT 메시지가 발생할때 실행된다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnSocketEvent(HWND hWnd, SOCKET socket, LPARAM lParam)
{
switch(LOWORD(lParam))
{
case FD_ACCEPT: OnAccept(hWnd, socket); break; // 클라이언트가 접속하면 메시지 발생. ( 대응하는 함수 accept )
case FD_READ: OnRead(hWnd, socket); break; // 데이터 수신이 가능하면 메시지 발생 ( 대응하는 함수 recv. recvfrom)
case FD_CLOSE: OnClose(hWnd, socket); break; // 상대가 접속을 종료하면 메시지 발생 ( 대응하는 함수 없음)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 클라이언트가 접속하면 메시지 발생 STL 은 따로 공부가 많이 필요한 부분이다... 공부하자~~~
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnAccept(HWND hWnd, SOCKET socket)
{
SOCKADDR_IN PeerAddr;
int AddrLen = sizeof(PeerAddr);
SOCKET hClientSocket = accept( socket, (SOCKADDR*)&PeerAddr, &AddrLen );
WSAAsyncSelect( hClientSocket, hWnd, WM_SOCKETEVENT, FD_READ | FD_CLOSE );
// STL
set<int>::iterator it = g_SerialSet.begin();
for( int i = 0; it != g_SerialSet.end(); i++, it++)
{
if( i != *it)
break;
}
g_SerialSet.insert(i); // 삽입
char pNewName[CHATNAMESIZE];
wsprintf( pNewName, "*^%d^*", i); // 기본 아이디 *^(넘버)^*
SendPacket( hClientSocket, PACKET_CHATNAME_FIRST, pNewName, lstrlen(pNewName));
// CLIENTDATA ( 소켓, 아이디, IP, Port 가 저장되어 있는 구조체 ) 현재 접속된 클라이언트에 대한 정보를 저장한다..
CLIENTDATA* pClientData = new CLIENTDATA;
pClientData->hsocket = hClientSocket;
lstrcpy( pClientData->pChatName, pNewName); // lstrcpy 나 strcpy 나 같다.. 다른 것들도 마찬가지....
pClientData->RequestInfo.dwIp = PeerAddr.sin_addr.s_addr ;
pClientData->RequestInfo.nPort = PeerAddr.sin_port;
g_ClientDataList.push_back(pClientData); // 현재 저장되어 있는 클라이언트 정보를 List 에 등록한다.
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 데이터 수신이 가능하면 메시지가 발생된다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnRead(HWND hWnd, SOCKET socket)
{
int nDataSize;
PACKETFLAG packetFlag;
char* ppData = NULL;
// 변수에 어떤 값도 없는데 ReadPacket 으로 보내고 있다.. ppData 의 경우는 할당도 안했고..... 맨 아래를 보게 되면 ppData 의 해제도 보이게 된다.
// 변수 데이터의 크기(nDataSize) 와 패킷 플래그(packetFlag) 데이터(ppData)를 ReadPacket 에서 크기를 할당받는 것이다.
// Call-By-Reference 의 개념.....
if( ReadPacket( hWnd, socket, &nDataSize, &packetFlag, &ppData) == FALSE )
return;
// 들어온 socket 에 따라 처리를 해준다..
if( socket != g_ClientSocket)
ProcessServerPacket( socket, packetFlag, ppData, nDataSize);
else
ProcessClientPacket(hWnd, socket, packetFlag, ppData, nDataSize);
delete[] ppData;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 데이터의 크기를 할당한다. OnRead 에서 받은 nDataSize , packetFlag, ppData의 Call-By-Reference 개념으로 크기를 할당한다.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BOOL ReadPacket(HWND hWnd, SOCKET socket, int *pDataSize, PACKETFLAG *pPacketFlag, char **ppData)
{
int pHeader[2];
int nRecvSize = 0, nTotalRecvSize = 0;
// 데이터를 받는다. recv
while( nTotalRecvSize < sizeof(pHeader))
{
nRecvSize = recv( socket, (char*)pHeader + nTotalRecvSize, sizeof( pHeader) - nTotalRecvSize, 0);
if( nRecvSize != SOCKET_ERROR)
{
nTotalRecvSize += nRecvSize;
continue; // 에러가 아니라면 데이터를 계속 받는다. 그러다가 while 문이 FALSE 되면 탈출
}
return FALSE; // 에러일경우 return FALSE
}
int nDataSize = pHeader[0] - nTotalRecvSize;
char *pPacketData = new char[nDataSize +1 ];
pPacketData[nDataSize] = '\0';
nTotalRecvSize = 0; // 다시 사용하기 위해 0 으로 초기화
while (nTotalRecvSize < nDataSize)
{
nRecvSize = recv( socket, pPacketData + nTotalRecvSize, nDataSize - nTotalRecvSize, 0 );
if( nRecvSize != SOCKET_ERROR)
{
nTotalRecvSize += nRecvSize;
continue;
}
if( WSAGetLastError() != WSAEWOULDBLOCK) // WSAGetLastError 가장 최근에(마지막) 실패한 소켓연산의 에러를 얻는 함수
{
return FALSE;
}
}
// 할당.....
*pDataSize = nDataSize;
*pPacketFlag = (PACKETFLAG)pHeader[1];
*ppData = pPacketData;
return TRUE;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 서버 패킷 처리
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void ProcessServerPacket( SOCKET socket, PACKETFLAG PacketFlag, char* pData, int nDataSize)
{
static int i = 0 ;
switch( PacketFlag )
{
//-----------------------------------------------------------------------------------
// Oncommand 에서 IDC_SEND (메시지를 보냈을때...) 를 할때 발생한다...
// 현재 메세지를 채팅프로그램에 접속되어 있는 모든(begin ~ end)곳에 SendPacket...
//-----------------------------------------------------------------------------------
case PACKET_STRING:
for( g_it = g_ClientDataList.begin() ; g_it != g_ClientDataList.end(); g_it++ )
SendPacket( (*g_it)->hsocket, PACKET_STRING, pData, nDataSize );
break;
//-----------------------------------------------------------------------------------
// 클라이언트 정보 대화상자에서 OK( IDC_CLIENTOK) 를 선택할때 발생한다..
// 클라이언트에 대한 정보를 채팅프로그램에 접속되어 있는 모든(begin ~ end)곳에 SendPacket...
//-----------------------------------------------------------------------------------
case PACKET_CLIENTINFO:
for( g_it = g_ClientDataList.begin() ; g_it != g_ClientDataList.end(); g_it++ )
{
if( lstrcmp( (*g_it)->pChatName, pData ) == 0) // 입력한 정보가 있는지 검사... 있다면 탈출
break;
}
if( g_it != g_ClientDataList.end()) // 끝이 아닐경우 데이터 전송
SendPacket(socket, PACKET_CLIENTINFO, &(*g_it)->RequestInfo, sizeof( (*g_it)->RequestInfo ) );
else // 끝일경우 NULL...
SendPacket(socket, PACKET_CLIENTINFO, NULL, 0);
break;
//-----------------------------------------------------------------------------------
// 대화명 변경 대화상자에서 OK(IDC_CHATNAMEOK) 선택시 발생한다.
// 내용은 위에 것과 같은 원리이다... 쉽게 분석 가능
//-----------------------------------------------------------------------------------
case PACKET_CHATNAME_CHANGE:
for( g_it = g_ClientDataList.begin(); g_it != g_ClientDataList.end(); g_it++)
{
if( lstrcmp( (*g_it)->pChatName, pData) == 0)
break;
}
if( g_it != g_ClientDataList.end() )
{
SendPacket(socket, PACKET_CHATNAME_CHANGE, "", 0);
break;
}
assert( g_it == g_ClientDataList.end() ); // assert 함수는 assert.h 에 있는 함수로 에러를 체크하는 것이다..
for( g_it = g_ClientDataList.begin(); g_it != g_ClientDataList.end(); g_it++)
{
if( (*g_it)->hsocket == socket)
break;
}
if( (*g_it)->pChatName[0] == '*' )
{
int nSerial = atoi( &(*g_it)->pChatName[2] );
g_SerialSet.erase(nSerial);
}
lstrcpy((*g_it)->pChatName, pData);
SendPacket(socket, PacketFlag, pData, nDataSize );
break;
default:
assert(FALSE);
} // switch( PacketFlag )
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 클라이언트 패킷처리
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void ProcessClientPacket( HWND hWnd, SOCKET socket, PACKETFLAG PacketFlag, char *pData, int nDataSize)
{
switch(PacketFlag)
{
case PACKET_STRING:
AddStringToList( GetDlgItem(hWnd, IDC_SAVE), pData); // 채팅창(화면 : IDC_SAVE)에 출력
break;
case PACKET_CHATNAME_FIRST:
case PACKET_CHATNAME_CHANGE:
if( nDataSize == 0)
{
MessageBox( hWnd, "이미 사용중인 대화명입니다.", "에러", MB_OK);
break;
}
char pOldChatName[CHATNAMESIZE];
lstrcpy(pOldChatName, g_pChatName);
lstrcpy(g_pChatName, pData);
assert( g_pChatName[0] != '\0');
SetWindowText( hWnd, g_pChatName);
char pNotifyMsg[128];
if( PacketFlag == PACKET_CHATNAME_FIRST)
wsprintf(pNotifyMsg, "[%s]님이 입장했습니다.", g_pChatName);
else
wsprintf(pNotifyMsg, "[%s]님의 대화명이 [%s](으)로 변경되었습니다.", pOldChatName, g_pChatName);
// 대화명이 변경되었으니 바뀐 대화명을 접속해 있는 모든곳에 알려준다.
SendPacket(socket, PACKET_STRING, pNotifyMsg, lstrlen( pNotifyMsg));
break;
case PACKET_CLIENTINFO:
char pClientMsg[128];
if( nDataSize == 0)
wsprintf( pClientMsg, "요청하신 대화명이 없습니다.");
else
{
REQUESTINFO* pInfo = (REQUESTINFO*)pData;
in_addr* paddr = (in_addr*)&pInfo->dwIp;
wsprintf( pClientMsg, "주소 : %s, 포트번호 : %d", inet_ntoa(*paddr), pInfo->nPort);
}
AddStringToList(GetDlgItem(hWnd, IDC_SAVE), pClientMsg);
break;
} // switch(PacketFlag)
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 화면에 글씨 출력
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void AddStringToList(HWND g_hwndList, char *szItem)
{
SendMessage( g_hwndList, LB_ADDSTRING, 0, (LPARAM)szItem );
int nCount = (int)SendMessage(g_hwndList, LB_GETCOUNT, 0, 0);
SendMessage(g_hwndList, LB_SETTOPINDEX, nCount -1, 0);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SOCKET_EVENT FD_CLOSE 메시지 발생..
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void OnClose(HWND hWnd, SOCKET socket)
{
closesocket(socket);
// 소켓이 서버가 아니라면....
if( socket != g_ClientSocket)
{
for( g_it = g_ClientDataList.begin(); g_it != g_ClientDataList.end(); g_it++)
{
if((*g_it)->hsocket == socket)
break;
}
assert(g_it != g_ClientDataList.end());
if( (*g_it)->pChatName[0] == '*')
{
int nSerial = atoi( &(*g_it)->pChatName[2] );
g_SerialSet.erase(nSerial);
}
char pNotifyMsg[CHATNAMESIZE + 30];
wsprintf( pNotifyMsg, "[%s]님이 퇴장셨습니다.", (*g_it)->pChatName);
delete (*g_it);
g_ClientDataList.erase( g_it);
for( g_it = g_ClientDataList.begin(); g_it != g_ClientDataList.end(); g_it++)
SendPacket( (*g_it)->hsocket, PACKET_STRING, pNotifyMsg, lstrlen(pNotifyMsg));
} // if( socket != g_ClientSocket)
else{
char pMsg[] = "서버가 종료 되었습니다. 모든 프로그램을 종료합니다.";
MessageBox(hWnd, pMsg, "알림", MB_OK);
SendMessage(hWnd, WM_DESTROY, 0, 0 );
}
}
좀 길기때문에 cpp 를 분리해야 하지만..;; 그냥 여기선 쭉~~~ 작성해버렸다... .exe 파일을 여러개 실행하여 하나는 서버를 선택하고
나머지들은 (자신의 컴퓨터에서 테스트를 해야하기 때문에) ip 주소를 127.0.0.1 로 작성하여 클라이언트를 선택해서 실행하면 된다..
반드시 이전에 올라온 세팅이 끝난상태여야 한다..(대화상자라던가.. 메뉴라던가... 이전에 올라온것 참고...)
패킷에대한 것은 따로분석하여 공부를 하는 것이 좋다.. 패킷에 대한 처리를 공부하고 간단한 게임을 멀티가 되게 만들어 보는 것도 좋은 방법이라 생각된다..
'네트워크' 카테고리의 다른 글
IOCP 구현 (0) | 2013.05.22 |
---|---|
WSAAsyncSelect 사용하기 (0) | 2013.05.14 |
c# 비동기 방식의 콜백함수 (0) | 2013.05.14 |
C# 비동기 클라이언트 소켓서버 (0) | 2013.05.14 |
C# TCP 소켓통신 Server (0) | 2013.04.25 |