LAB/TCP_IP

TCP / IP 소켓 프로그래밍

it-lab-0130 2024. 9. 3. 09:11

프로세스 와 프로세서

[프로세스 Process]

"컴퓨터에서 연속적으로 실행되고 있는 프로그램"

*** 실행되는 프로그램들은 전부 프로세스 라고 보면 된다. 이것보다 더 작은 실행 단위는 스레드(Thread) 프로세스마다 최소 스레드 1개는 만들어진다.

[프로세서 Processor]

"정보를 전달하는 여러 장치로 구성되어 있으며, 이를 통해 사용자가 어플리케이션을 실행하거나 파일을 변경할 때 요청한 작업을 처리하는것"

즉, CPU 라는 물리적 장치 (= 프로세서)로 사용자가 입력한 프로그램의 명령을 해석하고 그 결과를 출력하는 역할

병렬처리 / 병행 처리

[병렬 처리]

"여러 작업을 동시에 실행하는 방법"

2개 이상의 코어가 각기 다른 프로세스틔 명령을 실행해서 각 프로세스가 같은 순간에 실행되도록 하는 방법

[병행 처리]

"하나의 코어가 여러 프로세스를 돌아가면서 조금씩 처리 하는것"

미세단위로 프로그램 실행 순서를 바꾸며 실행시켜 사람이 보기에는 동시에 실행되는 것처럼 보인다.

동기 / 비동기

[동기]

동기식 처리 모델(Synchronous Processing Model)은 직렬적으로 일을 처리한다.

즉, 요청이 들어오면 순차적으로 작업을 수행하고, 해당 작업이 수행중이면 다음 작업은 대기하게 된다.

서버는 한번에 많은 요청이 들어오고 동시에 많은 요청을 처리해야하기 때문에 이러한 방식과는 맞지 않는다. 만약 서버가 동기식으로 되어있다면, 수만명이 서버에 한번에 요청을 보냈을때, 가장 마지막에 요청을 보낸 사람은 앞에 9999명의 요청이 모두 처리될때까지 기다려야하는 일이 발생할 수 있다.

동기식은 위에서 설명했듯이 순차적으로 일을 처리한다. 특정 작업이 수행중이라면 다음작업을 받지 않고 해당 작업이 끝날때까지 기다린다.

[비동기]

비동기식 처리 모델(Asynchronous Processing Model or Non-Blocking Processing Model)은 병렬적으로 일을 처리한다.

즉, 요청이 들어오면 해당 요청에 의한 작업이 끝나지 않았더라도 계속 요청을 받는다. 그리고 작업이 끝났다는 이벤트가 오면 해당 요청을 처리한다.

비동기식은 동기식과 다르게 들어오는 모든 일을 우선 받아두고 끝났다는 이벤트가 전달되면 해당 이벤트를 처리한다

[Context Switching이란?]

"작업의 주체가 현재 Context를 잠시 중단하고 다른 Context를 실행하는 것을 Context Switching이라 한다."

CPU의 코어가 1개(자원이 1개)라면 동시에 단 하나의 프로세스만 실행이 가능하다. CPU scheduling을 통해서 하나의 CPU를 여러 작업들이 공유할 수 있게 cpu 시간을 나누어 작업을 수행한다.

이때, 프로세서가 지금까지 실행되던 프로세스(A)를 중지하고 다른 프로세스(B)의 PCB정보를 바탕으로 프로세스(B)를 실행하는 것을 Process Context Switching이라고 한다.

동일한 프로세스 속에서 하나의 쓰레드(a)를 중지하고 다른 쓰레드(b)의 TCB정보를 바탕으로 쓰레드 (b)를 실행하는 것을 Thread Context Switching이라고 한다.

참고로, Process는 하나이상의 Thread로 동작하기 때문에 Context Switching의 최소 단위는 TCB이다.

[Process context switching vs Thread context switching]

프로세스는 하나 이상의 쓰레드를 포함한다. 이 쓰레드들은 고유한 Stack영역의 메모리와 고유한 registers를 할당 받으며 Heap영역의 메모리에서 선언된 데이터는 서로 공유한다.

동일한 프로세스 속에서 thread context switching이 발생할 경우 processor는 stack영역의 주소와 registers 주소를 포함한 thread의 context 정보만을 변경하면 된다.

