layered

4 TCP 기반 서버/클라이언트 1 본문

네트워크/열혈 TCP IP 소켓 프로그래밍

4 TCP 기반 서버/클라이언트 1

스윗푸들 2023. 4. 30. 00:39

4-2 TCP 기반 서버, 클라이언트 구현


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

 

 

리눅스에서는 send/recv 대신 read/write를 사용하기도 한다.

윈도우에서는 send/recv만 사용할 수 있다.

 

윈도우에서 소켓은 closesocket()으로 닫는다.

 

메모

리눅스에서만 read/write는 파일을 다룰 때에도 사용하지만 send/recv는 소켓에서만 사용할 수 있다(대신 여러 옵션을 부여할 수 있다는 장점이 있다).

리눅스에서만 작동시키려면 read/write를 쓰고, 윈도우와의 호환성을 고려한다면 send/recv를 쓰는 것이 좋은 것 같다.

 

연결요청 대기상태로의 진입

소켓에 주소를 할당했다면, 이제 연결을 해도 된다고 알려야 한다. 즉, 소켓을 LISTENING 상태로 만드는 것이다.

 

int listen(SOCKET s, int backlog); // 성공: 0, 실패: SOCKET_ERROR
int listen(int sock, int backlog); // 성공: 0, 실패: -1

 

s, sock

아까 만들어 두었던 소켓이다. 

 

backlog

서버는 클라이언트의 요청을 큐로 관리한다. 요청을 받은 순서대로 큐에 쌓아 두었다가 하나씩 꺼내면서 처리하는 것이다.

그래서 backlog는 큐의 길이를 설정한다. 예를 들어 5라면 최대 5개의 클라이언트 요청을 큐에 대기시킬 수 있다는 의미이다.

somaxconn(socket max connection)은 지원 가능한 최댓값을 의미한다.

 

#define SOMAXCONN 0x7fffffff

 

클라이언트의 연결요청 수락

연결요청을 수락한다는 것은 클라이언트와 데이터를 주고받을 수 있는 상태가 된다는 것이다.

 

SOCKET accept(SOCKET s, sockaddr *addr, int *addrlen); // 성공: 새로운 소켓, 실패: INVALID_SOCKET
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); // 성공: 파일 디스크립터, 실패: -1

 

accept가 호출되면 내부적으로는 새로운 소켓을 생성해서 클라이언트 소켓에 연결하고 그 소켓을 반환한다.

그래서 서버 프로그램 내에는 서버 소켓과, accpet 함수로부터 반환된 소켓 두 개가 존재하게 된다.

 

클라이언트의 주소 정보(IP 주소, 포트 번호)를 얻고 싶다면 미리 생성해둔 주소 구조체를 두 번째 파라미터로 넘겨 주면 된다. 굳이 필요하지 않다면 NULL을 넘겨 주면 된다.

 

여기서 주의할 점은 주소 구조체의 크기를 담는 변수를 따로 만들어 두고, 그 포인터를 전달해야 한다는 것이다.

 

메모

왜 accept 함수는 소켓을 새로 생성할까? 찾아봤지만 그럴 듯한 정보가 없었다.

그냥 서버에서 클라이언트 소켓에 대한 정보를 가지고 있어야 하니까, 그 소켓을 가리키는 포인터 정도의 느낌인 것 같다.

 

왜 포인터를 넘겨 주는 건지 잘 모르겠다. 연결되는 클라이언트의 주소 정보로 초기화하기 위함이라고 하지만, 이미 정의된 구조체를 넘겨 주니까 크기는 정해져 있는 게 아닌가...?

개인적인 의견: sockaddr은 단지 대표적인 구조체일 뿐이고, 주소 체계에 따라 다양한 구조체들이 있다(예를 들면 AF_UNIX에서 다루는 sokaddr_un은 크기가 110바이트이다). 그래서 서버에서는 일단 sockaddr로 정의하긴 했지만 클라이언트의 실제 주소 타입에 따라서 같이 넘겨 준 크기 변수가 변경될 수 있어야 하는 것이다.

 

 

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

 

 

서버에 비해 과정이 단순해진 것을 볼 수 있다. bind 과정이 사라졌는데, 이는 연결을 요청할 때 운영체제에서 자동으로 IP 주소(컴퓨터에 할당된 IP)와 포트 번호(임의)를 할당해 주기 때문이다.

 

