본문 바로가기

윈도우 소켓 프로그래밍

[소켓프로그래밍] 3. TCP 서버 클라이언트 통신

이 게시글은 'TCP/IP 윈도우 소켓 프로그래밍 (김선우 저, 한빛아카데미)'를 공부한 내용을 기반으로 작성됨


 

책 2부로 넘어오자마자 굉장히 재밌는 실습이 나를 반긴다.. 그것은 바로 TCP server-client 통신

 

 

https://www.researchgate.net/figure/TCP-client-server-socket-flow_fig6_370038185

 

TCP server-client 통신은 위 도식과 같이 진행된다. 차례대로 알아보도록 하자.

 

  1. Server - socket() : socket()은 네트워크 프로그래밍에서 '소켓'을 생성하기 위한 함수. 소켓은 네트워크 통신의 endpoint를 의미하며, 소켓을 통해 서버-네트워크 혹은 컴퓨터-컴퓨터 간 데이터 통신이 가능해진다.
  2. Server - bind() : bind()는 소켓을 특정 IP 주소의 포트 번호에 연결하기 위해 사용됨. 소켓을 생성한 후에, 서버 소켓이 특정 IP 주소와 포트에서 연결을 수신하려면 bind() 함수를 호출해야 함. 서버 소켓에서 필수적으로 호출해야 하는 함수이다! (보통 서버 application에서 여러 client가 동일한 port로 접속될 수 있도록하기 위해 bind()를 사용). bind()를 실행하고 나면 소켓은 지정된 IP 주소와 port에 바인딩 되어 client의 요청을 수신할 준비가 완료된다.
  3. Server - listen() : listen()은 server socket이 client로부터 들어오는 연결 요청을 받을 준비가 되도록 소켓을 listening state로 전환해준다. 일반적으로 bind() 함수 호출 이후에 사용된다.
  4. Server - accept() : accept()는 server socket에서 대기 중인 client의 연결 요청을 수락하는 역할을 함. accept()를 호출하면 server는 client의 연결 요청을 받아들여 새로운 통신 소켓을 생성하고, 이 소켓을 통해 client와 데이터 통신 수행
  5. Client - connect() : connect()는 client socket이 server socket에 연결을 요청할 때 사용. Server가 accept()하여 server와 client간 연결이 이루어지면 두 소켓 간 데이터를 주고받을 수 있는 통신 채널이 형성 됨.
  6. send() & recv() : send()와 recv() 함수는 네트워크 소켓을 통해 데이터를 송수신할 때 사용됨. TCP 혹은 UDP 네트워크에서 데이터 송수신을 위해 필수적으로 사용된다. server / client 모두 상황에 맞게 사용할 수 있음.

 

 

 

 

이제 위 도식이 어떻게 코드로 구현되는지 알아보도록 하자. 먼저 server 코드이다.

(나는 visual studio 환경에서 구현했는데, 두 개의 프로젝트를 생성해서 각각 server, client 소스 코드를 작성해 주었다. 하나의 프로젝트에서 server / client 두 소스 코드를 작성하면 어떻게 되는지는 잘 모르겠다.)

 

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>

#define SERVERPORT 9000
#define BUFSIZE 512


