Network/Windows 소켓 프로그래밍 입문에서 고성능 서버까지!

TCP 소켓 프로그래밍 입문

Tony Lim 2023. 11. 16. 14:18

소켓의 본질에 대한 이해

  • 소켓은 OS커널에 구현 되어 있는 프로토콜 요소에 대한 추상화된 인터페이스
  • 장치 파일의 일종으로 이해할 수 있음
  • 일반 파일에 대한 개념이 대부분 적용됨

 

TCP 상태 다이어그램과 적절한 상태 전이

  •  파란색이 server이고 연결을 대기하고 있다가 끊길때는 client가 끊자고 해서 server는 passive close가 이뤄진다.
  • server가 먼저 연결을 끊어버리는것은 적절한 전이 가 아니다.

 

TCP 에코 서비스 전체 흐름

  • socket() -> bind() 가 끝나야 CLOSED -> LISTEN으로 변경된다. 
  • listen()을 통해 대기하다가 client 가 connect를 시도하면 server 가 accept하고 연결이 된다.
  • socket은 server socket , accept에서 반환하는 socket이 존재한다.

 

TCP 에코 서버 제작: 서버 접속대기

#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")

int _tmain(int argc, _TCHAR* argv[])
{
	//※윈속 초기화
	WSADATA wsa = { 0 };
	if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		puts("ERROR: 윈속을 초기화 할 수 없습니다.");
		return 0;
	}

	//1. 접속대기 소켓 생성
	SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		puts("ERROR: 접속 대기 소켓을 생성할 수 없습니다.");
		return 0;
	}

	//2. 포트 바인딩
	SOCKADDR_IN	svraddr = { 0 };
	svraddr.sin_family = AF_INET;
	svraddr.sin_port = htons(25000);
	svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	if (::bind(hSocket, (SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
	{
		puts("ERROR: 소켓에 IP주소와 포트를 바인드 할 수 없습니다.");
		return 0;
	}

	//3. 접속대기 상태로 전환
	if (::listen(hSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		puts("ERROR: 리슨 상태로 전환할 수 없습니다.");
		return 0;
	}

	//4. 클라이언트 접속 처리 및 대응
	SOCKADDR_IN clientaddr = { 0 };
	int nAddrLen = sizeof(clientaddr);
	SOCKET hClient = 0;
	char szBuffer[128] = { 0 };
	int nReceive = 0;

	//4.1 클라이언트 연결을 받아들이고 새로운 소켓 생성(개방)
	while ((hClient = ::accept(hSocket,
		(SOCKADDR*)&clientaddr,
		&nAddrLen)) != INVALID_SOCKET)
	{
		puts("새 클라이언트가 연결되었습니다."); fflush(stdout);
		//4.2 클라이언트로부터 문자열을 수신함.
		while ((nReceive = ::recv(hClient, szBuffer, sizeof(szBuffer), 0)) > 0)
		{
			//4.3 수신한 문자열을 그대로 반향전송.
			::send(hClient, szBuffer, sizeof(szBuffer), 0);
			puts(szBuffer); fflush(stdout);
			memset(szBuffer, 0, sizeof(szBuffer));
		}

		//4.3 클라이언트가 연결을 종료함.
		::shutdown(hSocket, SD_BOTH);
		::closesocket(hClient);
		puts("클라이언트 연결이 끊겼습니다."); fflush(stdout);
	}

	//5. 리슨 소켓 닫기
	::closesocket(hSocket);

	//※윈속 해제
	::WSACleanup();
	return 0;
}

listen 의 2번째 인자에 원래 backlog , 즉 accpet처리중에 다른 client들이 연결을 시도 할 때 몇명까지 대기하게 할것이냐를 정하는 숫자를 썼었는데

요즘은 다 OS가 관리해준다.

tcp layer에서 segment의 조립이 일어나고 kernel buffer에서 application buffer로 copy가 일어나는 방식으로 recv()가 일어난다.

server에서 recv 에서 0을 반환했다는것은 client가 연결을 종료 했다는것을 의미한다.

 

tcp client

#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")


int _tmain(int argc, _TCHAR* argv[])
{
	//※윈속 초기화
	WSADATA wsa = { 0 };
	if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		puts("ERROR: 윈속을 초기화 할 수 없습니다.");
		return 0;
	}

	//1. 접속대기 소켓 생성
	SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		puts("ERROR: 소켓을 생성할 수 없습니다.");
		return 0;
	}

	//2. 포트 바인딩 및 연결
	SOCKADDR_IN	svraddr = { 0 };
	svraddr.sin_family = AF_INET;
	svraddr.sin_port = htons(25000);
	svraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (::connect(hSocket,
		(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
	{
		puts("ERROR: 서버에 연결할 수 없습니다.");
		return 0;
	}

	//3. 채팅 메시지 송/수신
	char szBuffer[128] = { 0 };
	while (1)
	{
		//사용자로부터 문자열을 입력 받는다.
		gets_s(szBuffer);
		if (strcmp(szBuffer, "EXIT") == 0)		break;

		//사용자가 입력한 문자열을 서버에 전송한다.
		::send(hSocket, szBuffer, strlen(szBuffer) + 1, 0);
		//서버로부터 방금 보낸 문자열에 대한 에코 메시지를 수신한다.
		memset(szBuffer, 0, sizeof(szBuffer));
		::recv(hSocket, szBuffer, sizeof(szBuffer), 0);
		printf("From server: %s\n", szBuffer);
	}

	//4. 소켓을 닫고 종료.
	//::shutdown(hSocket, SD_BOTH);
	::closesocket(hSocket);
	//※윈속 해제
	::WSACleanup();
	return 0;
}