클라이언트가 서버에게 연결하려면 서버가 준비가 되어 있어야 하므로, 서버에서 listen 함수를 호출한 이후에 클라이언트에서 연결을 요청할 수 있다.

 

int connect(SOCKET s, const sockaddr *name, int namelen); // 성공: 0, 실패: SOCKET_ERROR
int connect(int sock, const struct sockaddr *addr, socklen_t addrlen); // 성공: 0, 실패: -1

 

여기서 성공이란 서버에 의해 연결 요청이 접수되었다는 것으로, 큐에 등록되었음을 의미한다. accept 함수가 호출되었다는 것이 아니다!

 

TCP 기반 서버, 클라이언트의 함수 호출 관계

 

 

서버는 소켓을 생성한 후 bind로 주소를 할당하고 LISTENING 상태가 된다.

그러면 클라이언트에서는 서버에 연결을 요청할 수 있게 된다. 또는 클라이언트에서 connect를 호출하기 이전에 서버에서 accept를 먼저 호출할 수도 있다.

이렇게 연결이 되고 나면 데이터를 주고받고 나서 각자 소켓을 닫는다.

 

4-3 Iterative 기반의 서버, 클라이언트 구현


클라이언트가 전송한 데이터를 그대로 재전송하는 에코 서버와 클라이언트를 구현하는 부분이다.

 

Iterative 서버의 구현

지금까지 공부한 서버는 하나의 클라이언트에게 응답을 하면 바로 종료되는 형태였다. 하지만 서버는 연속적으로 클라이언트의 연결 요청을 수락할 수 있어야 한다(여러 클라이언트에게 동시에 서비스를 제공할 수도 있어야 하지만 이건 나중에...).

 

 

그래서 이제 구현할 서버는 반복문을 이용해, 클라이언트와 연결을 하고 끊었을 때 다시 accept 함수를 호출하도록 할 것이다.

서버와 클라이언트가 데이터를 여러 번 주고받을 테니 결국 send/recv 부분도 반복문으로 작성하게 된다.

 

(참고) TCP 데이터 전송 함수


소켓과 연관된 데이터 구조체에는 지역/원격 주소 외에도 송수신 버퍼(또는 소켓 버퍼)가 존재한다. 송신 버퍼는 전송할 데이터를, 수신 버퍼는 받은 데이터를 임시로 저장하는 영역이다.

데이터 전송 함수는 이 버퍼에 접근하는 함수라고 보면 된다.

 

send()

 

int send(SOCKET s, const char *buf, int len, int flags); // 성공: 전송한 바이트 수, 실패: SOCKET_ERROR
ssize_t send(int sock, const void *buf, size_t len, int flags); // 성공: 전송한 바이트 수, 실패: -1

 

연결할 소켓, 보낼 데이터를 담은 버퍼, 데이터의 크기, 옵션을 전달한다.

 

recv()

 

int recv(SOCKET s, char *buf, int len, int flags); // 성공: 받은 데이터 수(연결 종료 시 0)
ssize_t recv(int sock, void *buf, size_t len, int flags);

 

리눅스에서 사용하는 read/write는 마지막의 flags가 없다.

 

server.c

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[]) {
	int serv_sock, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;

	struct sockaddr_in serv_addr, clnt_addr;
	socklen_t clnt_addr_size;

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == -1)
		error_handling("socke() error!");

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(9000);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
		error_handling("bind() error");

	if (listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	clnt_addr_size = sizeof(clnt_addr);

	for (i = 0; i < 5; i++) {
		clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
		if (clnt_sock == -1)
			error_handling("accept() error");
		else
			printf("connected client %d\n", i + 1);
		
		while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
			write(clnt_sock, message, str_len);

		close(clnt_sock);
	}
	close(serv_sock);
	return 0;
}

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

 

client.c

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[]) {
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_addr;

	sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock == -1)
		error_handling("socket() error");
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(9000);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
		error_handling("connet() error");
	else
		puts("connected...............");

	while (1) {
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);

		if (!strcmp(message, "Q\n"))
			break;

		write(sock, message, strlen(message));
		str_len = read(sock, message, BUF_SIZE - 1);
		message[str_len] = 0;
		printf("Message from server: %s", message);
	}
	close(sock);
	return 0;
}

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

 

메모

내 나름대로 코드를 썼을 때에는 connected가 안 되어서 책에 있는 걸 그대로 쓰니까 잘 실행된다.

딱히 틀린 게 없어 보이는데 왜 안 되는지 모르겠다...