Note-Taking / / 2024. 9. 27. 09:41

C언어, Java에서의 Server, Client 프로그래밍 정리

TCP Server Socket Programming

 

C 언어로 작성된 TCP 서버 소켓 프로그래밍 예시입니다. 서버는 클라이언트 연결을 기다리고, 연결되면 데이터를 주고받습니다.

 

1. TCP 서버 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")  // Winsock 라이브러리를 연결하기 위한 pragma 지시문

#define MAX_BUFFER_SIZE 1024  // 데이터를 송수신할 때 사용할 버퍼의 최대 크기를 1024 바이트로 설정

int main() {    
    WSADATA wsaData;  // Winsock 초기화를 위한 구조체
    SOCKET serverSocket, clientSocket;  // 서버 소켓과 클라이언트 소켓 변수 선언
    struct sockaddr_in serverAddr, clientAddr;  // 서버와 클라이언트의 주소 정보를 저장할 구조체
    char buffer[MAX_BUFFER_SIZE];  // 클라이언트로부터 수신할 메시지를 저장할 버퍼
    int clientAddrSize;  // 클라이언트 주소 크기를 저장할 변수

    // Winsock 초기화
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("Failed to initialize winsock.\n");
        return -1;  // Winsock 초기화 실패 시 프로그램 종료
    }

    // 소켓 생성 (TCP 프로토콜 사용)
    if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Failed to create socket.\n");
        return -1;  // 소켓 생성 실패 시 프로그램 종료
    }

    // 서버 주소 및 포트 설정
    serverAddr.sin_family = AF_INET;  // 주소 체계는 IPv4 사용
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 모든 네트워크 인터페이스에서 연결을 허용 (INADDR_ANY)
    serverAddr.sin_port = htons(5001);  // 포트 번호를 5001로 설정 (호스트 바이트 순서를 네트워크 바이트 순서로 변경)

    // 소켓 바인딩 (서버 소켓에 IP 주소와 포트 번호를 할당)
    if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
        printf("Failed to bind socket.\n");
        return -1;  // 바인딩 실패 시 프로그램 종료
    }

    // 클라이언트 연결 요청 대기 (최대 1개의 연결 요청을 큐에 저장)
    if (listen(serverSocket, 1) == SOCKET_ERROR) {
        printf("Failed to listen on socket.\n");
        return -1;  // 클라이언트 연결 대기 실패 시 프로그램 종료
    }

    printf("서버가 실행 중입니다...\n");

    // 클라이언트 연결 수락
    clientAddrSize = sizeof(clientAddr);  // 클라이언트 주소 구조체의 크기를 설정
    if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize)) == INVALID_SOCKET) {
        printf("Failed to accept client connection.\n");
        return -1;  // 클라이언트 연결 수락 실패 시 프로그램 종료
    }

    printf("클라이언트가 연결되었습니다.\n");

    // 메시지 송수신 루프
    while (1) {
        // 클라이언트로부터 메시지 수신
        memset(buffer, 0, MAX_BUFFER_SIZE);  // 버퍼 초기화
        int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);  // 클라이언트로부터 메시지를 수신
        if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
            break;  // 수신 오류 또는 클라이언트가 연결을 끊은 경우 루프 종료
        }

        printf("수신한 메시지: %s\n", buffer);  // 수신한 메시지를 출력

        // 클라이언트에게 응답 메시지 전송
        char response[MAX_BUFFER_SIZE];  // 응답 메시지를 저장할 버퍼
        snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer);  // 응답 메시지 생성
        send(clientSocket, response, strlen(response), 0);  // 응답 메시지를 클라이언트에게 전송
    }

    // 연결 종료
    closesocket(clientSocket);  // 클라이언트 소켓 닫기
    closesocket(serverSocket);  // 서버 소켓 닫기
    WSACleanup();  // Winsock 리소스 해제

    return 0;
}

 

코드 설명:

 

1. Winsock 초기화:

WSAStartup(MAKEWORD(2, 2), &wsaData) 함수로 Winsock을 초기화. 이 단계가 성공해야 네트워크 작업이 가능해.

실패하면 -1을 반환하고 프로그램을 종료해.

2. 소켓 생성:

socket(AF_INET, SOCK_STREAM, 0) 함수로 TCP 소켓을 생성.