하지만 process context switching이 발생할 경우 processor는 thread의 context뿐만 아니라 process의 context까지 모두 변경해야 한다.


TCP / IP

윤성우의 열혈 TCP/ IP 소켓 프로그래밍

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

" 모든 프로그램은 server와 client 통신 방식이 동일하다 즉, 소켓 전송 구조가 동일하다."

[IP]

인터넷에 연결되어 있는 모든 장치들 (컴퓨터, 서버장비, 스마트폰 등)을 식별할 수 있도록 각각의 장비에게 부여되는 고유주소이다. 즉, 변하지않고 컴퓨터에 고정적으로 부여된 IP(고유주소)로 TCP/IP 통신할 때 서버 컴퓨터 클라이언트 컴퓨터를 찾는 주소가된다.

[PORT]

포트는 소프트웨어 기반이며 컴퓨터의 운영체제에서 관리한다. 각 포트는 특정 프로세스 또는 서비스와 연결 시켜 준다.

즉, TCP/ IP에서 실행될 프로그램을 찾는 역할로 사용된다.

[소켓 Socket]

컴퓨터와 데이터를 송수신하려면 인터넷이라는 네트워크 망에 연결해야 한다.

프로그래밍에서 네트워크 망의 연결에 사용되는 도구가 소켓이다.

[****소켓을 이용한 서버와 클라이언트간의 전송 구조****]

기억하고 머리에 새겨야할 그림 !!!!!!

 

[SERVER 프로그램 구현]

#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 serv_sock; // 서버 소켓(ip와 port) 담을 정수값 (중복되지 않음)
	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)); // memeset (): file 버퍼에 있는 값을 0으로 초기화 시켜줌 
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); // 서버 주소(ip)를 변환시키는 방법
	serv_addr.sin_port=htons(atoi(argv[1]));    // 서버 포트를 변환 시키는 방법  (소켓에 담기 위해)
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )  // bind() : 송장에 적는다 보내는 주소 
		error_handling("bind() error"); 
	
	if(listen(serv_sock, 5)==-1)                        // listen() : 클라이언트 요청이 들어올 때까지 대기 시켜 놓는 작업
		error_handling("listen() error");
	/////////////////////////////////////////////////////////////////////////////////// 대기상태 (준비단계)
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);  // clnt_sock :  클라이언트 소켓connect()를  accept()로 받아와  서버안에 새로운 클라이언트 소켓 만들어짐 
	if(clnt_sock==-1)    ////connect()를 accept()로 받얐을 시 반환되는 값이 -1이면  실행 (즉, -1이면 함수가 실행되지 않았다는 의미 )
		error_handling("accept() error");  
	
	write(clnt_sock, message, sizeof(message));   // write() : 데이터를 전송하는 곳  (주고 받을 데이터를 write()함수로 공유 ) 매개변수로 (새로 만들어진 클라이언트 소켓, 문자열, 문자열 크기)
	close(clnt_sock);	// 데이터 공유 되면 클라이언트 소켓 종료시킴 
	close(serv_sock);  // 데이터 공유 되면 서버 소켓 종료 시킴
	return 0;
}

void error_handling(char *message) // 소켓이 생성되지 않으면 종료 시키는 함수 
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

여기서 알아야할 함수

* socket() : 소켓 생성 함수 / 성공 시 파일 디스크립터, 실패 시 -1반환

* bind() : 소켓의 주소정보 (IP와 PORT) 할당 함수 ex.)택배 운송장 역할 (보내는 주소 적기)

* listen() : 소켓의 연결 요청이 가능한 준비상태 만들어주는 함수

* accept() : 연결요청에 대한 수락 하는 함수

* write() : 데이터를 전송하는 함수 (소켓에 담아 데이터 공유)

 

[Client 프로그램 구현]

#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));  // 파일 버퍼에 있는 값 0으로 초기화
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));  // 위 3줄은 내가 접속할 서버의 정보(IP, PORT) 를 담는다.
		
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) // connect():서버와 연결 요청 보내는 곳 
		error_handling("connect() error!");
	
	str_len=read(sock, message, sizeof(message)-1);  // read() : 문자열 읽어오는 함수 (서버 write()를 읽어온다.) 매개변수로 (서버에 새로 만들어진 클라이언트 소켓 , 읽을 문자열, 문자열 크기)
	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() : 클라이언트 소켓을 서버 소켓에 연결요청 보내는 함수

