Java Category/Java

[Java] TCP/IP 네트워킹

ReBugs 2023. 8. 12.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


IP 주소로 프로그램들이 통신할 때는 약속된 데이터 전송 규약이 있다.

이것을 전송용 프로토콜이라고 부른다.

인터넷에서 전송용 프로토콜은 아래의 두 가지로 나뉜다.

  • TCP(Transmission Control Protocol) : 우선 연결 후 데이터 전송
  • UDP(User Datagram Protocol) : 우선 데이터 전송 후 연결

 

TCP 네트워킹

 TCP는 연결형 프로토콜로 상대방이 연결된 상태에서 데이터를 주고받는다.

클라이언트가 연결 요청을 하고 서버가 연결을 수락하면 통신 회선이 고정되고, 데이터는 회선을 통해 전달된다.

회선 고정
TCP/IP 통신에서는 클라이언트와 서버 간의 회선은 여러 가지가 있는데, 그중에서 하나의 회선을 고정적으로 사용한다.

그렇기 때문에 TCP는 보낸 데이터가 순서대로 전달되며 손실이 발생하지 않는다.

"순서대로 전달되며 손실이 발생하지 않는다."
예를 들어, 배열에 {1, 2, 3}이 있으면 순서대로 1, 2, 3을 출력하는 것처럼 데이터의 흐름을 순서대로 보내고, 전달받으며 연결이 정상적으로 되어야 데이터 송수신이 이뤄지기 때문에 손실이 발생하지 않는다.

 

TCP는 IP와 함께 사용하기 때문에 TCP/IP라고도 한다.

TCP는 웹 브라우저가 웹 서버에 연결할 때 사용되며 이메일 전송, 파일 전송, DB연동에도 사용된다.

 

자바는 TCP 네트워킹을 위해 java.net 패키지에서 ServerSocket과 Socket 클래스를 제공하고 있다.

  • ServerSocket :  클라이언트의 연결을 수락하는 서버 쪽 클래스
  • Socket : 클라이언트에서 연결 요청할 때와 클라이언트와 서버 양쪽에서 데이터를 주고받을 때 사용되는 클래스

출처 : 이것이 자바다 유튜브 동영상 강의

ServerSocket을 생성할 때는 바인딩할 Port 번호를 지정해야 한다.

위 그림에서는 50001번이 Port이다.

서버가 실행되면 클라이언트는 Socket을 이용해서 서버의 IP 주소와 Port 번호로 연결 요청을 할 수 있다.

ServerSocket은 accept() 메소드로 연결 수락을 하고 통신용 Socket을 생성한다.

그러고 나서 클라이언트와 서버는 양쪽의 Socket을 이용해서 데이터를 주고받게 된다.

 


 

TCP 서버

ServerSocket 객체 생성

TCP 서버 프로그램을 개발하려면 우선 ServerSocket 객체를 생성해야 한다.

아래는 50001번 포트에 바인딩하는 ServerSocket를 생성하는 코드이다.

ServerSocket serverSocket = new ServerSocket(50001);

 

ServerSocket을 생성하는 또 다른 방법은 기본 생성자로 객체를 생성하고 Port 바인딩을 위해 bind() 메소드를 호출하는 것이다.

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(50001));

 

네트워크 어댑터가 여러 개라면 IP가 여러 개 일 수 있다.

따라서 서버 컴퓨터에 여러 개의 IP가 할당되어 있을 경우, 특정 IP에서만 서비스를 하고 싶다면

InetSocketAddress의 첫 번째 매개값으로 해당 IP를 주면 된다.

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("아이피 주소",50001));
만약 포트가 이미 다른 프로그램에서 사용 중이라면 BindException이 발생한다.
이 경우에는 다른 포트로 바인딩하거나 해당 포트를 사용 중인 프로그램을 종료하고 다시 실행하면 된다.

클라이언트와 연결