AF_INET: IPv4 주소 체계를 사용.

SOCK_STREAM: 스트림 기반, 즉 TCP 프로토콜을 의미.

3. 서버 주소 설정:

sin_family: 주소 체계를 AF_INET(IPv4)로 설정.

sin_addr.s_addr: htonl(INADDR_ANY)로 모든 인터페이스에서 접속을 허용.

sin_port: htons(5001)로 포트 번호를 설정. htons는 호스트 바이트 순서를 네트워크 바이트 순서로 변환해.

4. 바인딩:

bind() 함수로 소켓에 IP 주소와 포트 번호를 바인딩. 실패 시 오류 메시지를 출력하고 종료.

5. 클라이언트 연결 대기:

listen() 함수로 서버 소켓을 클라이언트 연결 대기 상태로 설정. 최대 1개의 클라이언트를 대기열에 넣음.

6. 클라이언트 연결 수락:

accept() 함수로 클라이언트 연결을 수락하고, 성공하면 클라이언트와의 통신에 사용할 소켓을 반환.

7. 메시지 송수신:

서버는 **recv()**로 클라이언트가 보낸 메시지를 수신하고, 수신된 메시지를 처리한 후 **send()**를 통해 응답 메시지를 전송.

8. 연결 종료:

closesocket() 함수를 사용하여 클라이언트와 서버 소켓을 닫고, WSACleanup()으로 Winsock 라이브러리 리소스를 해제.

 

TCP Client Socket Programming

 

C 언어로 작성된 TCP 클라이언트 소켓 프로그래밍 예시입니다. 클라이언트는 서버에 연결하여 데이터를 전송하고 응답을 받습니다.

 

2. TCP 클라이언트 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")  // Winsock 라이브러리를 연결하기 위한 pragma 지시문

#define MAX_BUFFER_SIZE 1024  // 버퍼의 최대 크기를 1024 바이트로 설정

int main() {
    WSADATA wsaData;  // Winsock 초기화를 위한 데이터 구조체
    SOCKET clientSocket;  // 클라이언트 소켓 변수
    struct sockaddr_in serverAddr;  // 서버 주소 정보를 저장할 구조체
    struct in_addr ipAddr;  // 서버 IP 주소를 저장할 구조체 (inet_pton 사용 시 필요)

    char buffer[MAX_BUFFER_SIZE];  // 서버로부터 수신할 메시지를 저장할 버퍼
    char message[MAX_BUFFER_SIZE];  // 서버로 보낼 메시지를 저장할 버퍼

    // Winsock 초기화 (WSAStartup 사용)
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("Failed to initialize winsock.\n");
        return -1;  // Winsock 초기화 실패 시 종료
    }

    // 소켓 생성 (TCP 소켓 생성)
    if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Failed to create socket.\n");
        return -1;  // 소켓 생성 실패 시 종료
    }

    // 서버 정보 설정 (IPv4, 포트 5001 사용)
    serverAddr.sin_family = AF_INET;  // 주소 체계를 IPv4로 설정
    serverAddr.sin_port = htons(5001);  // 포트 번호를 5001로 설정 (htons 사용)

    // 서버 IP 주소 설정 (로컬호스트: 127.0.0.1)
    // inet_pton 함수를 사용하여 IP 주소를 네트워크 바이트 순서로 변환
    if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
        printf("Invalid address/ Address not supported.\n");
        return -1;  // IP 주소 변환 실패 시 종료
    }

    // 서버에 연결 시도
    if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
        printf("Failed to connect to server.\n");
        return -1;  // 서버 연결 실패 시 종료
    }

    // 메시지 송수신 루프
    while (1) {
        // 사용자로부터 메시지 입력
        printf("메시지를 입력하세요 (종료하려면 q 또는 Q): ");
        fgets(message, MAX_BUFFER_SIZE, stdin);  // 표준 입력으로부터 메시지를 받음
        message[strcspn(message, "\n")] = '\0';  // 입력된 문자열에서 개행 문자 제거

        if (strcmp(message, "q") == 0 || strcmp(message, "Q") == 0) {
            break;  // 사용자가 'q' 또는 'Q'를 입력하면 루프 종료 (프로그램 종료)
        }

        // 서버로 메시지 전송
        send(clientSocket, message, strlen(message), 0);  // 입력된 메시지를 서버로 전송

        // 서버로부터 응답 수신
        memset(buffer, 0, MAX_BUFFER_SIZE);  // 수신할 버퍼를 초기화
        int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);  // 서버로부터 데이터 수신
        if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
            break;  // 수신 오류 또는 서버가 연결을 종료했을 때 루프 종료
        }

        printf("서버로부터 받은 응답: %s\n", buffer);  // 서버로부터 받은 메시지를 출력
    }

    // 연결 종료
    closesocket(clientSocket);  // 클라이언트 소켓 닫기
    WSACleanup();  // Winsock 리소스를 해제

    return 0;
}

 

