프로그래밍 언어 활용/JAVA

네트워크 : TCP 통신 구현하기

프린이8549 2024. 2. 13. 17:49

0. 들어가기에 앞서

 본고에서는 자바만을 활용해 서버와 클라이언트 간 간단한 통신을 구현하는 과정을 기술하고자 한다.

 

 다만, 원활한 이해를 위해 본격적으로 구현하기에 앞서 본 과정에서 등장하는 용어들에 대해 잠깐이나마 알아보는 시간을 가지고자 한다.

 

네트워크

여러 대의 컴퓨터들이 연결되어 있는 통신망.

 

이런 통신망을 통해 데이터들을 교환하는 행위를 네트워킹이라고 한다.

 

서버와 클라이언트

서버

클라이언트에게 서비스를 제공해주는 프로그램 또는 컴퓨터를 의미한다(클라이언트의 요청에 응답하는 것).

클라이언트

서버에 요청하는 컴퓨터(서비스를 제공받는 고객).

 

// 서버와 클라이언트 클래스 생성(편의상 함께 기술함)
class TCPServer{
}
class TCPClient{
}

 

서버에 요청하기 위해서는 요청하고자 하는 서버의 IP 주소(또는 도메인), 포트 번호를 알아야 한다.

 

IP 주소(Internet Protocol Address)

네트워크상에서 컴퓨터들을 식별해줄 수 있는 번호(실제 주소와 동일한 역할)

 

InetAddress 클래스를 통해 IP 주소와 관련된 네트워크의 정보를 확인할 수 있다.

InetAddress localhost = InetAddress.getLocalHost(); // 내PC(지역 호스트)에 대한 정보 반환
System.out.println("내 PC 명 : " + localhost.getHostName() ); // 호스트의 이름을 반환
System.out.println("내 IP 주소 : " + localhost.getHostAddress() ); // IP 주소 반환

 

단, InetAddress 클래스 활용 시 UnknownHostException 발생 가능하기에 예외처리해주어야 한다.

도메인(domain)

사전적으로는 영토, 분야, 영역, 범위를 뜻하는 단어.

 

프로그래밍에서는 인터넷에 연결된 컴퓨터를 사람이 쉽게 기억하고 입력할 수 있도록 문자(영문, 한글 등)로 만든 인터넷주소를 의미한다. 예시) www.naver.com

 

도메인을 통해서 해당 호스트의 정보를 확인할 수 도 있다.

InetAddress googleHost = InetAddress.getByName("www.google.com"); // 도메인 통해 해당 호스트의 정보 반환
System.out.println("구글의 서버명 : " + googleHost.getHostName());
System.out.println("구글의 IP 주소 : " + googleHost.getHostAddress());

InetAddress[] googleAllHost = InetAddress.getAllByName("www.google.com");
System.out.println("구글 호스트 개수 : " + googleAllHost.length);

 

포트 번호(Port Number)

호스트 내에서 실행되고 있는 프로세스를 구분짓기 위한 16비트의 논리적 할당(0 ~ 65536개).

 

포트는 컴퓨터 안에서 프로그램을 찾을 때 사용. 일종의 아파트 건물(IP 주소) 내 호수(포트 번호).

 

하나의 PC 내에서 데이터를 받을 프로세스가 하나가 아닐 경우, 어떤 프로세스(ex. 웹 서버인지 메일 서버인지)인지 알아야 데이터가 제대로 전송될 수 있는데, 이때 사용하는 식별자가 바로 포트 번호이다.

 

 자바를 활용해 서버와 클라이언트 간 통신을 구현하고자 할 때 데이터를 입출력하고자 한다면, 서버와 클라이언트간 스트림이 필요하다. 이때 필요한 것이 소켓 프로그래밍이다.

 

1. 소켓 프로그래밍이란?

소켓(Socket)을 이용한 통신 프로그래밍을 의미한다

 

Server와 Client가 특정 Port를 통해 실시간으로 양방향 통신을 하는 방식이다.

 

참고)

 웹에서는 주로 Http 통신을 활용한다. Http 통신은 Client의 요청(Request)이 있을 때만 서버가 응답(Response)하여 해당 정보를 전송하고 곧바로 연결을 종료하는 방식이다. Client가 요청을 보내는 경우에만 Server가 응답하는 단방향 통신으로 반대로 Server가 Client에게 요청을 보낼 수는 없다.

1.1.Socket

프로세스 간 통신에 사용되는 양 끝단(endpoint)을 의미한다.

  • 소켓은 응용프로그램에서 TCP/IP를 이용하는 창구 역할을 하며, 두 프로그램이 네트워크를 통해 서로 통신을 수행할 수 있도록 양쪽에서 생성되는 링크의 단자이다.[참고 1]
  • 따라서 서버와 그 서버에 접속하고자 하는 클라이언트가 있다면 양자에게 소켓이 존재해야 함

