이번 글에서는 클라이언트와 서버로 구성된 시스템을 만드는 방법을 알아보겠습니다. 클라이언트가 서버에 문자열을 전송하면, 서버는 해당 문자열을 반전(역순)시킨 후 다시 클라이언트에게 반환하는 구조입니다.
여기서는 소켓 프로그래밍(Socket Programming) 개념을 활용합니다. 클라이언트와 서버를 연결하려면 포트를 생성해야 하는데, 포트 번호는 소켓이 사용할 수 있는 임의의 숫자입니다. 단, 연결을 성립하려면 클라이언트와 서버가 동일한 포트 번호를 사용해야 합니다.
프로그램 실행 순서
프로그램을 시작할 때는 반드시 서버 프로그램을 먼저 실행해야 합니다.
gcc Server.c –o server
그다음 클라이언트 프로그램을 컴파일하고 실행합니다.
gcc Client.c –o client
서버가 실행되면 클라이언트로부터 문자열이 도착하기를 대기합니다. 클라이언트 쪽에서 문자열을 입력하면 해당 문자열이 서버로 전송됩니다. 서버는 받은 문자열을 출력하고, 반전된 문자열을 다시 클라이언트로 전송합니다. 마지막으로 클라이언트는 서버로부터 받은 반전된 문자열을 화면에 출력합니다.
클라이언트 측 코드
//Client Side Code
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 4000
main() {
struct sockaddr_in address;
int my_socket = 0, valread;
struct sockaddr_in server_address;
char str[100];
int l;
printf("Enter a String:");
fgets(str, 100, stdin); // 줄바꿈 문자가 입력될 때까지 문자열 읽기
char buffer[1024] = { 0 }; // 버퍼를 생성하고 0으로 초기화
// 소켓 파일 디스크립터 생성
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\nUnable to create Socket \n");
return -1;
}
memset(&server_address, '0', sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_port = htons(PORT);
if (inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr) <= 0) {
printf("\nThe address is not supported \n");
return -1;
}
// 서버에 소켓 연결
if (connect(my_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
printf("\nUnable to connect to the server \n");
return -1;
}
l = strlen(str);
send(my_socket, str, sizeof(str), 0); // 서버 측으로 메시지 전송
valread = read(my_socket, str, l); // 서버가 보낸 메시지 읽기
printf("%s\n", str);
}서버 측 코드
//Server Side Code
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 4000
char *strrev(char *str){
char *p1, *p2;
if (! str || ! *str)
return str;
// 두 포인터를 활용해 XOR 스왑 방식으로 문자열 반전
for (p1 = str, p2 = str + strlen(str) - 1; p2 > p1; ++p1, --p2){
*p1 ^= *p2;
*p2 ^= *p1;
*p1 ^= *p2;
}
return str;
}
main() {
int server_fd, new_socket, valread;
struct sockaddr_in address;
char str[100];
int addrlen = sizeof(address);
char buffer[1024] = { 0 };
int i, j, temp;
int l;
char* message = "Welcome to the server"; // 초기 메시지
// 소켓 파일 디스크립터 생성
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket connection failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 소켓을 4000번 포트에 바인딩
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("Unable to bind with the socket 4000");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("Listning.....");
exit(EXIT_FAILURE);
}
// 클라이언트의 접속 수락
if ((new_socket = accept(server_fd, (struct sockaddr*)&address,(socklen_t*)&addrlen)) < 0) {
perror("Accept");
exit(EXIT_FAILURE);
}
// 클라이언트가 보낸 문자열 읽기
valread = read(new_socket, str, sizeof(str));
l = strlen(str);
printf("\nString sent by client:%s\n", str);
strrev(str); // 문자열 반전
send(new_socket, str, sizeof(str), 0); // 반전된 문자열 전송
printf("\nReversed Strng is sent\n");
}핵심 동작 원리 정리
이 예제의 흐름을 요약하면 다음과 같습니다.
1. 서버: socket()으로 소켓을 생성하고, bind()를 통해 4000번 포트에 바인딩한 뒤 listen()으로 접속을 대기하고, accept()로 클라이언트 연결을 수락합니다.
2. 클라이언트: 사용자로부터 fgets()로 문자열을 입력받아 connect()로 서버에 접속한 후 send()로 문자열을 전송합니다.
3. 서버: read()로 문자열을 수신하고, strrev() 함수(XOR 스왑 기법)로 문자열을 반전한 뒤 send()로 결과를 되돌려 보냅니다.
4. 클라이언트: read()로 반전된 문자열을 받아 화면에 출력합니다.
참고로 클라이언트 코드에서는 주소로 127.0.0.1(루프백 주소)을 사용했기 때문에, 같은 머신 안에서 서버와 클라이언트를 모두 테스트할 수 있습니다. 실제 네트워크 환경에서 테스트하려면 서버의 실제 IP 주소로 변경하면 됩니다.
실행 결과
