Programming/Java

[JAVA] TCP 통신 소켓 프로그래밍(단방향 통신)

cbw1030 2019. 8. 2. 11:53
반응형

소켓 프로그래밍에 대해 포스팅하기에 앞서 기본적인 용어를 정리해보자.

 

[ 네트워크(Network) ]

  • 컴퓨터들이 통신 기술을 이용하여 그물망처럼 연결된 통신 이용 형태를 의미
    • 쉽게 말하면, 두 대 이상의 컴퓨터들을 연결하고 서로 통신할 수 있는 것

 

[ 인터넷(Internet) ]

  • 네트워크를 컴퓨터와 컴퓨터로 구성한 것을 인터넷이라고 한다.

 

[ IP(Internet Protocol) ]

  • 전 세계 수억대의 컴퓨터가 인터넷을 하기 위해서는 서로의 정체를 알 수 있도록 특별한 주소를 부여했는데 이 주소를 IP 주소라고 한다.
  • Internet Protocol의 줄임말로, 인터넷에서 컴퓨터의 위치를 찾아서 데이터를 전송하기 위해 지켜야 할 규약이다.
  • IP는 4개의 숫자로 구성되며 숫자의 크기에 따라 IPv4, IPv6로 나뉜다.
    • 일반적으로 IPv4는 10진수로 표현하며 각 자리는 .으로 구분하고 IPv6는 16진수로 표현하며 각 자리를 :으로 구분한다.
IPv4 192.166.1.2 32비트, 각 숫자는 1바이트
IPv6 2001:0000:0000:3f5c 128비트, 각 숫자는 4바이트

 

 

[ TCP(Transmission Control Protocol) ]

  • IP로 컴퓨터의 위치를 찾았다면 원래의 목적인 정보를 전달해야한다. 인터넷은 패킷을 이용해서 데이터를 전달하므로 이 패킷을 전달하는 규약 또한 필요하다.
  • TCP란 소켓 프로그래밍 중의 하나로 스트림 통신 프로토콜이라고 부르며, 양쪽의 소켓이 연결된 상태여야만 가능하기 때문에 연결지향 프로토콜이라고도 한다.
    • 연결지향 방식은 한 번 연결되면 연결이 끊어질때까지는 송신한 데이터가 차례대로 목적지의 소켓에 전달되는 신뢰성 있는 통신이 가능하다.
  • TCP는 신뢰성이 있는 프로토콜이기 때문에 송신한 쪽의 데이터가 수신 측에 차례대로, 중간에 유실되는 일 없이 도착하는 것을 의미한다. 이러기 위해서는 수신 측과 송신 측이 미리 연결을 맺고 연결된 순서대로 데이터를 교환해야 한다.
  • TCP는 데이터의 흐름을 제어할 수는 있지만, 데이터가 전송되어야 할 경로에 대한 규약은 아니기 때문에 경로에 대한 규약인 IP를 함께 사용한다. 이 둘은 반드시 붙어다녀야 하는 존재이므로 TCP/IP라고 표기한다.
패킷이란?
- 데이터를 일정한 크기로 자른 단위이자 인터넷에서 정보를 전달하는 단위이다.
- 나누어진 패킷이 순서대로 도착한다는 보장은 없어서 규칙이 필요하다

위의 용어를 정리했으니 소켓 프로그래밍에 대해 알아보도록 하겠습니다.

 

[ 소켓이란? ]

  • 자바 프로그램은 소켓(Socket)이라는 개념을 통해서 네트워크 통신을 한다.
  • 소켓은 네트워크 부분의 끝 부분을 나타내며, 실제 데이터가 어떻게 전송되는지 상관하지 않고 읽기/쓰기 인터페이스를 제공한다.
    • 쉽게 예를들면, 내가 부산에 거주하는 친구와 대화를 하고 싶으면 스마트폰을 이용해 전화통화를 한다. 이러한 경우, '스마트폰'은 '소켓'이라고 할 수 있다.
  • 클라이언트는 Socket 객체를 생성하여 TCP 서버와 연결을 시도한다.
  • 서버는 SocketServer 객체를 생성하여 TCP 연결을 받아 클라이언트와 서버가 연결된다.
  • InputStream, OutputStream 객체를 통해 양방향 통신이 가능하다. 

서버 측부터 소스코드를 작성해보자.

[ 서버 측 소스코드 ]

package testPjt;

import java.net.ServerSocket;
import java.net.Socket;

public class MainClass {