* read() : 서버 write()함수를 읽어오는 함수

(서버에 accept()로 요청이 받아들여진 클라이언트 소켓을 새로 만들어지는데 그 소켓에 write담은 소켓을 read()로 읽어온다.)

 

[서버 실행 터미널]

** 먼저 실행 시켜야 된다!!!

gcc 실행 터미널에 포트 지정 9190으로 연결 커서가 깜빡깜빡 움직이면 포트 열어논 상태에서 클라이언트연결 되고 터미널에 write()보낸후 출력되면 포트 닫힘

"gcc hello_server.c -o hserver" : hello_server.c 파일을 컴파일해서 hserver라는 이름의 실행파일을 만드는 문장.

"./hserver" : 현재 디렉터리에 있는 hserver라는 이름의 파일을 실행시키라는 의미

[클라이언트 실행 터미널]

gcc 실행 터미널에 서버 IP주소와 포트를 적어 연결 "Message from server : Hello World!"출력 후 포트 닫힘

"./hclient 127.0.0.1 9190" : 컴퓨터 로컬 주소(127.0.0.1)내에서 서버와 클라이언트 프로그램을 모두 실행시키는 경우에 이러한 전달방식을 택한다.

저 수준 파일 입출력(Low-level File Access)과 파일 디스크립터(File Descriptor)

*** 꼭 이해하고 넘어갈 부분

1. 파일 디스크립터 란?

시스템으로부터 할당 받은 파일 또는 소켓에 부여된 정수를 의미한다.

즉, OS가 DB,키보드 ,모니터 등을 연결하는데 할당되는 정수값

"Process -> File에 접근 시 사용하는 추상적인 값"

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

▶ 약속된 정수값 / 프로그램이 실행되면 자동으로 할당되는 파일 디스크립터

[사용하는 이유는?]

서버에서 accept()로 받은 클라이언트 소켓을 새로운 파일 디스크립터로 정수값을 지정해주어 관리를 해야한다.

왜냐하면 accept()로 받은 클라이언트 소켓을 서버에서 다시 새로운 클라이언트 소켓으로 만들어지는데 다음 클라이언트를 받을때 덮어씌워지기 때문에 파일 디스크립트로 나눠 관리해야한다.

단, 약속된 이외의 값으로 정수 할당해주어야 한다.


파일열기

int open() : 데이터를 읽거나 쓰기 위해서 파일을 열 때 사용하는 함수

** 성공 시 파일 디스크립터, 실패 시 -1반환

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

int open(const char *path, int flag);

 

*path : 파일이름을 나타내는 문자열의 주소값 전달.

flag : 파일의 오픈 모드 정보 전달.

매개변수로 (대상이되는 파일의 이름 및 경로 정보 , 파일의 오픈모드 정보) 전달한다.

매개변수 flag에 전달할 수 있는 값과 그 의미는 다음과 같으며, 하나 이상의 정보를 비트 OR 연산자로 묶어서 전달이 가능하다.

[파일의 오픈모드]

오픈 모드
의미
O_CREAT
필요하면 파일을 생성
O_TRUNC
기존 데이터 전부 삭제
O_APPEND
기존 데이터 보존하고, 뒤에 이어서 저장
O_RDONLY
읽기 전용으로 파일 오픈
O_WRONLY
쓰기 전용으로 파일 오픈
O_RDWR
읽기, 쓰기 겸용으로 파일 오픈

 

파일닫기

int close() : 파일은 사용한 후 반드시 닫아줘야 한다. 파일을 닫을 때 호출하는 함수.

#include <unistd.h>

int close(int fd);

fd : 닫고자 하는 파일 또는 소켓의 파일 디스크립터 전달.

위 함수 호출하면서 파일 디스크립터를 인자로 전달하면 해당 파일은 닫히게(종료)된다. 소켓을 닫을 때에도 사용된다.

 

파일에 데이터 쓰기

ssize_t write() : 파일에 데이터를 출력(전송)하는 함수이다.

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);

fd : 데이터 전송대상을 나타내는 파일 디스크립터 전달.

buf : 전송할 데이터가 저장된 버퍼의 주소 값 전달.

nbytes : 전송할 데이터의 바이트 수 전달.

size_t : typedef 선언을 통해서 unsigned int로 정의 되어있다.