코드 설명:

 

1. Winsock 초기화:

WSAStartup(MAKEWORD(2, 2), &wsaData)를 호출하여 Winsock을 초기화해. 이 단계가 성공해야만 네트워크 관련 작업을 할 수 있어.

초기화에 실패하면, 프로그램은 -1을 반환하고 종료돼.

2. TCP 소켓 생성:

socket(AF_INET, SOCK_STREAM, 0)를 호출하여 TCP 소켓을 생성해. TCP는 연결 지향형 프로토콜이므로 데이터를 안전하게 전송할 수 있어.

소켓 생성에 실패하면 오류 메시지를 출력하고 종료돼.

3. 서버 주소 및 포트 설정:

서버는 IPv4(AF_INET)를 사용하고, 5001 포트로 통신을 하도록 설정돼.

IP 주소는 127.0.0.1(로컬호스트)로 설정돼 있어서 동일한 컴퓨터 내에서 서버에 연결할 수 있어.

4. 서버에 연결:

connect()를 호출하여 서버에 연결을 시도해. 서버가 수신 대기 중인 상태여야만 연결이 성립돼.

서버와의 연결이 실패하면 프로그램이 종료돼.

5. 사용자 입력 및 서버 통신:

루프를 통해 사용자는 서버로 보낼 메시지를 입력할 수 있고, send() 함수를 사용해 메시지를 서버로 전송해.

서버는 클라이언트의 메시지를 받고, 그에 대한 응답을 클라이언트로 다시 보내.

클라이언트는 recv() 함수로 서버의 응답을 수신한 후 이를 출력해.

6. 프로그램 종료 조건:

사용자가 ‘q’ 또는 **‘Q’**를 입력하면 프로그램이 종료되도록 구현돼.

7. 연결 종료:

통신이 완료되면 **closesocket()**을 호출해 클라이언트 소켓을 닫고, WSACleanup()을 호출해 Winsock 라이브러리에서 사용한 리소스를 해제해.


Java TCP Client-Server Socket Programming

 

1. TCP 서버 코드

 

TCPServer.java

