본문 바로가기

Ethernet Chat Server/아두이노와 윈도우 채팅 프로그램

채팅 프로그램 제작 전 : 윈도우 채팅 프로그램(BSD소켓)

안녕하세요.

Edward입니다.

이번시간에는 윈도우 채팅 프로그램에 대해서 알아보는 시간입니다.
윈도우 채팅 프로그램은 TCP기반 IPv4환경으로 작성으로 되며, 아두이노 코드와 비교 시 흐름은 같지만 코드가 다르다는 점 명심하시기 바랍니다.

출처 - http://www.hacure.com/b/it_tip-215

사용 헤더
#include <Winsock2.h> : 윈도우 환경에서 소켓 관련 함수를 호출하기 위한 함수가 포함된 헤더
#include <Stdlib.h>
#include <String.h> : 데이터를 보내고 받기위한 문자열 관련 함수가 포함된 헤더이면서 메모리 관리에 대한 함수를 포함하고 있습니다.
#pragma comment(lib,"ws2_32.lib") : 윈도우에서 소켓 프로그래밍을 위해 추가해야하는 라이브러리
추가하는 방법에는 아래 2가지가 있습니다.
1. Pragma로 포함선언
2. 해당 소스의 속성프로그램에서 링커 - 추가라이브러리 링크 목록에 추가를 해주면 됩니다.

공통 함수
1. WSAStartup() :
2. WSACleanup() :
3. Sock() : 소켓 생성. 실패 시 -1을 반환합니다.
4. Closesocket() : 소켓 닫기.

서버 프로그래밍 : 서버의 경우 주로 클라이언트에서 보내는 요청에 응답하기 위해 다음과 같이 통신이 이루어 집니다.

1. Sock() : 소켓 생성
2. 구조체 정보 할당
3. Bind() : 생성한 소켓을 커널에 등록
4. Listen() : 클라이언트으로부터의 요청을 받기위한 준비상태로 전환
5. Accept() : 클라이언트에서 보내는 요청에 대해 대기 / 허락
while( 연결조건 == true ){
6. Send() : 데이터 송신
7. Recv() : 데이터 수신
}
8. CloseSocket() : 생성된 소켓 닫기

Sock() - 소켓 생성
int server_sock;

server_sock = socket(PF_INET, SOCK_STREAM, 0);
if(server_sock == INVALID_SOCKET)
ErrorHandling("Socket() error!");

1. PF_INET은 프로토콜 체계 중 하나로 IPv4의 프로토콜 체계입니다. 프로토콜 체계에는 더 많은 종류가 있으나 다음에 다뤄보도록 하겠습니다.
2. SOCK_STREAM은 연결 지향성 소켓으로 TCP를 의미합니다. 연결지향성(TCP)소켓과 비연결지향성(UDP)소켓으로 나뉩니다.
3. 소켓함수 실행에 실패했을 경우 디버깅을 용이하게 하기 위해 에러메시지를 출력합니다. 위에서는 INVALID_SOCKET(-1)값을 반환할 경우 에러메시지를 출력합니다.

소켓 구조체에 정보 할당
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 주소체계를 설정합니다.
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(port));

1. memset()은 구조체 내부를 초기화 해줍니다. 위에서는 0으로 멤버변수를 초기화 하고 있습니다.
2. 주소체계를 설정하는 부분입니다. AF_INET은 IPv4의 주소체계를 의미합니다.
3. IP를 설정하는 부분입니다. INADDR_ANY는 자동으로 스스로의 IP를 불러와 저장합니다.
4. Port를 설정하는 부분입니다. Atoi()는 문자열로 받은 포트번호를 정수로 변환해줍니다.

Bind() - 생성된 소켓의 정보를 커널에 등록
if(bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR)
ErrorHandling("Bind() error!");

1. (struct sockaddr *)&server_addr은 server_addr의 주소를 의미합니다.
2. sizeof(server_addr)은 server_addr의 구조체 크기

