Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (3) TCP 기반 서버 및 클라이언트 - 1

로파이 2021. 3. 26. 22:30

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

윈도우 기반의 소켓 프로그래밍입니다.

TCP 기반 서버 및 클라이언트 구현 예시를 공부합니다.

 

TCP/IP 프로토콜 스택

본 내용과 관련 있는 5계층과 프로토콜 스택은?

  • 어플리케이션 계층 - Port 번호
  • 전송 계층 - TCP/UDP 프로토콜
  • 네트워크 계층 - IP 주소
  • 링크 계층 - MAC 주소
  • 물리 계층

TCP란

관련 내용:

2021.01.06 - [Computer Network] - [네트워크] 전송 계층 (2) TCP 프로토콜 / 혼잡 제어

 

[네트워크] 전송 계층 (2) TCP 프로토콜 / 혼잡 제어

TCP (Transmission Control Protocol) - 전송계층에서 신뢰성있는 통신을 위한 프로토콜 일대일 연결 지향적 (point to point) 파이프라인 전송 양방향 데이터 흐름 (full duplex) - TCP 헤더와 세그먼트 Sequenc..

narakit.tistory.com

TCP 서버의 기본적인 함수 호출 순서

내용 함수
소켓 생성 socket()
소켓 주소할당 bind()
연결요청 대기상태 listen()
연결허용 accept()
데이터 송수신 read()/write()
연결 종료 close()

 

3. 연결 요청 가능 상태 설정

서버 소켓이 accept 를 하기 전에 소켓을 연결 요청 가능 상태로 설정하고 클라이언트 연결 요청 정보를 저장하기위한 대기 큐 사이즈를 설정한다.

int listen(SOCKET sock, int backlog);
  • sock: 해당 소켓에 대한 핸들
  • backlog: 연결요청 대기 큐의 크기 정보 전달

4. 서버에서 클라이언트의 연결 요청을 수락

SOCKET accept(SOCKET sock, stuct sockaddr* addr, int* addrlen);
  • sock: 서버 소켓의 핸들
  • addr: 연결 요청을 한 클라이언트의 주소 정보를 담을 구조체를 전달
  • addrlen: addr 구조체 크기

클라이언트의 기본적인 함수 호출 순서

내용 함수
소켓 생성 socket()
연결요청 connect()
데이터 송수신 read()/write()
연결 종료 close()
int connect(SOCKET sock, struct sockaddr* servaddr, socklen_t addrlen);
  • 성공 시 0, 실패시 -1 반환
  • sock: 클라이언트 소켓 핸들
  • servaddr: 연결 요청할 서버의 주소에 대한 구조체
  • addrlen: servaddr 구조체 길이

연결 요청이 성공했다는 의미는 서버의 연결 요청 대기 큐에 진입 성공하였다는 의미이다. 실제 메세지를 수신하기 위해 connect()로 클라이언트 연결 요청을 수락하고 클라이언트 소켓 핸들을 받도록 한다.

 

TCP 기반 서버 프로그램 예제

Hello_Server_Win.c

더보기
#include <WinSock2.h>
#include <iostream>
#include "../ErrorHandling.h"
using namespace std;
/*
	hello 서버 예제 입니다.
*/
int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	// 윈속 라이브러리 초기화
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	// 1. 서버 소켓 생성
	SOCKET hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error");

	// 2. IP 주소, Port 번호 할당
	SOCKADDR_IN servAddr = {};
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 단일 IP 주소로 모든 범위의 IP 주소 수신
	servAddr.sin_port = htons(atoi(argv[1]));
	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	// 3. 연결 대기 상태 진입
	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	// 4. 클라이언트 정보 수신
	SOCKADDR_IN clntAddr = {};
	int szClntAddr = sizeof(clntAddr);
	SOCKET hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
		ErrorHandling("accept() error");

	// 연결이 정상적으로 되었다면 데이터 전송
	char message[] = "Hello world!";
	send(hClntSock, message, sizeof(message), 0);

	// 소켓 종료
	closesocket(hClntSock);
	closesocket(hServSock);
	// 윈속 라이브러리 해제
	WSACleanup();

	return 0;
}