ServerSocket이 생성되었다면 연결 요청을 수락을 위해 accept() 메소드를 실행해야 한다.

accept()는 클라이언트가 연결 요청하기 전까지 블로킹된다. 블로킹이란 실행을 멈춘 상태라는 뜻이다.

클라이언트의 연결 요청이 들어오면 블로킹이 해제되고 통신용 Socket을 리턴한다.

ServerSocket과 Socket
서버에서 ServerSocket 객체를 생성하고 그 객체에서 accept()를 호출하면 입출력 장치의 대기 상태와 마찬가지로 대기 상태가 된다.
이 대기 상태가 해제되려면 클라이언트가 연결 요청을 해야 한다.
클라이언트가 연결 요청을 하여 연결이 수락되면 서버와 클라이언트 모두 Socket 객체가 생성되고, 이 Socket 객체로 데이터 송수신이 이뤄진다.
Socket socket = serverSocket.accept();

IP 주소와 Port 번호를 얻기

리턴된 Socket을 통해 연결된 클라이언트의 IP 주소와 Port 번호를 얻고 싶다면 아래처럼 하면 된다.

InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
String clientIp = isa.getHostName; //클라이언트 IP 얻기
//또는 String clientIp = isa.getHostString;
String portNo = isa.getPort(); //클라이언트 Port 얻기

서버 종료하기

서버를 종료하려면 두 가지 방법이 있다.

Socket을 닫거나 ServerSocket을 닫거나

socekt.close; //Socket을 닫음
serverSocket.close // ServerSocket을 닫음

사용 예제

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	
	public static void main(String[] args) {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");
		
		//TCP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();
	}	

	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);	
					System.out.println( "[서버] 시작됨");
					
					//연결 수락 및 데이터 통신
					while(true) {
						System.out.println( "\n[서버] 연결 요청을 기다림\n");
						//연결 수락
						Socket socket = serverSocket.accept();
						
						//연결된 클라이언트 정보 얻기
						InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
						System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
						
						//연결 끊기
						socket.close();
						System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음");
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		try {
			//ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			System.out.println( "[서버] 종료됨 ");
		} catch (IOException e1) {}
	}
}

 

위와 같이 클라이언트 연결 대기 상태이다가 아래처럼 웹 브라우저에 "서버 IP:포트번호"를 입력하면

 

아래와 같이 서버에서 클라이언트와 연결이 되었다고 출력이 된다.

 


 

TCP 클라이언트

서버에 연결하기

클라이언트가 서버에 연결 요청을 하려면 Socket 객체를 생성할 때 생성자 매개값으로 서버 IP 주소와 Port 번호를 제공하면 된다.

로컬 컴퓨터에서 실행하는 서버로 연결 요청을 할 경우에는 IP 주소대신 localhost를 사용할 수 있다.

Socket socket = new Socket("IP", 50001);

위와 같이 Socket 객체를 만든 순간 서버에 연결 요청을 하게 된다.

 

IP 주소대신 도메인 이름을 사용하고 싶다면, DNS에서 IP 주소를 검색할 수 있도록 생성자 매개값으로 InetAddress를 제공해야 한다.

Socket socket = new Socket(InetAddress.getByName("domainName"), 50001);

 

기본 생성자로 Socket을 생성한 후 connect() 메소드로 연결 요청을 할 수도 있다.

Socket socket = new Socket();
socket.connect(new InetSocketAddress("domainName", 50001));

 

연결 요청 시 두 가지 예외가 발생할 수 있다.

UnknownHostException은 IP 주소가 잘못 표기되었을 때 발생하고, IOException은 제공된 IP와 Port 번호로 연결할 수 없을 때 발생한다.

따라서 두 가지 예외를 모두 처리해야 한다.


서버 연결 끊기

서버와 연결된 후에 클라이언트에서 연결을 끊고 싶다면 Socket의 close() 메소드를 호출하면 된다.

