[소켓 프로그래밍] TCP 서버 프로그램

Jin Hur·2022년 8월 7일
0

Server Programming

목록 보기
5/14

reference: "TCP/IP 소켓 프로그래밍" / 김선우 / 한빛아카데미

TCP 서버 프로그램 구현

TCP 클라이언트가 보내준 데이터를 화면에 표시하는 TCP 서버 구현 예제이다.


소켓 프로그래밍 공통 헤더(Common.h)

/*
* TCP/IP 소켓 프로그래밍 템플릿
*/

#define _CRT_SECURE_NO_WARNINGS // 구형 C 함수 사용 시 경고 끄기
#define _WINSOCK_DEPRECATED_NO_WARNINGS // 구형 소켓 API 사용 시 경고 끄기

// 윈속2 메인 헤더
#include <WinSock2.h>
// 윈속2 확장 헤더
#include <WS2tcpip.h>

#include <tchar.h> // _T(), ...
#include <stdio.h> // printf(), ...
#include <stdlib.h> // exit(), ...
#include <string.h> // strncpy(), ...

// ws2_32.lib 링크
#pragma comment(lib, "ws2_32")


// 소켓 함수 오류 출력 후 종료
// msg 인수로 전달된 문자열과 더불어 현재 발생한 오류 메시지를 화면에 메시지 상자로 표시하고,
// 응용 프로그램 종료
void err_quit(const char* msg)
{
	LPVOID lpMsgBuf;
	// 소켓 함수의 리턴값으로 오류 발생이 확인되었다면,
	// WSAGetLastError()가 리턴해주는 오류 코드에 대응하는 오류 메시지를
	// FormatMessage() 함수로 얻을 수 있다. 
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(char*)&lpMsgBuf, 0, NULL);
	MessageBoxA(NULL, (const char*)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}

// 소켓 함수 오류 출력
// err_quit 함수에서는 출력함수로 MessageBox() 함수 대신 printf()를 사용
// 그리고 exit(1) 제거
// 오류 메시지를 출력하되 응용 프로그램을 종료하지는 않는다.
void err_display(const char* msg)
{
	LPVOID lpMsgBuf;
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(char*)&lpMsgBuf, 0, NULL);
	printf("[%s] %s\n", msg, (char*)lpMsgBuf);
	LocalFree(lpMsgBuf);
}

// 소켓 함수 오류 출력
void err_display(int errcode)
{
	LPVOID lpMsgBuf;
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, errcode,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(char*)&lpMsgBuf, 0, NULL);
	printf("[오류] %s\n", (char*)lpMsgBuf);
	LocalFree(lpMsgBuf);
}

메인 함수

#include "Common.h"

#define SERVERPORT 9000
#define BUFSIZE 512

// TCP 서버(IPv4)
DWORD WINAPI TCPServer4(LPVOID arg) {	// DWORD = 4byte = 32bit
	int retval;

	// 소켓 생성
							// (주소 체계, 소켓 타입, 사용할 프로토콜)
	SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock == INVALID_SOCKET)
		err_quit("socket()");
	// (주소 체계 상수값)
	// AF_INET: IPv4
	// AF_INET6: IPv6
	// AF_BTH: 블루투스
	// 
	// (소켓 타입): 프로토콜 특성을 나타냄 
	// SOCK_STREAM: 사용할 프로토콜이 TCP인 경우
	// SOCK_DGRAM: 사용할 프로토콜이 UDP인 경우 
	//
	// (프토토콜)
	// TCP나 UDP 프로토콜은 '주소 체계'와 '소켓 타입'만으로 프로토콜을 결정할 수 있으므로,
	// 대개는 프로토콜 부분에 0을 넣는다. 

	// bind()
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;	// 주소 체계
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	if (retval == SOCKET_ERROR) err_quit("bind()");

	// listen()
	retval = listen(listen_sock, SOMAXCONN);
	if (retval == SOCKET_ERROR) err_quit("listen()");

	// 데이터 통신에 사용할 변수
	SOCKET client_sock;
	struct sockaddr_in clientaddr;
	int addrlen;
	char buf[BUFSIZE + 1];

	while (1) {
		// accept()
		addrlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
		if (client_sock == INVALID_SOCKET) {
			err_display("accept()");
			break;
		}

		// 접속한 클라이언트 정보 출력
		printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
			inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

		// 클라이언트와 데이터 통신
		while (1) {
			// 데이터 받기
			retval = recv(client_sock, buf, BUFSIZE, 0);
			if (retval == SOCKET_ERROR) {
				err_display("recv()");
				break;
			}
			else if (retval == 0)
				break;

			// 받은 데이터 출력
			buf[retval] = '\0';
			printf("%s", buf);
		}

		// 소켓 닫기
		closesocket(client_sock);
		printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
			inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
	}

	// 소켓 닫기
	closesocket(listen_sock);
	//소켓을 사용한 통신을 마치면 관련 리소스를 반환해야 함.
	// closesocket() 함수를 통해 관련 리소스를 운영체제에 반환
	return 0;
}

