-
[리눅스 네트워크] - 1 : echo 서버 만들기프로그래밍/컴퓨터 네트워크 2024. 4. 5. 00:44
네트워크 프로그래밍이란?
- 네트워크 프로그래밍은 시스템 프로그래밍의 연장선이다!
- 컴퓨터의 경우, 랜선이라 부르는 이더넷 케이블을 통해 Data가 송/수신 되고 있다.
- 이때, 케이블은 H/W 장치이기 때문에 케이블을 통해 송/수신 되는 Data에 접근하기 위해서는 kernel의 도움이 있어야 한다.
- 따라서, 네트워크 프로그래밍 == system call을 이용해 네트워킹을 위한 코드를 작성하고, 실행하는 것이다!
Socket 프로그래밍 코드 살펴보기 - server
Server의 동작 순서
- socket() : 소켓 생성
- bind() : 생성한 소켓에 주소 할당
- listen() : 클라이언트 연결 요청 대기
- accept() : 클라이언트 연결 승인
- read() / write() : 소켓 통신 (메시지 수신, 송신)
- close() : 소켓 닫기. 종료.
Server 동작 코드
[전체코드 보기]
더보기더보기#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <signal.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> const char *PORT = "12345"; int server_sock; int client_sock; void interrupt(int arg){ printf("\nYou typed Ctrl + C\n"); printf("Bye\n"); close(client_sock); close(server_sock); exit(1); } void removeEnterChar(char *buf){ int len = strlen(buf); for (int i = len - 1; i >= 0; i--){ if (buf[i] == '\n'){ buf[i] = '\0'; break; } } } int main(){ signal(SIGINT, interrupt); server_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_sock == -1){ printf("ERROR :: 1_Socket Create Error\n"); exit(1); } printf("Server On..\n"); int optval = 1; setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval)); struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(atoi(PORT)); socklen_t server_addr_len = sizeof(server_addr); if (bind(server_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1){ printf("ERROR :: 2_bind Error\n"); exit(1); } printf("Bind Success\n"); if (listen(server_sock, 5) == -1){ printf("ERROR :: 3_listen Error"); exit(1); } printf("Wait Client...\n"); client_sock = 0; struct sockaddr_in client_addr = {0}; socklen_t client_addr_len = sizeof(client_addr); while (1){ memset(&client_addr, 0, client_addr_len); client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len); if (client_sock == -1){ printf("ERROR :: 4_accept Error\n"); break; } printf("Client Connect Success!\n"); char buf[100]; while (1){ memset(buf, 0, 100); int len = read(client_sock, buf, 99); removeEnterChar(buf); if (len == 0){ printf("INFO :: Disconnect with client... BYE\n"); break; } if (!strcmp("exit", buf)){ printf("INFO :: Client want close... BYE\n"); break; } write(client_sock, buf, strlen(buf)); } close(client_sock); printf("Client Bye!\n"); } close(server_sock); printf("Server off..\n"); return 0; }
1-1. 소켓생성 : server_sock = socket(AF_INET, SOCK_STREAM, 0);
int socket(int domain, int type, int protocol)
- 기능 : 입력받은 주소 체계(domain), 소켓 유형(type), 프로토콜(protocol)에 맞는 소켓을 생성함.
- 입력값
- domain : 프로토콜 도메인
- AF_INET : IPv4
- AF_INET6 : IPv6
- AF_UNIX : 로컬통신 등...
- type : 소켓의 유형
- ex) SOCK_STREAM : TCP
- SOCK_DGRAM : UDP 등...
- protocol : 프로토콜 종류 ex) IPPROTO_TCP / IPPROTO_UDP 등... => 주로 0으로 설정하여 시스템이 적절한 프로토콜을 자동으로 선택 하도록 한다.
- domain : 프로토콜 도메인
- 반환값 : 생성된 소켓의 파일 디스크립터 (= 소켓을 식별하는 데 사용되는 정수. 식별자), 실패하면 -1을 반환한다.
- 라이브러리 : #include <sys/socket.h>
1-2. 소켓 옵션 지정 : setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval));
int setsockopt(int socket, int level, int optname, void *optval, socklen_t *optlen)
- 기능 : 소켓 옵션을 설정
- 입력값
- socket descriptor : 소켓 파일 디스크립터 (식별자) = > socket()함수의 반환값으로 얻음
- level : 프로토콜 수준
- ex) SOL_SOCKET : 소켓의 일반적인 레벨을 다룸
- IPPROTO_IP : IPv4 소켓에 대해, IP 프로토콜관련 옵션을 설정
- IPPROTO_TCP : TCP 프로토콜 레벨을 다룸 => 주로 SOL_SOCKET으로 설정!
- optname : setsockopt() 함수에서 설정하려는 특정 옵션을 식별하는 상수 값
- ex) SO_REUSEADDR : 주소 재사용 옵션. 소켓 종료 후에도 같은 주소와 포트를 빠르게 재사용 할 수 있음.
- SO_KEEPALIVE : 소켓 간 주기적인 keep-alive 패킷이 전송된다.
- SO_LINGER : close() 호출 시 보류 상태로 머무르는 시간을 제어
- 등...
- *optval : setsockopt() 함수에서 사용되는 옵션 값을 저장하는 포인터. 설정하려는 옵션 값의 주소를 가리킨다. 옵션 값은 다양한 데이터 형식일 수 있으므로 void 포인터로 전달된다.
- optlen : setsockopt() 함수에서 사용되는 옵션 값의 크기(byte)를 나타내는 변수. => 보통 이 값은 sizeof() 연산자를 사용하여 옵션 값의 크기를 계산하여 설정한다.
1-3. 소켓 구조체(sockaddr_in) 지정 : struct sockaddr_in { ~~ }
struct sockaddr_in { short int sin_family; // 주소 체계(AF_INET) unsigned short int sin_port; // 포트 번호 struct in_addr sin_addr; // IPv4 주소 unsigned char sin_zero[8]; // 소켓 구조체의 크기를 맞추기 위한 추가 공간 }; ================================================================================= struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(atoi(PORT)); socklen_t server_addr_len = sizeof(server_addr);
- 기능 : socket 의 주소 정보를 담당하는 구조체, IPv4 주소 체계를 사용하는 경우, sockaddr_in 구조체를 사용한다.
- 입력값
- sin_family : 주소체계. AF_INET = IPv4를 의미한다.
- sin_addr : 소켓의 IP 주소를 나타낸다. 이때, 네트워크 바이트는 빅 엔디안을 사용하고, 호스트 바이트는 리틀 엔디안을 사용하기 때문에 htonl()함수를 이용해 IPv4주소를 빅엔디안으로 변경해준다.
- 여기서, INADDR_ANY는 소켓을 바인딩할 때 사용되는 특별한 IPv4 주소로 "모든 네트워크 인터페이스"를 나타낸다. => 모든 네트워크 인터페이스에 대한 연결을 수락할 수 있도록 한다.
- sin_port : 소켓의 PORT 번호를 나타낸다. 마찬가지로, htons() 함수를 통해 PORT를 빅엔디안으로 변경해주어야 한다.
- sin_zero : 구조체 크기를 맞추기 위한 추가 공간(padding 느낌..?). 실제로 사용되지 않지만, 구조체의 크기를 맞추기 위해 포함됨.
2. 생성한 소켓에 주소 할당 : bind(server_sock, (struct sockaddr *)&server_addr, server_addr_len)
int bind(int socket, const struct sockaddr*, socklen_t)
- 기능 : 소켓을 특정 주소에 연결하여 특정 IP, PORT에 바인딩시키고 소켓에 주소 할당.
- 입력값
- socket : 바인딩할 소켓 파일 디스크립터(=식별자) => socket()함수의 반환값으로 얻음
- sockaddr* : 바인딩할 주소를 나타내는 sockaddr 구조체에 대한 포인터. (앞서 살펴본 구조체!)
- : 주소 구조체의 크기를 나타내는 변수. 일반적으로 sizeof(소켓 구조체) 형태로 쓰임 sizeof(struct sockaddr_in)
- 반환값 : bind() 성공시 0, 실패시 -1
- 라이브러리 : #include <sys/socket.h> 필요
3. client의 연결 요청 기다림 : listen(server_sock, 5)
int listen(int socket, int backlog)
- 기능 : 소켓을 수신 대기 상태로 전환하여 연결을 받아들일 수 있도록 준비. TCP 서버 프로그램에서 클라이언트의 연결 요청을 수신할 때 사용된다.
- 입력값
- socket : 수신 대기 상태로 전환할 소켓을 나타내는 소켓 디스크립터 = > socket() 함수의 반환값
- backlog : 대기열의 최대 길이를 지정. 동시에 처리할 수 있는 요청의 최대수
- 반환값 : listen() 성공시 0, 실패시 -1
- 라이브러리 : #include <sys/socket.h> 필요
4-1. 클라이언트 소켓 선언
client_sock = 0; struct sockaddr_in client_addr = {0}; socklen_t client_addr_len = sizeof(client_addr);
- Server Socket에서 데이터를 주고 받을 Client 용 Socket을 설정 (소켓 디스크립터, sockaddr_in 구조체, 구조체 len)
- => 만약, 여러 client와 연결하고자 한다면, 각각을 모두 배열 형식으로 선언해주면 된다!
4-2. 클라이언트 연결 수락 및 소켓 생성 : accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
int accept(int sock, struct sockaddr*, socklen_t)
- 기능 : 클라이언트의 연결 요청이 있을 때까지 블로킹 된다!
- 클라이언트의 연결 요청을 수락하고, 해당 소켓 생성 (socket + struct 지정 + bind() 기능의 통합)
- 서버 소켓의 경우, "소켓 생성(socket()) > 서버 소켓 구조체 정의 > 주소 할당(bind())" 을 모두 차례대로 해주어야 했다.
- 하지만, 서버 소켓 생성 이후 클라이언트를 생성할 때는 이미 서버를 통해 구성된 틀이 있으므로 accept 함수만 호출해주어도 구조체에 알맞게 할당되고, 알맞은 클라이언트 소켓 디스크립터를 반환 받을 수 있다.
- 입력값
- sock : 클라이언트의 요청을 수락할 서버 소켓의 디스크립터 (socket, bind, listen이 되어 있어야 한다.)
- sockaddr* : 클라이언트의 주소 정보를 담을 구조체에 대한 포인터 (클라이언트의 IP 주소와 포트 번호 등 저장)
- socklen_t : 클라이언트 주소 구조체의 크기를 나타내는 변수 포인터
- 반환값 : accept() 성공시 새로운 클라이언트의 파일 디스크립터를 반환. 실패시 -1
- 라이브러리 : #include <sys/socket.h> 필요
====================여기까지, 통신을 위한 sever & client의 소켓 생성이 완료 되었다.===========
이후에는 생성된 소켓을 이용해 파일 입출력 (read, write)처럼 메시지를 주고 받으면 된다!
5. read() / write() 송수신 기능 (=서버의 역할)
while (1){ memset(&client_addr, 0, client_addr_len); client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len); if (client_sock == -1){ printf("ERROR :: 4_accept Error\n"); break; } printf("Client Connect Success!\n"); char buf[100]; while (1){ memset(buf, 0, 100); int len = read(client_sock, buf, 99); removeEnterChar(buf); if (len == 0){ printf("INFO :: Disconnect with client... BYE\n"); break; } if (!strcmp("exit", buf)){ printf("INFO :: Client want close... BYE\n"); break; } write(client_sock, buf, strlen(buf)); } close(client_sock); printf("Client Bye!\n"); }
5-1. 소켓통신을 통한 메시지 수신 : read(client_sock, buf, 99);
ssize_t read(int fd, void *buf, size_t count);
- 기능 : 디스크립터로부터 데이터를 읽어 buf에 저장한다. (=메시지 수신)
- 입력값
- fd : 데이터를 읽을 디스크립터
- buf : 읽은 데이터를 저장할 버퍼의 포인터
- count : 읽을 최대 바이트 수를 나타내는 크기
- 반환값 : 성공 시: 읽은 바이트 수를 반환 / 실패시 -1을 반환
- read()함수는 디스크립터에 데이터가 도착하기 전까지 블로킹된다.
- 이때, 일반적으로 read함수는 파일의 끝에 도달할 때 0을 반환하지만, 소켓통신에서 read()함수가 0을 반환하는 경우는 해당 소켓 디스크립터가 종료된 경우를 의미한다!!
- 따라서, read()의 반환값이 0이라면, 해당 클라이언트와의 연결이 끊긴 것으로 간주하고 해당 클라이언트 소켓을 close()시켜야 한다.
5-2. 소켓통신을 통한 메시지 송신 : write(client_sock, buf, strlen(buf));
ssize_t write(int fd, const void *buf, size_t count);
- 기능 : 파일 디스크립터에 buf에 저장된 데이터를 쓴다. (= 메시지 송신)
- 입력값
- fd : 데이터를 쓸 디스크립터
- buf : 쓸 데이터가 저장된 버퍼의 포인터
- count : 쓸 바이트 수를 나타내는 크기
- 반환값 : 성공 시: 쓴 바이트 수를 반환 / 실패 시: -1을 반환
[동작 설명]
- 첫 번째 while(1)문
- 이때, 특정 client와의 연결이 끊기더라도 서버는 다른 클라이언트와 새롭게 연결 될 수 있으므로, 서버 소켓이 살아있는 한 다른 클라이언트와의 연결을 탐색해야 한다.
- 따라서 while(1)안에 accept()함수를 넣어 서버 소켓이 살아있는 한 다른 클라이언트와의 연결을 탐색할 수 있도록 한다.
- 두번째 whlie(1)문
- 클라이언트와의 연결이 성공하여 두번째 while(1)문 안으로 들어오면, 해당 클라이언트와의 연결이 끊어지거나 (len == 0) 해당 클라이언트가 exit 문자열을 보내기 전까지 계속 무한하게 클라이언트가 보낸 메시지를 read하고 write해주어야 한다.
- 따라서 while(1)문 안에 해당 클라이언트에 대한 서버의 동작을 넣어주어 클라이언트와의 통신이 끝나지 않는 한 무한히 동작하게 해준다.
6-1. 클라이언트 소켓 닫기, 종료 : close(client_sock);
int close(int sockfd)
- 기능 : 전달받은 디스크립터를 닫는데 사용된다. 여기서는 소켓 디스크립터를 전달하여 소켓을 종료시킨다.
- client 소켓에서 읽은 메시지 len == 0
- 입력값 : 닫을 소켓의 디스트립터
- 반환값 : close() 성공시 0, 실패시 -1을 반환한다.
6-2. 서버 소켓 닫기, 종료 : signal(SIGINT, interrupt);
signal(int signum, void (*handler)(int))
- 기능 : 프로그램이 signum에 해당하는 신호를 받았을 때 실행될 handler함수를 등록한다.
- 입력값
- signum : 등록할 시그널 번호 => 핸들러 함수의 첫번째 인자로 전달된다!
- * handler : 시그널을 받았을 때 호출될 핸들러 함수의 포인터
- 반환값 : 성공 시: 이전의 시그널 핸들러 함수의 포인터를 반환 / 실패 시: SIG_ERR을 반환
[작성한 interrupt() 함수]
void interrupt(int arg){ printf("\nYou typed Ctrl + C\n"); printf("Bye\n"); close(client_sock); close(server_sock); exit(1); }
- arg에는 전달한 signum인 SIGINT가 들어온다.
- 함수가 실행되면, 먼저 client 소켓을 닫은 후 server 소켓도 닫는다.
- 모든 소켓을 닫은 후, exit()함수를 통해 프로그램을 종료시킨다.
- 이때, exit(int status)함수에 인자를 전달하여 프로그램의 종료 상태를 외부로 전달할 수 있다.
- 여기서 status = 1은 "일반적인 오류" , "비정상적인 종료"를 의미한다.
Socket 프로그래밍 코드 살펴보기 - client
client 동작 순서
- socket() 소켓 생성
- connect() 연결 요청
- read() / write() 데이터 송수신
- close() 연결 종료
client 코드 분석
[전체 코드 보기]
더보기더보기//echo_client #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> const char *SERVER_IP = "127.0.0.1"; const char *SERVER_PORT = "12345"; int client_sock; void interrupt(int arg){ printf("\nYou typped Ctrl + C\n"); printf("Bye\n"); close(client_sock); exit(1); } int main(){ signal(SIGINT, interrupt); client_sock = socket(AF_INET, SOCK_STREAM, 0); if (client_sock == -1){ printf("ERROR :: 1_Socket Create Error\n"); exit(1); } //printf("Socket Create!\n"); struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(atoi(SERVER_PORT)); socklen_t server_addr_len = sizeof(server_addr); if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1){ printf("ERROR :: 2_Connect Error\n"); exit(1); } //printf("Connect Success!\n"); char buf[100]; while (1){ memset(buf, 0, 100); scanf("%s", buf); if (!strcmp(buf, "exit")){ write(client_sock, buf, strlen(buf)); break; } write(client_sock, buf, strlen(buf)); memset(buf, 0, 100); int len = read(client_sock, buf, 99); if (len == 0){ printf("INFO :: Server Disconnected\n"); break; } printf("%s\n", buf); } close(client_sock); //printf("Logout\n"); return 0; }
1-1. 소켓 생성 : client_sock = socket(AF_INET, SOCK_STREAM, 0);
- 앞서 살펴본 server 소켓 생성과 동일하다.
- 소켓 생성후, 해당 소켓의 디스크립터가 client_sock 변수에 저장된다.
1-2. 접속할 서버의 sockaddr_in 구조체 생성
struct sockaddr_in server_addr = { 0 }; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(atoi(SERVER_PORT)); socklen_t server_addr_len = sizeof(server_addr);
- 서버에서 sockaddr_in 구조체를 생성해준 것 처럼, client 측에서도 접속할 서버의 구조체를 생성해주어야 한다.
- (코드는 서버측과 동일한다.)
2. socket 연결하기 : connect(client_sock, (struct sockaddr*)&server_addr, server_addr_len)
int connect(int socket, const struct sockaddr*, socklen_t)
- 기능 : 클라이언트 소켓을 서버에 연결하는 데 사용된다.
- 전달받은 클라이언트의 소켓 디스크립터와 연결할 서버 소켓의 구조체 정보를 기반으로 서버에 연결 요청을 보내 연결을 수립한다.
- 해당 클라이언트 소켓은 server의 클라이언트 소켓과 연동되어 있다.
- 입력값
- socket : 클라이언트 소켓의 파일 디스크립터
- sockaddr* : 연결할 서버의 주소 정보를 담고 있는 struct sockaddr_in 구조체의 포인터
- socklen_t : 서버 소켓 구조체의 크기
- 반환값 : 성공시 0 / 실패시 -1 반환
3. read() / write() 데이터 송수신 기능
char buf[100]; while (1){ memset(buf, 0, 100); scanf("%s", buf); if (!strcmp(buf, "exit")){ write(client_sock, buf, strlen(buf)); break; } write(client_sock, buf, strlen(buf)); memset(buf, 0, 100); int len = read(client_sock, buf, 99); if (len == 0){ printf("INFO :: Server Disconnected\n"); break; } printf("%s\n", buf); } close(client_sock);
- 사용자의 입력을 받아 buf에 저장한다.
- 만약 입력값이 exit이라면 client소켓을 종료시킨다(close)
- 아니라면, buf에 저장된 사용자가 입력한 문자열을 wirte()함수를 통해 소켓 디스크립트에 작성한다. (=메시지 송신)
- read()함수를 통해 clietn_sock에 있는 문자열을 읽어온다 (=메시지 수신)
- client_sock연결이 끊겼다면(len == 0), client 소켓을 종료시킨다(close)
4. close() 종료시키기 & interrupt
signal(SIGINT, interrupt);
- 마찬가지로,signal()함수를 호출하여 interrupt가 발생하였을 때, interrupt 함수를 실행시켜준다.
'프로그래밍 > 컴퓨터 네트워크' 카테고리의 다른 글
[Linux] Time #date, hwclock, gettimeofday, time, localtime, clock (0) 2024.04.05 [리눅스 네트워크] - 2 : 채팅 서버 만들기 (feat. multi thread) (0) 2024.04.05 - 네트워크 프로그래밍은 시스템 프로그래밍의 연장선이다!