ssize_t : size_t앞에 s가 하나 더 붙어 signed를 의미한다. 즉, typedef 선언을 통해서 signed int로 정의 되어있다.

** 32비트 16비트 시스템 차이로 인해 자료형 표현방식이 달라지는데 이를 size_t와 ssize_t를 사용하여 4바이트 자료형이 필요한 곳에 코드의 변경을 최소화 할 수 있다. 나중에 size_t와 ssize_t의 typedef선언만 변경해 컴파일하면 되기 때문이다.

 

파일의 생성 및 데이터의 저장 예제

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

void error_handling(char* message);

int main(void)
{
	int fd;
	char buf[]="Let's go!\n";
	
	fd=open("data.txt", O_CREAT|O_WRONLY|O_TRUNC);
	if(fd==-1)
		error_handling("open() error!");
	printf("file descriptor: %d \n", fd);

	if(write(fd, buf, sizeof(buf))==-1)
		error_handling("write() error!");

	close(fd);
	return 0;
}

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

fd = open ("data.txt", O_CREAT | O_WRONLY | O_TRUNC );

open 매개변수 flag위치에 OR연산자로 3가지 오픈모드를 묶어서 전달한다.

※ O_CREAT | O_WRONLY | O_TRUNC : 아무것도 저장되어있지 않은 새로운 파일이 생성되어 쓰기만 가능하게 된다.

(단, data.txt라는 이름의 파일이 존재한다면, 이 파일의 모든 데이터는 지워져 버린다.)

if(write(fd, buf, sizeof(buf) == -1) : fd에 저장된 파일 디스크립터에 해당하는 파일에 buffer에 저장된 데이터를 전송하고 있다.

 

[실행 결과]

** permission denied 나오면 권한이 없어 그런것이니 sudo를 맨 앞에 붙이면 실행된다!

 

 

파일에 저장된 데이터 읽기

ssize_t read() : 데이터를 입력(수신)하는 기능의 함수.

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);

fd : 데이터 수신대상을 나타내는 파일 디스크립터 전달.

buf : 수신한 데이터를 저장할 버퍼의 주소값 전달.

nbytes : 수신할 최대 바이트 수 전달.

 

data.txt에 저장된 데이터를 read 함수를 이요해서 읽어들이는 예제

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define BUF_SIZE 100

void error_handling(char* message);

int main(void)
{
	int fd;
	char buf[BUF_SIZE];
	
	fd=open("data.txt", O_RDONLY);
	if( fd==-1)
		error_handling("open() error!");
	
	printf("file descriptor: %d \n" , fd);
	
	if(read(fd, buf, sizeof(buf))==-1)
		error_handling("read() error!");

	printf("file data: %s", buf);
	
	close(fd);
	return 0;
}

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

 

fd = open("data.txt", O_RDONLY); "data.txt를 읽기 전용으로 열었다."

if(read(fd, buf, sizeof(buf)) == -1) : read함수를 이용해서 char buf[BUF_SIZE]선언된 배열 buf에 읽어들인 데이터를 저장하고있다.

[실행 결과]

파일 디스크립터와 소켓 예제

파일도 생성하고, 소켓도 생성해본다. 그리고 반환되는 파일 디스크립터의 값을 정수형태로 비교해 보겠다.

#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 / fd3 : 하나의 파일과 두 개의 소켓을 생성하고 있다.

printf("file descriptor 1: %d\n", fd1); : 앞서 생성한 파일 디스크립터의 정수 값을 출력하고 있다.

[실행 결과]

0,1,2는 표준입출력에 이미 할당 되었기 때문에 일련의 순서대로 넘버링 3,4,5로 출력 되었다.


2. 소켓의 프로토콜과 그에 따른 데이터 전송특성

1. 프로토콜 이란?

"컴퓨터 상호간의 대화에 필요한 통신규약"

서로의 데이터를 주고 받기 위해 정의해 놓은 약속을 뜻한다.

소켓의 생성

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보전달.

type : 소켓의 데이터 전송방식에 대한 정보전달.

protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보전달

프로토콜 체계 (Protocl Family)

* 이런게 있다고만 알고 넘어가자 (IPv4인터넷 프로토콜 체계 알면 된다.)

socket 함수의 첫 번째 매개변수로, 생성되는 소켓이 사용할 프로토콜의 부류정보를 전달해야 한다.

