layered

간단한 서버/클라이언트 구현 본문

네트워크/열혈 TCP IP 소켓 프로그래밍

간단한 서버/클라이언트 구현

스윗푸들 2023. 5. 1. 02:12

Server.c

 

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <winsock2.h>
#include <WS2tcpip.h>;
#include <string.h>
#include <stdlib.h>

#define BUF_SIZE 1024

#pragma comment(lib, "Ws2_32.lib")

void error_handling(const char*);


int main() {
	WSADATA wsadata;
	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
		error_handling("WSAStartup() error!");

	SOCKET serv_sock, clnt_sock;
	struct sockaddr_in serv_addr, clnt_addr;
	char message[BUF_SIZE + 1];
	int clnt_addr_size;
	int port = 9000;
	int clnt_num = 1;
	int recv_len = 0;

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == INVALID_SOCKET)
		error_handling("socket() error!");
	else
		printf("socket created\n");

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	serv_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
		error_handling("bind() error!");
	else
		printf("socket binded\n");

	if (listen(serv_sock, 5) == SOCKET_ERROR)
		error_handling("listen() error!");
	else
		printf("server status: LISTENING\n");

	while (1) {
		clnt_addr_size = sizeof(clnt_addr);
		clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
		if (clnt_sock == INVALID_SOCKET)
			error_handling("accept() error!");
		else {
			printf("request accepted: connected to client %d\n", clnt_num);
			printf("IP: %s, PORT: %d\n", inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
		}

		while (1) {
			recv_len = recv(clnt_sock, message, BUF_SIZE, 0);
			if (recv_len == 0 || recv_len == SOCKET_ERROR) {
				printf("%d\n", WSAGetLastError());
				break;
			}
			printf("message from client: %s", message);
		}

		closesocket(clnt_sock);
		printf("client %d unconnected\n", clnt_num);
		clnt_num++;
	}

	closesocket(serv_sock);
	WSACleanup();
	return 0;
}

void error_handling(const char* message) {
	printf("%s\n", message);
}

 

Client.c

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#define BUF_SIZE 1024

#pragma comment(lib, "Ws2_32.lib")

void error_handling(const char*);

int main() {
	WSADATA wsadata;
	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
		error_handling("WSAStartup() error!");

	SOCKET serv_sock;
	struct sockaddr_in serv_addr;
	char message[BUF_SIZE + 1];
	int port = 9000;
	
	
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == INVALID_SOCKET)
		error_handling("socket() error!");
	else
		printf("socket created\n");

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);
	//serv_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

	if (connect(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
		error_handling("connect() error!");
		printf("%d", WSAGetLastError());
	}
	else {
		printf("connected to server!\n");
		while (1) {
			printf("Input message(Q to quit): ");
			fgets(message, BUF_SIZE, stdin);

			if (message[0] == 'Q')
				break;

			if (send(serv_sock, message, BUF_SIZE, 0) == SOCKET_ERROR)
				error_handling("send() failed!");
			else {
				printf("message sended\n");
			}
		}
		printf("unconnected\n");
	}

	closesocket(serv_sock);
	WSACleanup();

	return 0;
}

void error_handling(const char* message) {
	printf("%s\n", message);
}

 

클라이언트가 메시지를 보내면 서버에서 출력하는 프로그램이다. 서버에 연결된 클라이언트는 순차적으로 번호를 부여받는다.

아직 멀티스레드를 몰라서 두 개의 클라이언트를 실행시키게 되면, 먼저 연결된 클라이언트가 종료되어야 다음 클라이언트가 연결된다.

 

겪었던 시행착오들.

 

1 WSAEADDRNOTAVAIL 10049

클라이언트에서 서버의 주소를 설정하는 부분에서 발생한 오류이다.

 

serv_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

 

처음에는 이렇게 했는데 자꾸 connect가 실패했다. 그런데 telnet localhost로 했을 때에는 또 잘 되어서 보니까 INADDR_ANY 부분이 문제였다. 그래서 127.0.0.1(= localhost)로 했더니 잘 실행된다.

 

찾아보니 INADDR_ANY는 서버처럼, 들어오는 연결에 대한 바인드를 나타내는 데 사용된다고 한다.

 

메모

INADDR_ANY는 컴퓨터에 여러 IP가 존재할 때 IP 주소를 자동으로 선택한다. 쉽게 말하자면 여러 IP를 통칭한다고 보면 된다.

그래서 서버를 작성할 때 주소를 INADDR_ANY로 설정하면, 컴퓨터의 여러 IP 주소로 들어오는 데이터는 모두 서버에게 전달되게 된다.

하지만 클라이언트는 조금 다르다. 주소를 정확히 알아야 찾아갈 수 있다. 그러니 찾아가고자 하는 서버의 주소를 INADDR_ANY로 뭉뚱그리게 되면 클라이언트는 '거기가 어딘데?'가 되는 것이다.

 

2 WSAECONNRESET 10054

서버에서 클라이언트로부터 데이터를 받을 때, 처음에는 받은 데이터의 크기가 0이면 연결을 끊도록 했다.

그런데 클라이언트를 종료시킬 때 Q를 입력하는 게 아니라 그냥 프롬프트를 꺼 버리면 서버도 같이 종료가 되었다.

그래서 데이터를 받는 부분에 SOCKET_ERROR도 검사하도록 했더니 해결이 되었다.

 

메모

소켓을 정상적으로 종료하거나, 강제로 종료하거나에 상관없이 서버가 받는 데이터의 크기는 0일 거라고 생각했다. 어차피 둘 다 소켓이 없는 건 마찬가지이기 때문이다.

하지만 위에서 보듯이 강제로 종료할 경우 오류를 발생시킨다. 이는 서버에서 클라이언트와의 연결을 적절하게 끊는 과정이 필요하기 때문이다. 이를 테면 '나 연결 끊을게!' '어 알겠어~' 하는 것이다.

그래서 클라이언트가 예기치 않게 종료된다면 서버는 '얘 어디 갔어?'가 되어서 데이터를 받지 못하고 오류를 발생시키게 되는 것 같다.

 

(이 프로그램은 TCP 기반이니까 closesocket 함수는 4 way handshake를 실행하게 되는 건가...? 이건 더 찾아봐야겠다.)

 

사실 클라이언트를 강제로 종료시킨다고 해도 운영체제에서 알아서 소켓을 종료시킬 줄 알았다.

그런데 오류가 나왔고, 실제로 closesocket(serv_sock)을 printf()와 scanf_s() 사이에 넣고 실행해 보니 입력을 하기 전에 클라이언트가 정상적으로 종료되었다.

 

3 텔넷에서 메시지 문제

텔넷으로 접속하면 데이터를 한 글자씩 받는다... 라인 모드 때문이라고 하는데, 다른 서버를 실행하고 접속하면 엔터를 입력하기 전까지의 글자만큼 잘 받는다.

뭐가 문제인지 모르겠어서 일단은 그대로 두었다ㅠ

 

으아 힘들당