hello_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void handle_error(char *);
int main(int argc, char *argv[])
{
int listenFd;
int connFd;
struct sockaddr_in serverAddress, clientAddress;
socklen_t clientAddressLen;
char message[] = "Hello World in Network Programming.";
if(argc != 2){
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
if((listenFd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
handle_error("socket() error.");
memset(&serverAddress, 0, sizeof(serverAddress));
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddress.sin_port = htons(atoi(argv[1]));
if(bind(listenFd, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) < 0)
handle_error("bind() error");
// sleep(10);
if(listen(listenFd, 5) < 0)
handle_error("listen() error.");
// sleep(10);
clientAddressLen = sizeof(clientAddress);
if((connFd = accept(listenFd, (struct sockaddr *)&clientAddress, &clientAddressLen)) < 0) //
NULL/NULL
handle_error("accept() error.");
write(connFd, message, sizeof(message));
// printf("Size of message: %d\n", sizeof(message));
printf("Size of message: %zu bytes\n", sizeof(message)); //ld, lu possible
close(connFd);
close(listenFd);
return 0;
}
void handle_error(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
hello_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define MESSAGE_SIZE 256
void handle_error(char *);
int main(int argc, char* argv[])
{
int sockFd;
struct sockaddr_in serverAddress;
char message[MESSAGE_SIZE];
int messageLen;
if(argc != 3){
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
ch1/hello_client.c
if(connect(sockFd, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) < 0)
handle_error("connect() error.");
if((sockFd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
handle_error("socket() error.");
memset(&serverAddress, 0, sizeof(serverAddress));
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = inet_addr(argv[1]);
serverAddress.sin_port = htons(atoi(argv[2]));
// printf("connect() returns.\n");
if((messageLen = read(sockFd, message, sizeof(message))) < 0)
handle_error("read() error.");
printf("Message from server: %s, %d bytes (Max %d bytes)\n", message, messageLen,
MESSAGE_SIZE);
close(sockFd);
return 0;
}
void handle_error(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
각각 서버와 클라이언트를 구축하여 간단한 통신을 진행하는 내용이었습니다.
먼저 서버를 담당한 hello_server.c. 코드부터 살펴보겠습니다.
헤더는 입출력을 진행하기위한 라이브러리들과 인터넷 및 소켓 라이브러리를 사용하기 위한 부분으로 나누어집니다. 특이한점은 unistd라는 헤더는 처음 사용해보아서 찾아본 결과 유닉스 표준 관련 함수를 제공하는 라이브러리 였습니다.
Ex) read,write,close등…. 즉 기존 윈도우에서 진행할때는 i/o.h 와 같은 내용이었습니다.
위쪽에 에러를 핸들링하는 함수에 대한 선언을 미리 해주었는데, c언어 이기 때문에 main함수에서 호출하기 위해 컴파일러에게 함수의 존재를 알려주기 위함입니다.
내용을 정의하지 않은건 코드 가독성?을 위한 것으로 생각됩니다.
소켓 프로그래밍을 하기위한 변수와 구조체, 통신할 문자열등을 정의헀습니다
일단 클라이언트의 연결 요청을 듣기 위해 사용하는 리슨 파일디스크립터를 저장할 변수와 실제 통신할 클라이언트와 통신하기 위해 사용할 커넥터 소켓의 파일 디스크립터를 선언합니다.
또한 서버와 클라이언트의 주소를 저장할 sockaddr_in 구조체를 각각 선언합니다.
이러한 구조체를 이용해 각각의 IP주소와 포트번호를 저장합니다. 클래스가 아닌 구조체구조체 사용하는 것이 재미있는 것 같습니다. 즉 한번 할당하면 수정이 어려운 코드라 생각이 됩니다.
그 외에 클라이언트 주소의 길이를 저장하기 위한 socklen_t 타입의 변수(소켓 프로그래밍에서의 주소 길이를 처리할 때 이용)와 실제 전송에 사용할 문자열등을 선언합니다.
프로그램이 실행될 때, 올바른 인자의 개수가 전달되었는지 검사하는 조건문입니다.
여기서는 인자가 2개가 아닐경우(./hello_server 포트번호가 아닐경우) 종료합니다.
리스닝 소켓의 생성이 잘못되었을때,( -1이 반환되면 오류) 에러처리를 발생시킵니다.
serverAddress구조체의 모든 필드를 0 으로 초기화 하고
실제 통신에 사용할 주소를 설정합니다. 서버 IP주소와 포트를 할당합니다.
이제 서버 리슨 소켓을 생성한 후, 위에서 정의한 serverAddress에 정의된 주소와 포트로 소켓을 바인딩합니다. bind함수를 이용해서 바인딩을 진행합니다.
즉 listenFd에 serverAddress의 내용을 이용하여 바인딩을 한다는 의미입니다.
바인딩 후, listen메소드에 리스닝할 소켓을 전달하여 실제 리스닝을 시작합니다.
listenFd을 이용하여 리스닝을 진행하며, 최대 5명까지 연결을 진행할 수 있습니다.
하지만 아직 클라이언트를 감지할 수 있을 뿐 연결은 하지 않습니다. 소켓은 리스닝상태에 들어갑니다.
실제로 클라이언트와 연결하고, 메시지를 전송하는 과정입니다.
먼저 clientAddressLen변수에 client주소 구조체의 크기를 저장합니다. 클라이언트 주소길이를 왜 알아야 할까…라는 고민이 들어 조사해 보았으나. 일단은 accept과정에서 연결할 클라이언트의 주소 길이를 요구하는구나… 라고 생각하고 넘어갔습니다.
이제 클라이언트의 요청이 감지되면 accept메소드를 이용하여 클라이언트의 연결 요청을 수락하고, 새로 생성된 연결을 위한 소켓을 받습니다.
accept함수는 소켓 파일 디스크립터를 반환하며, 이 디스크립터를 이용하여 클라이언트와 소통할 수있습니다.
Write메소드를 이용하여 클라이언트 소켓(connFd)에 데이터를 전송합니다. 위에서 설정한 message 배열의 내용을 클라리언트에게 전송합니다.
특이한점은 전송할 데이터의 크기 역시 전송하는데, 아무래도 클라이언트측에서 데이터를 조립하는데 사용하지 않을까 라는 추측을 해보았습니다.
성공적으로 전송하면 전송한 메시지의 사이즈를 출력하고 사용한 소켓 파일디스크립터를 종료합니다.
다음으로 클라이언트측 코드를 살펴보겠습니다.
헤더파일은 서버측과 크게 다르지 않은 모습입니다. 굳이 다른 부분은 #define을 이용하여 매크로 상수를 정의한 부분입니다(이부분은 read에 사용합니다)
통신에 사용할 소켓의 파일 디스크립터, 서버의 주소를 저장하는 구조체, 서버로부터 메세지를 읽어들일 메시지를 저장할 버퍼, 읽어들인 메시지의 길이를 저장할 변수를 선언합니다.
인자가 3개가 아닐경우 해당 메시지를 띄우고 종료시킵니다. 1개가 추가된 이유는 서버의 ip를 알고 있어야 하기 때문입니다.
socket메소드를 이용하여 통신에 사용할 소켓 파일 디스크립터를 생성합니다.또한 에러가 발생할 경우 에러핸들링을 진행합니다.
서버 주소 초기화를 진행하는 부분입니다. serverAddress구조체를 초기화 후, 네트워크 주소와 포트 주소를 설정해줍니다.
Connect 함수에 serverAddress정보를 전달해 실제 서버와 연결을 시도합니다.
서버측에서는 클라이언트의 접속이 감지될 경우 메시지를 전송하도록 코딩했습니다.
클라이언트측에서는 read 메소드를 이용하여 서버에서오는 메시지를 저장하고, 메시지의 길이또한 저장합니다.
메시지를 읽는 부분까지 성공했다면 메시지를 출력 후, 사용한 파일 디스크립터를 종료합니다.
실제 실행을 위해서는 gcc를 이용해 컴파일 후 실행파일을 만들어줍니다.
gcc -o hello_client hello_client.c 등의 명령어를 사용합니다.
실행은 ./hello_server 포트번호 와 ./hello_client ip주소 포트번호로 각각 실행 가능하며
저는 ./hello_server 60001 ./hello_client 127.0.0.1 60001로 실행했으며 결과는 다음과 같습니다.
서버는 실행 후 클라이언트의 접속을 기다리며, 클라이언트가 접속하면 보낸 메시지의 크기를 전송 후 종료합니다. 클라이언트는 접속할 서버의 ip와 포트번호를 설정하여 접속 할 수 있으며, 서버로부터 받은 메시지와 크기를 출력하고 종료합니다.
서버는 36bytes를 전송하고 클라이언트는 36bytes를 수신한 모습입니다. 손실없이 잘 전송된 모습니다.
'프로그래밍 > 2024 네트워크 프로그래밍' 카테고리의 다른 글
6. Address conversion (0) | 2025.01.06 |
---|---|
5. inet_addr 살펴보기 (0) | 2025.01.06 |
4. 빅 엔디안, 리틀 엔디안 (0) | 2025.01.06 |
3. c에서의 파일복사 (0) | 2025.01.03 |
2. 파일 디스크립터에서 파일 열기/읽기/쓰기/닫기 (1) | 2025.01.03 |