Hello_Client_Win.c

클라이언트 예제의 경우 bind() 함수를 사용하지 않을 경우 자신에 대한 IP주소와 포트번호가 connect() 함수 실행시 운영체제에 의해 자동할당 받는다. 보통 서버가 서비스를 제공하므로 클라이언트 소켓에 IP 주소/포트번호 할당은 필요없다.

더보기
#include <WinSock2.h>
#include <iostream>
#include "../ErrorHandling.h"
using namespace std;
/*
	hello 클라이언트 예제 입니다.
*/
int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	// 윈속 라이브러리 초기화
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	// 1. 클라이언트 소켓 생성
	SOCKET hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	// 2. 서버 IP 주소, Port 번호 할당
	SOCKADDR_IN servAddr = {};
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]); // 문자열을 4바이트 주소 정보로 변환
	servAddr.sin_port = htons(atoi(argv[2])); // 포트 번호로 변환
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error");

	// 연결이 정상적으로 되었다면 서버로부터 데이터 수신
	char message[30] = {0};
	int strLen = recv(hSocket, message, sizeof(message), 0);
	if (strLen == -1)
		ErrorHandling("read() error");
	printf("Message from server: %s \n", message);

	// 소켓 종료
	closesocket(hSocket);
	// 윈속 라이브러리 해제
	WSACleanup();

	return 0;
}

Iterative 에코 서버, 에코 클라이언트

 

1. 서버는 한 번에 하나씩 클라이언트와 연결되어 에코 서비스를 제공한다.

2. 서버는 총 다섯 개의 클라이언트에게 순차적으로 서비스를 제공하고 종료한다.

3. 클라이언트는 문자열을 입력하여 서버에 전송한다.

4. 서버는 한 번 받은 문자열을 클라이언트에게 재전송한다.

5. 서버와 클라이언트 간의 문자열은 클라이언트가 Q를 입력할 때까지 계속한다.

 

echo_server_win.c

기본적으로 소켓 생성과 주소 할당 부분은 위 hello_server_win과 같다.

에코 서버는 5번 반복문을 돌면서 소켓을 연결하고 연결된 소켓으로부터 전송된 메세지를 재전송한다.

// 4. 클라이언트 정보 수신 (5번 반복)
char message[BUF_SIZE] = { 0 }; // 메세지 수신 버퍼
SOCKADDR_IN clntAddr = {}; // 클라이언트 주소를 담을 구조체
int szClntAddr = sizeof(clntAddr), strLen = 0;
for (int i = 0; i < 5; ++i)
{
	// 클라이언트 연결
	SOCKET hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == -1)
	{
		ErrorHandling("Accept() error");
	}
	else {
		printf("Connected client %d \n", i + 1);
	}
	
	// 메세지 에코
	while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
	{
		send(hClntSock, message, strLen, 0);
	}
	closesocket(hClntSock);
}

 

echo_client_win.c

사용자 입력으로부터 전송할 메세지를 입력받고 종료 조건이 아니라면 메세지를 전송한다.

클라이언트 소켓이 종료되면서 서버로부터 연결이 해제 된다.

// 메세지 전송 부분
char message[BUF_SIZE] = {};
while (1)
{
	fputs("Input mesage(Q to quit): ", stdout);
	fgets(message, BUF_SIZE, stdin);

	if (strcmp(message, "q\n") == 0 || strcmp(message, "Q\n") == 0)
		break;

	// 메세지 전송
	send(hSocket, message, strlen(message), 0);

	// 메세지 수신
	int strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
	message[strLen] = 0;
	printf("Message from server: %s\n", message);
}
// 소켓 종료
closesocket(hSocket);
// 윈속 라이브러리 해제
WSACleanup();

참고: 윤성우 저 TCP/IP 소켓 프로그래밍