[헤더파일 sys/socket.h에 선언되어 있는 프로토콜 체계]

이름
프로토콜 체계(Protocol Family)
PF_INET
IPv4 인터넷 프로토콜 체계
PF_INET6
IPv6 인터넷 프로토콜 체계
PF_LOCAL
로컬 통신을 위한 UNIX 프로토콜 체계
PF_PACKET
Low level 소켓을 위한 프로토콜 체계
PF_IPX
IPX노벨 프로토콜 체계

소켓의 타입 (Type)

* 이런게 있다고만 알고 넘어가자 (IPv4인터넷 프로토콜 체계 알면 된다.)

소켓의 전송방식으로 socket함수의 두번째 매개변수로 전달해야 한다.

1. 연결 지향형 소켓 (SOCK_STREAM)

▶ 중간에 데이터가 소멸되지 않고 목적지로 전송된다.

▶ 전송 순서대로 데이터가 수신된다.

▶ 전송되는 데이터의 경계(Boundary)가 존재하지 않는다.

2. 비 연결지향형 소켓(SOCK_DGRAM)

▶ 전송된 순서에 상관없이 가장 빠른 전송을 지향한다.

▶ 전송된 데이터는 손실의 우려가 있고, 파손의 우려가 있다.

▶ 전송되는 데이터의 경계(Boundary)가 존재한다.

▶ 한번에 전송할 수 있는 데이터의 크기가 제한된다.

"IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓"

#include <sys/socket.h>

int tcp_socket(PF_INET,  SOCK_STREAM, IPPROTO_TCP);

 

"IPv4 인터넷 프로토콜 체계에서 동작하는 비 연결지향형 데이터 전송 소켓"

#include <sys/socket.h>

int udp_socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

 

 

연결 지향형 소켓! TCP 소켓의 예제

* 이런게 있다고만 알고 넘어가자 (IPv4인터넷 프로토콜 체계 알면 된다.)

"전송되는 데이터의 경계 (Boundary)가 존재하지 않는다."

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.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=0;
	int idx=0, read_len=0;
	
	if(argc!=3){
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0); // TCP_소켓을 생성한다. IPPROTO_TCP는 생략가능
	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!");

	while(read_len=read(sock, &message[idx++], 1)) // read함수를 반복호출 / 이 함수가 호출될 때마다 1바이트 씩 데이터를 읽어들인다.
	{                                                                                // read()함수가 0을 반환하면 거짓을 의미하기 때문에 while문을 빠져나간다.
		if(read_len==-1)
			error_handling("read() error!");
		
		str_len+=read_len;                                              // read_len에 저장되어 있는 값은 항상 1이다. 1바이트씩 읽고있기 때문이다. 
                                                                                   // 결국 while문을 빠져나간 이후에 str_len에는 읽어들인 바이트 수가 저장된다.
	}

	printf("Message from server: %s \n", message);
	printf("Function read call count: %d \n", str_len);
	close(sock);
	return 0;
}

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

 

 

3. 소켓에 할당되는 IP주소와 PORT번호

[IP주소체계]

IPv4 (Internet Protocol version 4) : 4바이트 주소 체계

"255.255.255.255" 범위로 이루어져 있다.

IPv6(Internet Protocol version 6) : 16바이트 주소 체계

"DECF : DCBA : 1234 : 4567 : BBAA : CCDD : CDEE : FFFF "범위로 이루어져 있다.

[IP주소 기반의 데이터 전송과정]

3-1. 소켓의 구분에 활용되는 PORT번호

▶ PORT 번호는 하나의 운영체제 내에서 소켓을 구분하는 목적으로 사용되기 때문에 하나의 운영체제 내에서 동일한 PORT번호를 둘 이상의 소켓에 할당할 수 없다.

▶ PORT 번호는 16비트로 표현된다. 할당할 수 있는 포트번호 범위는 0이상 65535이하이다. 그러나 0부터 1023번까지는 잘알려진 PORT(Well-known-PORT)라고 해서, 특정 프로그램에 할당하기로 예약 되어있기 때문에, 제외하고 써야한다.

▶ 중복이 불가하다.

*** 우리가 흔히 말하는 데이터 전송의 목적기 주소에는 IP주소뿐만 아니라 PORT번호도 포함되어 전송되어야지 응용프로그램 소켓까지 데이터를 전달할 수 있다.