InputStream 과 OutputStream 을 가지고 있으며, 이를 통해 프로세스 간 통신이 이루어진다.

  • getInputStream() : 소켓을 위한 InputStream 을 리턴
  • getOutputStream()  소켓을 위한 OutputStream 을 리턴
  • 이 값들을 InputStreamReader 로 읽고, OutputStreamWriter 를 통해 전송

 

따라서 문자 입출력을 위해서는 보조 스트림을 활용해야 한다.

1.2.ServerSocket

포트와 연결되어(Bind) 포트를 통해 클라이언트의 연결 요청을 기다리며 요청이 들어오면 수락한다.

  • 이때 수락이라함은 통신 가능한 socket 이 생성됐으며 클라이언트의 소켓과 통신할 수 있도록 연결함을 의미

하나의 포트에는 하나의 ServerSocket 만 연결 가능하다(포트 독점).

  • 단, 프로토콜 다를 경우 같은 포트 공유 가능

즉, ServerSocket 은 소켓 간 연결만 처리하며 실제 데이터 통신은 소켓들 간에 발생한다.

// (서버 클래스)
// ServerSocket 생성
ServerSocket serverSocket = new ServerSocket(포트 번호);
Socket socket = server.accept(); // 클라이언트로부터 연결 요청이 오면 연결, 이후 클라이언트와 소통할 소켓 생성해 리턴
// 단, 이때 Exception 오류 발생하기에 try catch 구문으로 예외처리해주는 과정 필수
// (클라이언트 클래스)
// 클라이언트 소켓 생성
Socket socket = new Socket(서버 IP 주소, 서버 포트 번호); // 서버 클래스가 존재하는 컴퓨터의 IP 주소, 서버소켓에서 등록한 포트 번호
// 클라이언트 소켓은 따로 기다릴 필요가 없기 때문에 바로 생성
// 서버 프로그램으로의 연결 요청 및 데이터 전송 역할 수행

1.3.소켓 프로그래밍의 방식

소켓 프로그래밍에는 TCP 방식과 UDP 방식이 있다.

 

2. TCP(Transmission Control Protocol)

2.1.TCP란?

 서버, 클라이언트간 1:1 소켓 통신.

 

 TCP는 서로 간에 연결을 설정한 후에 데이터를 주고 받는 방식이다.

 

  TCP는 전화와 비슷하다. 전화를 하기 위해 전화번호를 입력하고 전화 버튼을 누르고 상대방이 전화를 받을 때까지 기다린 뒤 상대가 전화를 받아야지만 통화가 가능하다. 통화가 끝나면 연결이 끊어진다.

2.2. TCP 통신 과정

  1.  서버 프로그램은 ServerSocket 을 사용해 서버 컴퓨터의 특정 포트에서 클라이언트의 연결 요청을 처리할 준비 실시
  2. 클라이언트 프로그램은 접속할 서버의 IP 주소와 포트 정보 토대로 Socket 생성해 서버에 연결 요청
    • 즉, 서버가 먼저 실행되어 클라이언트의 요청을 기다려야 함
  3. ServerSocket은 클라이언트의 연결 요청 접수 시 서버에 새로운 Socket 생성해 클라이언트의 Socket과 연결
  4. 클라이언트의 소켓과 새로 생성된 서버의 소켓이 일대일 통신 통해 데이터 교환
    • 이때 데이터 송수신을 위해서는 입출력스트림을 생성해서 값을 입출력해주는 기능을 반복해주어야 함
    • '클라이언트 -> 소켓 -> 서버 ->  소켓 -> 클라이언트' 의 과정

TCP 통신 과정[참고 2]
TCP 통신 과정 [참고3]

2.3. TCP 통신 특징

앞서 TCP를 전화에 비유한 것처럼,  중간에 데이터들이 잘 도착했는지 상대의 응답을 통해 확인하고 분실된 데이터가 있다면 다시 보내는 과정을 거친다.

 

또한 전화가 내가 말한 순서대로 상대방에게 들리는 것처럼 TCP 역시 데이터를 받는 순서가 데이터를 보내는 순서와 동일하게 관리한다.

 

 따라서 TCP의 장점은 신뢰성 있게 데이터를 보낼 수 있다는 것이다.

 

 그러나 연결 과정 및 연결 해제 과정에서 많은 시간이 소요되며, 짧은 데이터를 보내는 경우 이는 낭비가 될 수 있다.

 

 따라서 소량의 데이터보다 대량의 데이터에 적합하다.

2.4. TCP 통신 구현하기

1. 클라이언트 -> 소켓