package com.jinsu.server;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {

    private static final int PORT = 5001;  // 서버가 실행될 포트 번호
    private static final int MAX_BUFFER_SIZE = 1024;  // 버퍼의 최대 크기 (참고로 사용됨, 실제 코드에선 사용되지 않음)

    public static void main(String[] args) {
        ServerSocket serverSocket = null;  // 클라이언트 연결을 수락하기 위한 서버 소켓
        Socket clientSocket = null;  // 클라이언트와 통신하기 위한 소켓

        try {
            // 서버 소켓 생성 (지정된 포트에서 클라이언트 연결을 대기)
            serverSocket = new ServerSocket(PORT);
            System.out.println("서버가 실행 중입니다...");

            // 클라이언트 연결 수락 (클라이언트가 연결할 때까지 대기)
            clientSocket = serverSocket.accept();
            System.out.println("클라이언트가 연결되었습니다.");  // 클라이언트 연결 성공 시 메시지 출력

            // 클라이언트로부터 메시지를 수신하기 위한 입력 스트림 생성
            InputStream input = clientSocket.getInputStream();  // 클라이언트에서 보낸 데이터를 읽기 위한 스트림
            BufferedReader reader = new BufferedReader(new InputStreamReader(input));  // 입력 스트림을 버퍼로 감싸서 읽기

            // 클라이언트에게 메시지를 전송하기 위한 출력 스트림 생성
            OutputStream output = clientSocket.getOutputStream();  // 클라이언트로 데이터를 보내기 위한 스트림
            PrintWriter writer = new PrintWriter(output, true);  // 출력 스트림을 PrintWriter로 감싸고, 자동으로 flush 설정

            String clientMessage;  // 클라이언트가 보낸 메시지를 저장할 변수

            // 클라이언트로부터 메시지를 수신하고 응답을 전송하는 루프
            while ((clientMessage = reader.readLine()) != null) {  // 클라이언트가 메시지를 보내면 읽어들임
                System.out.println("수신한 메시지: " + clientMessage);  // 수신한 메시지를 서버에 출력

                // 클라이언트에게 응답 전송
                writer.println("서버가 메시지를 수신했습니다: " + clientMessage);  // 클라이언트에게 응답 메시지 전송
            }

        } catch (IOException e) {
            // 예외 처리 (입출력 오류 발생 시)
            System.out.println("서버 오류: " + e.getMessage());
            e.printStackTrace();  // 오류 스택 추적 출력
        } finally {
            // 리소스 해제 (클라이언트 소켓과 서버 소켓을 닫음)
            try {
                if (clientSocket != null) {  // 클라이언트 소켓이 null이 아니면 닫음
                    clientSocket.close();
                }
                if (serverSocket != null) {  // 서버 소켓이 null이 아니면 닫음
                    serverSocket.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();  // 리소스 해제 중 발생한 예외 처리
            }
        }
    }
}

 

코드 설명:

 

1. 상수 선언

 PORT: 서버가 대기할 포트 번호로, 여기서는 5001번을 사용. 클라이언트는 이 포트로 서버에 연결해.

 MAX_BUFFER_SIZE: 이 변수는 참고용으로 설정되어 있지만 실제 코드에서는 사용되지 않아. 버퍼 크기를 제한하거나 관리할 때 사용될 수 있어.

2. 서버 소켓 및 클라이언트 소켓 생성

 serverSocket: 클라이언트의 연결 요청을 수락하는 역할을 하는 소켓.

 clientSocket: 서버와 연결된 클라이언트와의 통신을 담당하는 소켓.

3. 서버 소켓 생성 및 실행

 serverSocket = new ServerSocket(PORT): 지정된 포트에서 서버 소켓을 생성하고 클라이언트 연결을 대기.

 서버가 실행되면 "서버가 실행 중입니다..." 메시지를 출력.

4. 클라이언트 연결 수락

 clientSocket = serverSocket.accept(): 서버는 클라이언트의 연결 요청을 수락하고, 연결된 클라이언트와 통신할 수 있도록 clientSocket을 생성.

 클라이언트가 연결되면 "클라이언트가 연결되었습니다." 메시지를 출력.

5. 클라이언트와의 데이터 송수신 설정

 입력 스트림(InputStream)과 BufferedReader를 사용해 클라이언트가 보낸 데이터를 읽어들임.

 출력 스트림(OutputStream)과 PrintWriter를 사용해 클라이언트에게 데이터를 전송함. true 옵션으로 자동 flush를 설정해, 메시지가 즉시 전송됨.

6. 메시지 송수신 루프

 reader.readLine()으로 클라이언트로부터 메시지를 읽어들이고, 서버 콘솔에 출력.

 클라이언트에게 응답 메시지를 전송. 응답 내용은 "서버가 메시지를 수신했습니다: [클라이언트 메시지]".

7. 예외 처리 및 리소스 해제

 IOException 예외가 발생할 경우, 오류 메시지를 출력하고 예외 스택 추적을 출력함.

 finally 블록에서 클라이언트 소켓과 서버 소켓을 안전하게 닫음. 연결이 종료되면 소켓을 닫아 네트워크 리소스를 해제함.

 

2. TCP 클라이언트 코드

 

TCPClient.java

 

package com.jinsu.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TCPClient {

    // 서버의 주소 및 포트 설정
    private static final String SERVER_ADDRESS = "localhost";  // 서버 주소 (로컬호스트)
    private static final int SERVER_PORT = 5001;  // 서버 포트 번호
    private static final int MAX_BUFFER_SIZE = 1024;  // 버퍼의 최대 크기 설정

    public static void main(String[] args) {
        Socket socket = null;  // 서버와 통신하기 위한 소켓 객체
        BufferedReader reader = null;  // 서버로부터 데이터를 읽어오기 위한 BufferedReader
        PrintWriter writer = null;  // 서버로 데이터를 전송하기 위한 PrintWriter

        try {
            // 서버에 연결 (소켓 생성 및 연결 시도)
            socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            System.out.println("서버에 연결되었습니다.");  // 연결 성공 메시지 출력

            // 서버로 데이터를 보내기 위한 PrintWriter (자동으로 flush 설정: true)
            writer = new PrintWriter(socket.getOutputStream(), true);

            // 서버로부터 데이터를 받기 위한 BufferedReader
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 사용자 입력을 받기 위한 BufferedReader
            BufferedReader userInputReader = new BufferedReader(new InputStreamReader(System.in));
            String userInput;  // 사용자의 입력을 저장할 변수

            // 메시지 송수신을 위한 루프
            while (true) {
                System.out.print("메시지를 입력하세요 (종료하려면 q 또는 Q): ");
                userInput = userInputReader.readLine();  // 사용자로부터 입력을 받음

                // 'q' 또는 'Q'를 입력하면 프로그램 종료
                if ("q".equalsIgnoreCase(userInput)) {
                    break;
                }

                // 서버로 메시지 전송
                writer.println(userInput);  // 사용자 입력을 서버로 전송

                // 서버로부터 응답 수신
                String response = reader.readLine();  // 서버의 응답을 수신
                System.out.println("서버로부터 받은 응답: " + response);  // 서버의 응답 출력
            }

        } catch (IOException e) {
            // 예외 처리 (입출력 오류 발생 시)
            System.out.println("클라이언트 오류: " + e.getMessage());
            e.printStackTrace();  // 오류 스택 추적 출력
        } finally {
            // 리소스 해제 (클라이언트 소켓, 입출력 스트림을 닫음)
            try {
                if (reader != null) reader.close();  // BufferedReader 닫기
                if (writer != null) writer.close();  // PrintWriter 닫기
                if (socket != null) socket.close();  // 소켓 닫기
            } catch (IOException ex) {
                ex.printStackTrace();  // 리소스 해제 중 발생한 예외 처리
            }
        }
    }
}

 

코드 설명:

 

1. 상수 선언

SERVER_ADDRESS: 서버의 IP 주소 또는 도메인 이름 (이 코드에서는 로컬호스트인 "localhost"로 설정).

SERVER_PORT: 서버의 포트 번호 (5001로 설정). 서버가 이 포트에서 클라이언트의 연결을 대기함.

MAX_BUFFER_SIZE: 메시지의 최대 크기를 1024 바이트로 설정했으나, 실제로 이 변수는 사용되지 않음 (참고로 사용됨).

2. 소켓 연결 및 입출력 스트림 설정

socket: 서버와의 연결을 관리하는 TCP 소켓.

reader: 서버로부터 데이터를 수신하기 위한 BufferedReader.

writer: 서버로 데이터를 전송하기 위한 PrintWriter. true 인자로 자동 flush가 활성화되어, println() 호출 시 즉시 데이터가 전송됨.

3. 서버와의 연결

socket = new Socket(SERVER_ADDRESS, SERVER_PORT): 서버에 연결을 시도하고, 성공 시 소켓을 생성.

서버에 성공적으로 연결되면 "서버에 연결되었습니다." 메시지를 출력함.

4. 사용자 입력 및 메시지 송수신 루프

사용자가 입력한 메시지를 userInput에 저장하고, 이를 서버로 전송.

서버로부터 수신된 응답을 **readLine()**으로 읽어와 출력.

‘q’ 또는 **‘Q’**를 입력하면 while 루프가 종료되고 프로그램이 종료됨.

5. 리소스 해제

프로그램이 종료될 때(예외 발생 시 포함), 사용한 리소스(소켓, 스트림)를 안전하게 닫음.

 

주의 사항:

 

서버 연결 실패 시: 서버가 해당 포트에서 실행 중이지 않거나, 방화벽 또는 네트워크 문제가 있을 경우 연결에 실패할 수 있어.

예외 처리: 네트워크 통신 중 발생할 수 있는 다양한 입출력 예외를 처리하기 위해 try-catch-finally 블록을 사용했어.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유