3-3. 바이트 순서의 변환(Endian Conversions)

▶ unsigned short htons(unsigned short); "short형 데이터를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환해라!"

▶ unsigned short ntons(unsigned short); "short형 데이터를 네트워크 바이트 순서에서 호스트 바이트 순서로 변환해라!"

** 뒤에 s가 붙는 함수는 s가 2바이트 short를 의미하므로 PORT번호의 변환에 사용된다.

▶ unsigned long htonl(unsigned long);

▶ unsigned long ntonl(unsigned long);

** 뒤에 l이 붙는 함수는 l이 4바이트 Long을 의미하므로 IP주소의 변환에 사용된다.

Short : 2바이트 / Long : 4바이트

*** htons(host to network short) => 'h'는 호스트(host) 바이트 순서를 의미한다.

*** ntons(network to host short) => 'n'은 네트워크(network) 바이트 순서를 의미한다.

3-4. 인터넷 주소의 초기화와 할당

[인터넷 주소의 초기화]

소켓 생성과정에서 흔히 등장하는 인터넷 주소정보의 초기화 방법

struct sockaddr_in addr;  
char *serv_ip = "211.217.168.13"; //  IP주소 문자열 선언
char *serv_port = "9190"; //PORT번호 문자열 선언
memset(&addr, 0, sizeof(addr)); //구조체 변수addr의 모든 멤버 0으로 초기화
addr.sin_family = AF_INET; //주소체계지정
addr.sin_addr.s_addr = inet_addr(serv_ip); // 문자열 기반의 IP주소 초기화
addr.sin_port = htons(atoi(serv_port)); //문자열 기반의 PORT번호 초기화

memset()

▶ 동일한 값으로 바이트단위 초기화 할때 호출하는 함수이다.

▶ 위의 코드에서는 문자열로 표현된 IP주소와 PORT번호를 기반으로 하는 sockaddr_in구조체 변수의 초기화 과정이다.

▶ 서버 프로그램에서 주로 등장한다.

[클라이언트 주소정보 초기화]

서버에서 보내주는 IP / PORT 번호를 받아 연결요청용 소켓을 만든다.

[INADDR_ANY]

서버 소켓의 생성과정에서 매번 서버IP주소를 입력하는 것은 귀찮은 일이 될 수 있다. INADDR_ANY를 이용하여 주소정보를 초기화 해도 된다.

struct sockaddr_in addr;  
char *serv_port = "9190"; //PORT번호 문자열 선언
memset(&addr, 0, sizeof(addr)); //구조체 변수addr의 모든 멤버 0으로 초기화
addr.sin_family = AF_INET; //주소체계지정
addr.sin_addr.s_addr = hton1(INADDR_ANY); // 문자열 기반의 IP주소 초기화
addr.sin_port = htons(atoi(serv_port)); //문자열 기반의 PORT번호 초기화

▶ 소켓의 IP주소를 이렇게 초기화 할 경우 소켓이 동작하는 컴퓨터의 IP주소가 자동으로 할당되기 때문에 IP주소를 직접 입력하는 수고를 덜 수 있다.

▶ 할당받은 IP중 어떤 주소를 통해서 데이터가 들어오더라도PORT번호만 일치하면 수신 할 수 있다.

*** 서버 프로그램의 구현에 많이 선호되는 방법이다.

[소켓에 인터넷 주소 할당하기]

bind()함수의 역할 : 초기화된 주소정보를 소켓에 할당하는 일만 남았다.

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

sockfd : 주소정보를 (IP와 PORT를) 할당할 소켓의 파일 디스크립터.

myaddr : 할당하고자 하는 주소정보를 지니는 구조체 변수의 주소 값.

addrlen : 두번째 매개변수로 전달된 구조체 변수의 길이정보.

위 함수호출이 성공하면, 첫번째 매개변수에 해당하는 소켓에 두번째 매개변수로 전달된 주소정보가 할당된다.

******서버 소켓 초기화 과정 ******

서버 프로그램이라면 아래의 코드구성을 기본적으로 갖추게 된다.

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";

/* 서버 소켓(리스닝 소켓) 생성 */
serv_sock = socket(PF_INET, SOCK_STREAM, 0);

/* 주소정보 초기화 */
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(serv_port));

/* 주소정보 할당*/
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));