    public static void main(String[] args) {
		
        // 네트워크 관련된 것은 예외처리를 모두 해줘야하므로 일단은 null 처리를 한다.
        ServerSocket serverSocket = null; 
        Socket socket = null;
	
        try {
            serverSocket = new ServerSocket(9000);
            System.out.println("클라이언트 맞을 준비 완료!"); 
			
            socket = serverSocket.accept();
            System.out.println("클라이언트 연결!");
            System.out.println("socket : " + socket);
			
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
			
            try {
				
                if (socket != null) {
                    socket.close();
                }
				
                if (serverSocket != null) {
                    serverSocket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

ServerSocket 클래스와 Socket 클래스로 일단은 인스턴스 변수들(serverSocket, socket)만 선언했다.

 

네트워크와 관련된 부분은 모두 예외처리를 하지 않으면 에러가 나기 때문에 null 처리를 한 후, try 구문 내부에 객체를 생성한다. 

 

인스턴스 변수(serverSocket)에 객체를 만들 때는 이 프로그램의 포트번호를 할당해야 한다.

 

아무거나 넣어도 상관이 없기에 임의로 9000번을 할당했다.

 

두 줄 밑으로 내려가면 ServerSocket 클래스의 객체인 serverSocket으로 accept() 메서드를 호출하고 있다.

 

이 accept() 메서드가 하는 역할은 클라이언트가 들어오는 것을 대기하는 역할을 한다.

 

즉, 클라이언트가 해당 9000번 포트로 연결을 시도한다면 accept 메서드는 대기를 풀고, 클라이언트와 연결시키는 Socket 클래스의 객체인 socket에 반환하게 된다.

 

그래서 accpet 메서드로부터 받은 socket이 바로 클라이언트랑 1:1로 연결된 소켓이다.

 

이제 socket으로 클라이언트랑 통신을 가능하게 한다.

 

 

 

그렇다면 이제 클라이언트 측 소스코드를 작성해보자.

[ 클라이언트 측 소스코드 ]

package testPjt;

import java.net.Socket;

public class Client {

    public static void main(String[] args) {
		
        Socket socket = null;
		
        try {
            socket = new Socket("localhost", 9000);
			
            System.out.println("서버연결!");
            System.out.println("socket : " + socket);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

클라이언트 측 소스코드도 마찬가지로 예외처리 구문 밖에다가 인스턴스 변수인 socket만 선언해주고 null을 할당해주자.

 

try 구문 내부에 있는 socket = new Socket("localhost", 9000) 부분이 연결을 시도하는 부분이다.

 

첫 번째 파라미터에는 연결할 IP 주소를, 두 번째 파라미터에는 포트 번호를 넘겨주어야 해당 주소로 연결을 시도한다.

 

localhost 대신에 127.0.0.1을 적어도 된다. 둘 다 동일하기 때문이다.

 

이제 localhost 주소로 아까 만든 서버에 연결을 요청을 해보자.

 

좌측이 클라이언트 측 코드 / 우측이 서버 측 코드

 

서버 측에서 실행을 하면 '클라이언트 맞을 준비 완료!'가 콘솔에 찍힌다.

 

 

 

클라이언트 측에서 실행을 하면 '서버 연결!'이 콘솔에 찍힌다. 

 

 

 

클라이언트 측 콘솔에 '서버연결!'이 찍히는 동시에 서버 측 콘솔에 '클라이언트 연결!'이 찍히면서 통신이 끝나게 된다.

 

 

 

finally 구문으로 close를 꼭 해줘야한다.

 짚고 넘어가야할 중요한 부분이 있는데 ServerSocket와 Socket을 닫아줘야한다.

 

ServerSocket을 닫는 것과 Socket을 닫는 것을 혼동해서는 안된다.  ServerSocket을 닫으면 사용중인 localhost의 포트가 해제되며 다른 서버가 해당포트를 바인드할 수 있게 된다. 또한 해당 ServerSocket을 통해 수용된 모든 소켓의 연결이 끊어진다.

 

서버 소켓은 프로그램이 종료될 때 자동으로 닫힌다. 그래서 ServerSocket의 사용이 끝나고 곧바로 프로그램을 종료할 예정이라면 꼭 닫아야 할 필요는 없다. 하지만 종료한다고 문제가 되진 않는다.

 

클라이언트와 소켓 통신이 진행중인 상황에서 ServerSocket이 강제로 종료되지 않도록 하기 위해서는 synchronized를 사용한다고도 하는데 이 부분은 나중에 포스팅으로 다루도록 하겠습니다.


 

블로그 참고 

https://coding-factory.tistory.com/270

https://m.blog.naver.com/highkrs/220840680504

 

두 블로그에 감사합니다.

반응형