socket.close();

사용 예제

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
	
public class ClientExample {
	public static void main(String[] args) {
		try {
			//Socket 생성과 동시에 localhost의 50001 Port로 연결 요청;
			Socket socket = new Socket("localhost", 50001);

			System.out.println( "[클라이언트] 연결 성공");

			//Socket 닫기
			socket.close();
			System.out.println("[클라이언트] 연결 끊음");
		} catch (UnknownHostException e) {
			//IP 표기 방법이 잘못되었을 경우
		} catch (IOException e) {
			//해당 포트의 서버에 연결할 수 없는 경우
		}
	}
}

<서버(클라이언트 연결 전)>

 

<클라이언트>

 

<서버(클라이언트 연결 후)>

 


 

입출력 스트림으로 데이터 주고받기

클라이언트가 연결 요청을 하고 서버가 연결 수락을 했다면, 아래의 그림과 같이 양쪽 Socket 객체로부터 각가 입력 스트림과 출력 스트림을 얻을 수 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

아래는 Socket으로부터 InputStream과 OutputStream을 얻는 코드이다.

InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

 

데이터 보내기

상대방에게 데이터를 보낼 데이터를 byte[] 배열로 생성하고, 이것을 매개값으로 해서 OutputStream의 write() 메소드를 호출하면 된다.

아래의 코드는 문자열로부터 UTF-8로 인코딩한 바이트 배열을 얻어내고, write() 메소드로 전송한다.

String data = "보낼 데이터";
byte[] bytes = data.getBytes("UTF-8");
OutputStream os = socket.getOutputStream();
os.write(bytes);
os.flush();

 

문자열을 좀 더 간편하게 보내고 싶다면 보조 스트림인 DataOutputStream을 연결해서 사용하면 된다.

String data = "보낼 데이터";
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(data); //UTF-8로 씀
dos.flush();

데이터 받기

데이터를 받기 위해서는 받은 데이터를 저장할 byte[] 배열을 하나 생성하고, 이것을 매개값으로 해서 InputStream의 read() 메소드를 호출하면 된다.

read() 메소드는 읽은 데이터를 byte[] 배열에 저장하고 읽은 바이트 수를 리턴한다.

받는 데이터가 문자열이라면 다음과 같이 byte[] 배열을 UTF-8로 디코딩해서 문자열로 얻을 수 있다.

byte[] bytes = new byte[1024];
InputStream is = socket.getInputStream();
int num = is.read(bytes);
String data = new String(bytes, 0, num, "UTF-8");

 

문자열을 좀 더 간편하게 받고 싶다면 보조 스트림인 DataInputStream을 연결해서 사용하면 된다.

단, 이 방법은 상대방이 DataOutputStream으로 문자열을 보낼 때만 가능하다.

DataInputStream dis = new DataInputStream(socket.getInputStream());
String data = dis.readUTF();

 


사용 예제

다음은 TCP 클라이언트가 보낸 메시지를 다시 돌려보내는 에코(메아리) TCP 서버를 구현한 예제이다.

출처 : 이것이 자바다 유튜브 동영상 강의

보조 스트림 X

EchoServer.java

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	
	public static void main(String[] args) {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");
		
		//TCP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();
	}	

	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);	
					System.out.println( "[서버] 시작됨");
					
					//연결 수락 및 데이터 통신
					while(true) {
						System.out.println( "\n[서버] 연결 요청을 기다림\n");
						//연결 수락
						Socket socket = serverSocket.accept();
						
						//연결된 클라이언트 정보 얻기
						InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
						System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
						
						
						//데이터 받기
						InputStream is = socket.getInputStream();
						byte[] bytes = new byte[1024];
						int readByteCount = is.read(bytes);
						String message = new String(bytes, 0, readByteCount, "UTF-8");
						
						//데이터 보내기
						OutputStream os = socket.getOutputStream();
						bytes = message.getBytes("UTF-8");
						os.write(bytes);
						os.flush();
						System.out.println("[서버] 받은 데이터를 다시 보냄 : " + message);
						
						
						//연결 끊기
						socket.close();
						System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음");
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		try {
			//ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			System.out.println( "[서버] 종료됨 ");
		} catch (IOException e1) {}
	}
}

 