// TCP 서버(IPv6)
DWORD WINAPI TCPServer6(LPVOID arg)
{
	int retval;

	// 소켓 생성
	SOCKET listen_sock = socket(AF_INET6, SOCK_STREAM, 0);
	if (listen_sock == INVALID_SOCKET) err_quit("socket()");

	// 듀얼 스택을 끈다. [Windows는 꺼져 있음(기본값). UNIX/Linux는 OS마다 다름]
	int no = 1;
	setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no));

	// bind()
	struct sockaddr_in6 serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin6_family = AF_INET6;
	serveraddr.sin6_addr = in6addr_any;
	serveraddr.sin6_port = htons(SERVERPORT);
	retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	if (retval == SOCKET_ERROR) err_quit("bind()");

	// listen()
	retval = listen(listen_sock, SOMAXCONN);
	if (retval == SOCKET_ERROR) err_quit("listen()");

	// 데이터 통신에 사용할 변수
	SOCKET client_sock;
	struct sockaddr_in6 clientaddr;
	int addrlen;
	char buf[BUFSIZE + 1];

	while (1) {
		// accept()
		addrlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addrlen);
		if (client_sock == INVALID_SOCKET) {
			err_display("accept()");
			break;
		}

		// 접속한 클라이언트 정보 출력
		char ipaddr[INET6_ADDRSTRLEN];
		inet_ntop(AF_INET6, &clientaddr.sin6_addr, ipaddr, sizeof(ipaddr));
		printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
			ipaddr, ntohs(clientaddr.sin6_port));

		// 클라이언트와 데이터 통신
		while (1) {
			// 데이터 받기
			retval = recv(client_sock, buf, BUFSIZE, 0);
			if (retval == SOCKET_ERROR) {
				err_display("recv()");
				break;
			}
			else if (retval == 0)
				break;

			// 받은 데이터 출력
			buf[retval] = '\0';
			printf("%s", buf);
		}

		// 소켓 닫기
		closesocket(client_sock);
		printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
			ipaddr, ntohs(clientaddr.sin6_port));
	}

	// 소켓 닫기
	closesocket(listen_sock);
	return 0;
}

int main(int argc, char* argv[])
{
	// 윈도우에서는 소켓 생성 전에 윈속 초기화와
	// 소켓 닫기 후 윈속 종료 단계가 더 필요함 
	// 즉 모든 윈속 프로그램은 최초 소켓 함수를 호출하기 전에 반드시 윈속 초기화 함수인
	// WSAStartup() 함수를 호출해야 함.
	// 이 함수는 프로그램에서 사용할 윈속 버전을 요청하여 윈속 라이브러리(WS2_32.DLL)를 초기화한다. 
	// 이 함수가 실패하면 DLL이 메모리에 로드되지 않는다. 

	// 윈속 초기화
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
		return 1;
	// WSADATA 구조체를 전달하면 이를 통해 윈도우 운영체제가 제공하는 윈속 구현에 관한 정보를 얻을 수 있다.


	// 멀티스레드를 이용하여 두 개의 서버를 동시에 구동한다.
	HANDLE hThread[2];
	hThread[0] = CreateThread(NULL, 0, TCPServer4, NULL, 0, NULL);
	hThread[1] = CreateThread(NULL, 0, TCPServer6, NULL, 0, NULL);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);

	// 윈속 종료
	WSACleanup();
	// 프로그램을 종료할 떄는 윈속 종료 함수인 WSACleanup() 함수를 호출해야 한다. 
	// 이 함수는 윈속 사용의 중지를 운영체제에 알려서 관련 리소스를 반환하는 역할을 한다. 
	return 0;
}

텔넷(telnet) 클라이언트로 테스트

텔넷(telnet) 클라이언트(윈도우 OS에 내장)를 사용하여 구현한 TCP 서버 프로그램을 테스트할 수 있다.

같은 PC에서 테스트하기에 IPv4 루프백 주소인 127.0.0.1을 사용한다.

다음과 같이 루프백 주소와 9000번 포트와 연결된 응용 프로그램, 즉 구현한 TCP 서버 프로그램과 연결한다.

연결 후 TCP 서버 프로그램의 콘솔에 연결 정보가 뜬다.

텔넷 클라이언트에 문자열을 입력하면 TCP 서버 콘솔에 뜨는 것을 확인할 수 있다.

0개의 댓글