[TCP/IP Socket]Chapter 01 - 네트워크 프로그래밍과 소켓의 이해

Lee Jeong Min·2020년 12월 30일
1

네트워크

목록 보기
1/17
post-thumbnail

네트워크 관련 공부 정리내용이며 참고서는 윤성우의 열혈 TCP/IP 소켓프로그래밍입니다.
실행 환경은 Ubuntu 20.04 Vscode & Windows10 Visual Studio2019 입니다.
소스코드 : https://github.com/hustle-dev/SocketProg

01-1 네트워크 프로그래밍과 소켓의 이해

소켓 프로그래밍에 사용되는 함수들 정리

소켓 완성과정을 전화기에 비유

전화기(소켓)

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 성공 시 파일 디스크립터, 실패 시 -1 반환

전화번호 부여(소켓의 주소정보, IP & PORT)

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 성공시 0 실패 시 -1 반환

전화기의 케이블 연결(소켓의 연결요청 가능한 상태)

#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 성공시 0, 실패 시 -1 반환

전화벨이 울리면 수화기를 듦(소켓의 연결요청 수락)

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 성공시 파일 디스크립터, 실패 시 -1 반환
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char * meassage);

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

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if(argc!= 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

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

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    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);
    clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
    if(clnt_sock == -1)
        error_handling("accep() error");
    
    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
}

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

서버 부분에 해당하는 코드이며 앞에서 설명한 socket, bind, listen, accept함수들이 들어가 있음을 확인할 수 있음 --> 서버 소켓

클라이언트측 소켓 함수

전화를 건다(서버 소켓으로 연결요청)

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
// 성공 시 0, 실패 시 -1 반환
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char * message);

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

    if(argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    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_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    str_len = read(sock, message, sizeof(message) - 1);
    if(str_len == -1)
        error_handling("read() error!");

    printf("Message from server: %s \n", message);
    close(sock);
    return 0;
}

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

connect 함수를 통해 연결요청을 하고 있는 모습을 볼 수 있다. --> 클라이언트 소켓


01-2 리눅스 기반 파일 조작하기

리눅스는 소켓을 파일의 일종으로 구분하여 파일 입출력 함수를 소켓 입출력에, 즉 네트워크 상에서의 데이터 송수신에 사용할 수 있다. 반면 윈도우는 파일과 소켓을 구분하고 있어서 데이터 송수신 함수를 따로 참조해야된다.

저 수준 파일 입출력과 파일 디스크럽터

저 수준: 표준에 상관없이 운영체제가 독립적으로 제공하는~ 의 의미

리눅스의 파일 디스크립터
0 : 표준입력 Standard Input
1 : 표준출력 Standard Output
2 : 표준에러 Standard Error

--> 자동으로 할당되는 파일 디스크립터들

각종 파일 관련 함수들

파일열기

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int flag);
// 성공 시 파일 디스크립터, 실패 시 -1 반환

파일닫기

#inclde <unistd.h>
int close(int fd);
// 성공 시 0, 실패시 -1 반환

파일에 데이터 쓰기

#include <unistd.h>
ssize_t wrtie(int fd, const void * buf, size_t nbytes);
// 성공 시 전달한 바이트 수, 실패 시 -1 반환

size_t 와 ssize_t는 시스템에서 정의하는 자료형이기 때문에 _t가 붙고 일반적으로 typedef 선언을 통해 size_t는 unsigned int로 ssize_t는 signed int로 정의 되어 있다고 한다. 이러한 자료형을 사용하는 이유는 과거 16비트이던 시절부터 시스템의 차이나 시간의 흐름에 따라 자료형의 표현방식이 달라지기 때문에 선택된 자료형의 변경을 최소화 하기 위해 4바이트 자료형이 필요한곳에 이러한 자료형을 사용하였다고 한다.

파일 디스크립터와 소켓

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>

int main(void)
{
    int fd1, fd2, fd3;
    fd1 = socket(PF_INET, SOCK_STREAM, 0);
    fd2 = open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
    fd3 = socket(PF_INET, SOCK_DGRAM, 0);

    printf("file descriptor 1: %d\n", fd1);
    printf("file descriptor 2: %d\n", fd2);
    printf("file descriptor 3: %d\n", fd3);
    
    close(fd1); close(fd2); close(fd3);
    return 0;
}

출력 결과는 fd1 = 3, fd2 = 4, fd3 = 5로 나오게 되는데 그 이유로 파일 디스크립터의 0, 1, 2는 표준 입출력에 이미 할당이 되었기 때문이다.


01-3 윈도우 기반으로 구현하기

리눅스와 윈도우 기반으로 동시에 공부하는 이유

소켓 프로그래밍의 경우 윈도우와 리눅스 둘이 유사하기 때문에 동시에 공부하는 것이 효과적이라고 함.

윈속의 초기화

윈속 프로그래밍을 하는 경우 반드시 WSAStartup함수를 호출해 라이브러리의 초기화 작업이 필요하다.

#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 성공시 0, 실패 시 0이 아닌 에러코드 값 반환

초기화된 라이브러리의 해제

#include <winsock2.h>
int WSACleanup(void);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환

보통 프로그램 종료 직전 위의 함수를 호출


01-4 윈도우 기반의 소켓관련 함수와 예제

윈도우 기반 소켓관련 함수들

#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
// 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환

int bind(SOCKET s, const struct sockaddr * name, int nameLen);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환

int listen(SOCEKT s, int backlog);
// 성공시 0, 실패 시 SOCCKET_ERROR 반환

SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen);
// 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환

int connect(SOCKET s, const struct sockaddr * name, int nameLen);
// 성공 시 0, 실패시 SOCKET_ERROR 반환

int closesocket(SOCKET s);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환

윈도우 기반 서버, 클라이언트 예제

서버 코드

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

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartUp() error!");

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	szClntAddr = sizeof(clntAddr);
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
		ErrorHandling("accept() error");

	send(hClntSock, message, sizeof(message), 0);
	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

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

클라이언트 코드

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

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;
	if (argc != 3)
	{
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}

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

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
		ErrorHandling("read() 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);
}

윈도우 기반 입출력 함수

#include <winsock2.h>
int send(SOCKET s, const char * buf, int len, int flags);
// 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환

int recv(SOCKET s, const char * buf, int len, int flags);
// 성공 시 수신한 바이트 수(단 EOF 전송시 0), 실패 시 SOCKET_ERROR 반환

send와 recv모두 리눅스에서 또한 사용이 됨. 책에서 read와 write를 당분간 사용하는 이유는 리눅스에서는 파일의 입출력과 소켓의 입출력이 동일함을 강조하기 위함이고 윈도우에서는 파일의 입출력과 소켓의 입출력이 다르기 때문에 다른 함수인 send와 recv를 써야만 함

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글