Listen() - 클라이언트로부터의 요청을 받아들이기 위한 상태로 전환
if(listen(server_sock, 5) == SOCKET_ERROR)
ErrorHandling("Listen() error!");

1. 위에서 5는 대기를 받을 접속 갯수를 의미합니다.

Accept() - 클라이언트의 접속 요청을 수락
client_addr_size = sizeof(client_addr);
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_size);

if(client_sock == -1)
ErrorHandling("Accept() error!");

1. 클라이언트에게 접속 요청을 받으면 해당 주소로 연결을 시도합니다.

클라이언트 프로그래밍 - 클라이언트 부분에서의 프로그래밍은 서버에 비해 단순합니다. 단지 정보를 할당하고 서버에 접속요청을 하기만 하면 됩니다.

1. 소켓 구조체에 정보 할당
2. Connect() : 서버에 접속 요청을 보냅니다.

소켓 구조체에 정보 할당
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(atoi(port));

1. sin_addr.s_addr부분에서 long형 ip주소를 입력해 주어야 하는데 inet_addr은 문자열을 long형 ip주소로 변환시켜 줍니다.

Connect() - 서버로 접속을 요청합니다.
if(connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
ErrorHandling("Connect() error!");

글을 작성하고 있는 저같은 경우 connect()부분에서 client_sock이 아닌 server_sock을 써놓고 오류를 찾지못해 한참 헤맸습니다. 서버는 처음에 client_sock을 별도로 생성해주지만 클라이언트는 생성해주지 않기 때문에 착각할 경우 소켓핸들오류가 날 가능성이 높습니다. 꼭 client_sock으로 할당해주세요!

Send() / Recv() : 데이터를 보내고 받는 용도로 쓰이는 함수들입니다. Read() / Write()으로도 데이터를 주고받을 수 있지만 Send() / Recv()가 Read() / Write()와는 달리 소켓 전용 라이브러리 함수라서 사용했습니다.

Send() / Recv() - 데이터를 발신 및 수신

while(1){
if(ch == 's'){
if(send(client_sock, serv_buff, strlen(serv_buff)+1, 0) == -1)
printf("Send() error! : %u\n", WSAGetLastError());

if(recv(client_sock, serv_buff, BUFF, 0) == -1)
printf("recv() 오류!%u\n", WSAGetLastError());

printf("클라이언트로부터의 메시지 : %s\n", serv_buff);
}
else if(ch == 'c'){
if(recv(client_sock, clnt_buff, BUFF, 0) == -1)
printf("Recv() error! : %u\n", WSAGetLastError());
printf("서버로부터의 메시지 : %s\n", clnt_buff);
}

if(send(client_sock, clnt_buff, strlen(clnt_buff)+1, 0) == -1)
printf("Send() error! : %u\n", WSAGetLastError());
}

1. 한쪽에서 보내면 한쪽에서는 받아야 합니다. 서버 프로그램에서 Send()로 데이터를 전송했다면 클라이언트에서는 먼저 Recv()로 데이터를 수신해야하고, 반대의 경우 프로그래밍 순서도 바뀌어야 합니다.
2. Send()의 경우 데이터를 전송할때 문자열 끝에 NULL을 붙여 전송하므로 원래 serv_buff에 1을 더해서 보내야 합니다.
3. 앞에서와 달리 Send()와 Recv()에서는 따로 작성한 에러 메시지 출력함수를 사용하지 않고 %u와 WSAGetLastError()을 사용했는데 이는 최근 발생한 에러코드를 불러와 출력해주기 때문에 자주 오류가 발생하는 부분에 사용하면 디버깅에 매우 유용합니다.

예전에 말씀드렸다시피, 우리의 최종 목표는 W5500-EVB를 활용하여 채팅프로그램을 구현하는 것이라서 윈도우 채팅프로그램의 주석과 예제를 보면서 어떻게 구현할 것인지만 인지하시면 됩니다.
그리고 W5500-EVB는 Accept와 bind를 제공하지 않습니다. 이유는 다음 W5500-EVB 시간에 하도록 하겠습니다.