외로운 Nova의 작업실

c 소켓프로그래밍 공부 - 3(helloworld 전송 서버) 본문

Programming/C

c 소켓프로그래밍 공부 - 3(helloworld 전송 서버)

Nova_ 2022. 3. 22. 11:42

저번 2장에서 정리한것들을 기반으로 간단하게 소켓서버를 만들어보고 클라이언트가 접속하면 hello world 문장을 클라이언트에게 줘서 클라이언트가 콘솔에 출력하는 콘솔앱을 한번 만들어보겠습니다.

저번시간에는 이번 3장에서 쓸 내용들을 정리해봤는데요, 이번 장을 공부하면서 정의가 필요하다면 2장으로 넘어가서 보고 오면 이해가 되실겁니다.

먼저, 컴퓨터간의 통신은 굉장히 복잡하게 이루어져있습니다. 하드웨어적으로 구현 가능해야하다보니 필요한 조건들도 많습니다. 가령 통신을 할려면 sin_family를 정해줘야하는 것같이 말이죠.

저 sin_family를 모르신다면 지난 2장으로 넘어가서 한번 봐주시고 이해해주시기바랍니다.

또한 소켓 프로그래밍은 최소 c언어 책한권은 다읽어보고 예제까지 다 구현이 가능했다하시는 분들이 공부하기에 적합하다고 생각합니다.

구조체, 포인터, 포인터 배열 등등 자료형과 관련된 내용이 많기 때문이죠.

이 글을 읽는 독자분들이 c언어를 마스터하신분들이라고 생각하고 글을 이어나가겠습니다.

먼저, 윈도우 os에서 동작하는 소켓을 만들려면 ws2_32.lib을 링크시켜줘야합니다.

라이브러리는 바이너리형태로 저장되어있는데, ws2_32 라이브러리는 nic와 같은 네트워크 관련 장치들이 하드웨어적으로 동작하게만드는 아주 근본적인 명령어들을 가지고있습니다.

또한 c언어 개발자들이 이러한 명령어들을 가지고 c언어 형태로 편하게 사용할수 있도록 도와주게 만든것이 헤더파일이라고 설명할 수 도 있습니다.

우리는 이러한 c언어 개발자들이 만든 헤더파일 winsock2.h를 가지고 코딩을 이어나가게될것입니다.

라이브러리 링크방법https://ghostweb.tistory.com/470 이 주소로 들어가면나옵니다.

링크까지 완료됬다면 이제 서버 프로그램을 작성해보도록 하겠습니다.

작성하기전에 c언어 소켓 프로그래밍을 배운다는 것은 c언어 개발자들이 만들어 놓은것을 배워서 우리가 원하는 프로그램을 짜는것이라는 점을 잊지않았으면 좋겠습니다.

이말을 하는 이유는 c언어 개발자들이 소켓 통신을 위해 아래와 같은 함수들을 만들고 순서대로 호출해야 통신이 가능하게 설계했으므로 우리는 그 함수들을 알고 순서대로 호출하는 법을 배워야한다는 것입니다.

아래는 c언어개발자들이 서버프로그램을 개발하기위해 만든 순차적인 함수 호출 순서입니다.

1.socket() - socket생성

2.bind() - ip주소 port정보를 socket에 설정

3.listen() - 통신할 준비 완료

4.accept() - 클라이언트로부터 오면 클라이언트와 연결된 소켓 반환

5.send(),recv() - 클라이언트에게 송신,수신을 함

6.closesocket() - 소켓을 닫음

위와같은 순서로 소켓을 생성하고 이 소켓으로 클라이언트와 통신합니다. 즉, 소켓은 통신을 하기위해 필요한 기능들을 넣은 하나의 object라고 생각해도 괜찮습니다.

이 흐름을 알고 함수들 정의를 지난 2장에서 이해하셧다면 아래와 같은 코드를 짤 수 있습니다,