PrintWriter 와 OutputStreamWriter 를 사용해 데이터를 읽고 서버로 전송해준다.

  • PrintWriter : 데이터 출력 시 print(), println() 메소드를 가지고 있는 보조스트림
// 클라이언트 클래스
System.out.print("서버에게 보낼 내용 : ")
String msg = sc.nextLine();
PrintWriter pw = new PrintWriter( new OutputStreamWriter( socket.getOutputStream() ) );
pw.println(msg); // 서버로 데이터 전송
pw.flush(); // 버퍼 내에 있는 값들을 전부 비워줌
2. 소켓 -> 서버

소켓에 전달된 msg 의 데이터 값을 받기 위해서 서버 클래스에서 BufferedReader 와 InputStreamReader 를 사용해 서버로 값을 받아온다.

// 서버 클래스
BufferedReader br =  new BufferedReader( new InputStreamReader( socket.getInputStream() ) ); // 소켓으로부터 데이터 읽어옴
String message = br.readLine(); // 서버 클래스 내의 message 변수 안에 클라이언트 클래스의 msg의 내용이 저장됨
System.out.println("클라이언트로부터 전달받은 메세지 : " + message);
3. 서버 -> 소켓

 이후 보낼 데이터를 소켓을 통해 클라이언트로 전달해준다.

// 서버 클래스
System.out.print("클라이언트에게 보낼 내용 : ");
String sendMessage = sc.nextLine();
PrintWriter pw = new PrintWriter(new OutputStreamWriter( socket.getOutputStream() ) ); // 소켓으로 데이터를 바깥으로 보냄
pw.println(sendMessage); // 클라이언트한테 출력
pw.flush();
4. 소켓 -> 클라이언트
// 클라이언트 클래스
BufferedReader br = new BufferedReader( new InputStreamReader( socket.getInputStream() ) ); // 소켓으로부터 데이터를 읽어옴
String message = br.readLine();
System.out.println("서버로부터 전달받은 메세지 : " + message);

 

소켓과 서버 소켓도 Scanner 클래스, Stream클래스들과 마찬가지로 사용 후에는 닫아줘야 한다.

5. 최종 구현
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class TCPClient {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		
		BufferedReader br = null;
		PrintWriter pw = null;
		Socket socket = null;
        
		int port = 3000;
		
		String serverIP = "192.168.30.12";
		try {
			socket = new Socket(serverIP, port);
			// 만약에 통신이 실패한다면 null 값이 socket 객체에 담긴다
			
			if (socket != null) { // 서버와 잘 연결된 경우
				System.out.println("서버와 연결 성공!!");
				// 입력용 스트림
				br = new BufferedReader ( new InputStreamReader( socket.getInputStream() ) );
				// 출력용 스트림
				pw = new PrintWriter(socket.getOutputStream());
				
				while(true) {
					System.out.print("서버에게 보낼 내용 : ");
					String sendMessage = sc.nextLine();
					pw.println(sendMessage);
					pw.flush();
					
					String message = br.readLine();
					System.out.println("서버로부터 전달받은 메세지 : " + message);
				}
			}
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			pw.close();
			try {
				br.close();
                socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		sc.close();
	}
}

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCPServer {

public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		
		BufferedReader br = null ;
		PrintWriter pw = null;
		
		int port = 3000;
		
		ServerSocket server = null;
		try {
			server = new ServerSocket(port); 
			
			System.out.println("클라이언트 요청을 기다리고 있습니다.");
			
			Socket socket = server.accept();
			System.out.println(socket.getInetAddress().getHostAddress() + "가 연결을 요청함..."); 
			
			// 입력용 스트림(클라이언트로부터 전달된 값을 한 줄 단위로 읽어들이기 위한 입력용 스트림)
			br = new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
			// 출력용 스트림(클라이언트에게 값을 한 줄 단위로 출력할 수 있는 출력용 스트림)
		    pw = new PrintWriter( socket.getOutputStream() );
		    
		    while(true) {
		    	String message = br.readLine();
		    	System.out.println("클라이언트로부터 전달받은 메세지 : " + message);
		    	
		    	System.out.print("클라이언트에게 보낼 내용 : ");
		    	String sendMessage = sc.nextLine();
		    	
		    	pw.println(sendMessage); // 클라이언트한테 출력
		    	pw.flush(); 
		    }
		    
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			pw.close();
			try {
				br.close();
                server.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		sc.close();
	}
}

 


[참고 1] 출처 : https://wildeveloperetrain.tistory.com/122 

[참고 2] 출처: https://lktprogrammer.tistory.com/62 ,"맛있는 프로그래머의 일상"

[참고 3] 출처: https://cafe.naver.com/eztcp/550