// 소켓 함수 오류 출력 후 종료
void err_quit(const char* msg)
{
	LPVOID lpMsgBuf;
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPSTR)&lpMsgBuf, 0, NULL);
	MessageBoxA(NULL, (LPCSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}


// 소켓 함수 오류 출력
void err_display(const char* msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	printf("[%s] %s msg, (char *)lpMsgBuf");
	LocalFree(lpMsgBuf);
}


int main(int argc, char* argv[])
{
	int retval;


	// 윈속 초기화
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
		return 1;

	// socket()
	SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock == INVALID_SOCKET) err_quit("socket()");


	// bind()
	SOCKADDR_IN serveraddr;
	ZeroMemory(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = bind(listen_sock, (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;
	SOCKADDR_IN clientaddr;
	int addrlen;
	char buf[BUFSIZE + 1];


	while (1) {
		// accept()
		addrlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (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("[TCP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);


			// 데이터 보내기
			retval = send(client_sock, buf, retval, 0);
			if (retval == SOCKET_ERROR) {
				err_display("send()");
				break;
			}
		}

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

	}

	// closesocket()
	closesocket(listen_sock);

	// 윈속 종료
	WSACleanup();
	return 0;

}

 

 

 

다음으로 client의 소스 코드이다.

 

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>

#define SERVERIP "127.0.0.1"
#define SERVERPORT 9000
#define BUFSIZE 512


// 소켓 함수 오류 출력 후 종료
void err_quit(const char* msg)
{
	LPVOID lpMsgBuf;
	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPSTR)&lpMsgBuf, 0, NULL);
	MessageBoxA(NULL, (LPCSTR)lpMsgBuf, msg, MB_ICONERROR);
	LocalFree(lpMsgBuf);
	exit(1);
}


// 소켓 함수 오류 출력
void err_display(const char* msg)
{
	LPVOID lpMsgBuf;
	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL, WSAGetLastError(),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&lpMsgBuf, 0, NULL);
	printf("[%s] %s msg, (char *)lpMsgBuf");
	LocalFree(lpMsgBuf);
}


// 사용자 정의 데이터 수신 함수
int recvn(SOCKET s, char* buf, int len, int flags)
{
	int received;
	char* ptr = buf;
	int left = len;

	while (left > 0) {
		received = recv(s, ptr, left, flags);
		if (received == SOCKET_ERROR)
			return SOCKET_ERROR;
		else if (received == 0)
			break;
		left -= received;
		ptr += received;
	}

	return (len - left);

}




int main(int argc, char* argv[])
{
	int retval;


	// 윈속 초기화
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
		return 1;

	// socket()
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == INVALID_SOCKET) err_quit("socket()");


	// connet()
	SOCKADDR_IN serveraddr;
	ZeroMemory(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(SERVERIP);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = connect(sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));
	if (retval == SOCKET_ERROR) err_quit("connect()");


	// 데이터 통신에 사용할 변수
	char buf[BUFSIZE + 1];
	int len;


	// 서버와 데이터 통신
	while (1) {
		// 데이터 입력
		printf("\n[보낼 데이터] ");
		if (fgets(buf, BUFSIZE + 1, stdin) == NULL)
			break;

		// '\n' 문자 제거
		len = strlen(buf);
		if (buf[len - 1] == '\n')
			buf[len - 1] = '\0';
		if (strlen(buf) == 0)
			break;


		// 데이터 보내기
		retval = send(sock, buf, strlen(buf), 0);
		if (retval == SOCKET_ERROR) {
			err_display("send()");
			break;
		}
		printf("[TCP 클라이언트] %d바이트를 보냈습니다. \n", retval);

		// 데이터 받기
		retval = recvn(sock, buf, retval, 0);
		if (retval == SOCKET_ERROR) {
			err_display("recv()");
			break;
		}

		else if (retval == 0)
			break;


		// 받은 데이터 출력
		buf[retval] = '\0';
		printf("[TCP 클라이언트] %d바이트를 받았습니다. \n", retval);
		printf("[받은 데이터] %s\n", buf);

	}

	// closesocket()
	closesocket(sock);


	// 윈속 종료
	WSACleanup();
	return 0;

}

 

 

 

 

Server 코드를 실행시킨 후에 cmd 창에서 'netstat -a -n'을 입력한다.

 

netstat : TCP/IP 네트워크 연결과 상태 정보를 표시

-a : 모든 연결과 연결 대기 포트를 표시

-n : 주소와 포트 번호를 숫자 형식으로 표시

 

 

 

그러면 위와 같은 결과를 확인할 수 있다. server에서 사용하는 port number인 9000의 상태가 LISTENING이다. client의 연결을 받아들일 준비가 된 것이므로, 이제 client 소스 코드를 실행해보자.

 

 

 

그러면 127.0.0.1:9000이 외부 주소 127.0.0.1:62541과 ESTABLISHED (연결됨) 상태로 전환된다. 이때 IP 주소의 ':' 뒷부분이 port number임을 기억하자. server는 0.0.0.0:9000과 127.0.0.1:9000으로 2개의 port를 가지고 있는데, 두 개 모두 port number가 9000인 것이다. 또한 client는 port number가 62541인 하나의 포트를 가지고 있다.

 

 

 

 

Client에서 어떠한 메시지를 server로 전송하면, 그것이 다시 Server → Client로 되돌아 오는 것을 볼 수 있다. 사실 이것은 Server 코드에서 server가 client로부터 받은 메시지를 고대로 다시 전송하도록 되어있기 때문이다. 크게 신경쓰지 않아도 된다.

 

 

 

Client에서 접속을 끊고 난 후 netstat를 다시 찍어보면 위와 같은 결과를 얻을 수 있다. client가 접속을 종료했음에도 불구하고 사용하던 62541번 port가 TIME_WAIT 상태로 남아있는 것이다. 만약 5분 후에 다시 netstat 명령어를 찍어보면 62541번 port가 사라진다.

 

 

 

  • local IP 주소 & local port number : 서버 또는 클라이언트 자신의 주소이다. 서버 내에서 local port number라고 이야기 하면 서버 자신의 것을 말하는 것이다.
  • remote IP 주소 & remote port number : 서버 또는 클라이언트가 통신하는 상대의 주소이다.