#include <stdio.h>#include <winsock2.h>void ErrorHandling(char* message);

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

	SOCKET servSocket, clntSocket;//서버 소켓과 클라이언트 소켓 선언
	WSADATA wsaData;//소켓 버전을 맞추기위해 WSAStartup() 함수를 선언해야하는데 거기에 필요한 인자 선언
	SOCKADDR_IN servAddr, clntAddr;// 소켓 주소 구조체 선언char buf[] = "hello world";//클라이언트로 보낼 문자열 선언int clntAddrLen;//클라이언트 소켓의 주소 구조체의 길이 선언if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)// 소켓 버전 호환을 위해 함수 호출
		ErrorHandling("WSAStartup() error!");

	servSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);// 소켓 생성if (servSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr));// 서버 주소 구조체 메모리 초기화

	servAddr.sin_family = AF_INET;// 서버 소켓 주소 체계는 IPv4설정
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);// ip는 현재 컴퓨터 ip
	servAddr.sin_port = htons(atoi(argv[1]));// cmd에서 입력한 port번호를 빅엔디안으로 변경후 short형으로 변경후 port에 넣어줌if (bind(servSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)// 위에서 설정한 서버구조체를 서버소켓에 설정
		ErrorHandling("bind() Error!");

	if (listen(servSocket, 5) == SOCKET_ERROR)//클라이언트로부터 요청을 받기 시작
		ErrorHandling("listen() error!");

	clntAddrLen = sizeof(clntAddr);// 클라이언트의 주소 구조체의 크기 설정
	clntSocket = accept(servSocket, (SOCKADDR*)&clntAddr, &clntAddrLen);// 만약 클라이언트로부터 요청을 받는다면 클라이언트와 연결된 소켓을 반환if (clntSocket == INVALID_SOCKET)
		ErrorHandling("accept() error!");

	send(clntSocket, buf, sizeof(buf), 0);//클라이언트소켓에게 buf의 문자열을 송신
	closesocket(clntSocket);// 클라이언트와 연결된 소켓 닫음
	closesocket(servSocket);//서버 소켓 닫음

	WSACleanup();//소켓 버전 호환 종료return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputc('\\n', stderr);
	exit(1);
}

클라이언트의 경우에도 c언어 개발자들이 만든 함수 호출 흐름이 있습니다. 이는 아래와 같습니다.

1.socket() - 소켓 생성

2.connect() - 서버 주소와 연결시도

3.send() recv() - 서버로부터 받거나 줌

4.closesocket() - 소켓 종료

이 함수들의 반환형이나 인자들 또한 어떤 일을 하는 지 궁금하다면 지난 2장으로 돌아가서 한번 보시기 바랍니다.

위와 같은 흐름을 알면 아래와 같이 클라이언트의 코드를 짤 수 있습니다.

#include <stdio.h>#include <winsock2.h>void ErrorHandling(char* maessage);

int main(int agrc, char* argv[]) {

	SOCKET hSocket;//호스트 소켓 생성
	SOCKADDR_IN servAddr;//서버 주소 구조체 선언char message[30];//서버로 부터 받을 버퍼선언int checkRecv;// recv잘 됫는지 확인 변수
	WSADATA wsaData;

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartyp() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);// 소켓 생성if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr));// 서버 주소 구조체 초기화
	servAddr.sin_family = AF_INET;// 서버 주소의 계열은 IPv4
	servAddr.sin_port = htons(atoi(argv[2]));// 포트번호는 cmd인자로 받은 것if (inet_pton(AF_INET, argv[1], &servAddr.sin_addr) != 1)// ip주소는 cmd 첫번째인자로 받은 것
		ErrorHandling("inet_pton() error! ");

	if(connect(hSocket, (SOCKADDR*) &servAddr, sizeof(servAddr)) == SOCKET_ERROR)//호스트 소켓으로 서버 주소 구조체에 연결시도
		ErrorHandling("connect() Error!");

	checkRecv = recv(hSocket, message, sizeof(message) - 1, 0);//서버로부터 문자열이오면 message변수에 넣음if (checkRecv == -1)// 실패하면 error출력
		ErrorHandling("recv() Error!");

	printf("Message from server : %s \\n", message);//서버로 부터 받은 메세지 출력

	closesocket(hSocket);//소켓 종료
	WSACleanup();

	return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputc('\\n', stderr);
	exit(1);
}

서버를 실행시킬때는 port번호 9190만주고 클라이언트에게는 127.0.0.1 9190을 주면 한 컴퓨터 내에서도 잘 작동한다.

초보 개발자분들은 이 글을 읽고 모르는 내용이 많을 수 밖에없습니다.

저 또한 여러가지들을 검색해서 이해하면서 코드를 짯습니다.

제가 해드릴 말은 모르는게 많아도 그 모르는 것을 알려주는 사람이 없어도 포기하지말고 이것저것 검색하다보면 해결되는경우가 많습니다.

저도 예전에 main함수의 argv와 argc가 뭔지도 몰랐습니다. 이걸 궁금해하고 알아내기까지 한달정도가 걸렸던것같습니다.

그러니 포기하지말고 계속 코딩합시다!

Comments