ClientExample.java

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
	
public class ClientExample {
	public static void main(String[] args) {
		try {
			//Socket 생성과 동시에 localhost의 50001 Port로 연결 요청;
			Socket socket = new Socket("localhost", 50001);

			System.out.println( "[클라이언트] 연결 성공");
			
			
			//데이터 보내기
			String sendMessage = "나는 자바가 좋아~~";
			OutputStream os = socket.getOutputStream();
			byte[] bytes = sendMessage.getBytes("UTF-8");
			os.write(bytes);
			os.flush();
			System.out.println("[클라이언트] 데이터를 보냄 : " + sendMessage);

			//데이터 받기
			InputStream is = socket.getInputStream();
			bytes = new byte[1024];
			int readByteCount = is.read(bytes);
			String receiveMessage = new String(bytes, 0, readByteCount, "UTF-8");
			System.out.println("[클라이언트] 데이터 받음 : " + receiveMessage);
			
			
			//Socket 닫기
			socket.close();
			System.out.println("[클라이언트] 연결 끊음");
		} catch (UnknownHostException e) {
			//IP 표기 방법이 잘못되었을 경우
		} catch (IOException e) {
			//해당 포트의 서버에 연결할 수 없는 경우
		}
	}
}

<서버(클라이언트 연결 전)>

 

<클라이언트>

<서버(클라이언트 연결 후)>


보조 스트림 O

EchoServer.java

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	
	public static void main(String[] args) {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");
		
		//TCP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();
	}	

	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);	
					System.out.println( "[서버] 시작됨");
					
					//연결 수락 및 데이터 통신
					while(true) {
						System.out.println( "\n[서버] 연결 요청을 기다림\n");
						//연결 수락
						Socket socket = serverSocket.accept();
						
						//연결된 클라이언트 정보 얻기
						InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
						System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
						
						
						//데이터 받기
						DataInputStream dis = new DataInputStream(socket.getInputStream());
						String message = dis.readUTF();
						
						//데이터 보내기
						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeUTF(message);
						dos.flush();
						System.out.println("[서버] 받은 데이터를 다시 보냄 : " + message);
						
						
						//연결 끊기
						socket.close();
						System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음");
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		try {
			//ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			System.out.println( "[서버] 종료됨 ");
		} catch (IOException e1) {}
	}
}

 

ClientExample.java

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
	
public class ClientExample {
	public static void main(String[] args) {
		try {
			//Socket 생성과 동시에 localhost의 50001 Port로 연결 요청;
			Socket socket = new Socket("localhost", 50001);

			System.out.println( "[클라이언트] 연결 성공");
			
			
			//데이터 보내기
			String sendMessage = "나는 자바가 좋아~~";
			DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
			dos.writeUTF(sendMessage);
			dos.flush();
			System.out.println("[클라이언트] 데이터를 보냄 : " + sendMessage);

			//데이터 받기
			DataInputStream dis = new DataInputStream(socket.getInputStream());
			String receiveMessage = dis.readUTF();
			System.out.println("[클라이언트] 데이터 받음 : " + receiveMessage);
			
			
			//Socket 닫기
			socket.close();
			System.out.println("[클라이언트] 연결 끊음");
		} catch (UnknownHostException e) {
			//IP 표기 방법이 잘못되었을 경우
		} catch (IOException e) {
			//해당 포트의 서버에 연결할 수 없는 경우
		}
	}
}

 

<서버 (클라이언트 연결 전)>

 

<클라이언트>

 

<서버 (클라이언트 연결 후)>

댓글