소켓? HTTP?

byeol·2023년 4월 7일
0

소켓 통신과 HTTP 통신을 비교해보려고 합니다.

프로젝트를 하며 챗팅 프로그램을 구현해야하는데
"소켓 통신"을 이용해야 한다는 말을 듣고 평상시에 명확하지 않았던 개념을 이번에 정리하겠습니다.


네트워크를 통해 서버로부터 데이터를 가져오기 위한 통신을 크게
"소켓" 통신과 "HTTP" 통신이 있습니다.
이 둘의 차이점과 각 특징을 정리하겠습니다.

HTTP(HyperText Transfer Protocol)

HTML 파일을 전송하는 프로토콜이라는 의미를 가지지만 현재는 JSON, Image 파일 등 또한 전송합니다.

HTTP 통신은 몇 가지 특징을 가지고 있습니다.

  • 클라이언트 : 서버 구조
    클라이언트의 요청이 있을 때마 서버는 요청에 따른 응답을 합니다.
    따라서 클라이언트의 요청이 있을 때마 응답하는 단방향 통신입니다.
    서버는 클라이언트에게 먼저 요청할 수 없습니다.
  • 무상태 프로토콜
    서버가 클라이언트의 연결 상태를 보존하지 않기 때문에
    클라이언트가 바로 이전에 한 요청과 똑같은 요청을 하더라고 다시 동일한 요청을 서버에 보내야 합니다. (하지만 로그인 상태 정보와 같이 유지가 필요한 것은 쿠키가 세션을 이용합니다.)
  • 비연결성
    클라이언트가 서버에 요청을 하고 응답을 받으면 바로 TCP/IP 연결을 끊어 연결을 유지하지 않습니다.
    한계 : 새로 연결될 때마다 TCP/IP 연결을 새로 맺어야 하므로 3-way handshake에 따른 시간이 추가됩니다.
    해결 : HTTP 지속 연결을 통해 이를 해결합니다. 처음 연결이 되고 나서, 여러 파일의 요청/응답이 다 끝난 뒤에 연결을 종료합니다.
  • HTTP 메세지를 통해 통신합니다.
    HTTP 메세지는 HTML, TEXT, 음성, 영상, 파일, JSON, XML 등을 전송할 수 있습니다.

HTTP 기본 구조

start-line
header
empty line
message body

✅ HTTP start-line

➡️ HTTP 요청 start-line

GET /search?q=hello&hl=ko HTTP/1.1

method 공백 request-targer 공백 HTTP-version

  • method : 서버가 수행해야할 동작
    • GET : 리소스 조회
    • POST : 요청 내역 처리
    • PUT, DELETE
  • request-target : 요청 대상, 절대경로 '/'로 시작하는 경로, /absolut-path[?query]
  • HTTP-version

⬅️ HTTP 응답 start-line

HTTP/1.1 200 OK

HTTP-version 공백 status-code 공백 response-phrase

  • HTTP-version
  • HTTP 상태코드
    • 200 : 성공
    • 400 : 클라이언트 오류
    • 500 : 서버 내부 오류
  • response-phrase : 사람이 이해할 수 있는 짧은 상태 코드 설명

✅ HTTP Header

위 그림과 같이 HTTP Header는 일반 헤더와 요청/응답 헤더 그리고 엔티티 헤더로 구성되어져 있습니다.

⏺️ 일반(General) 헤더
요청 및 응답 메세지 모두에서 공통적으로 사용 가능한 일반 목적의 헤더 항목입니다.

포함된 정보는 아래와 같습니다.

  • Date : 요청과 응답 시에 자동으로 현재 날짜와 시간이 만들어집니다.
  • Connection : 클라이어느와 서버의 연결 방식 설정
    • HTTP/1.0에서는 keep-alive가 디폴트 값이 아니고, 표준도 아님
    • HTTP/1.1에서는 keep-alive를 더 이상 사용하지 않고, 기본적으로 Persisten 연결을 지원합니다. 만약 응답 이후 TCP 연결을 끊어야 하는 경우에만 Connection:close로 헤더를 선언합니다.
  • Pragma : 캐시제어, HTTP/1.0에서 쓰던 것으로 HTTP/1.1에서는 Cache-Control이 쓰입니다.
  • Cache-Control: 캐시를 허용할지 말지를 제어하기 위해서 사용합니다.
    • no-store : 캐시를 저장하지 않음
    • no-cache : 모든 캐시를 쓰기 전에 서버에 해당 캐시를 사용해도 됙는지 확인하겠다는 의미
    • must-revalidate : 만료된 캐시만 서버에서 확인
    • public : 공유 캐시에 저장해도 된다는 의미
    • private :'브라우저' 같은 특정 사용자 환경에서만 저장
    • max-age : 캐시의 유효기산 명시
  • Transfer-Encoding : body 내용 자체 압축 방식 지정
  • Upgrade : 프로토콜 변경할 때 사용
  • Via : 중계 서버의 이름, 버전, 호스트명
  • Expires : 자원의 만료 일자
  • Allow : 사용이 가능한 HTTP 메소드 방식
  • Last-Modified : 최근 수정된 날짜
  • ETag : 캐시 업데이트 정보를 위한 임의의 식별 숫자

⏺️ 엔티티(Entity) 헤더
요청 및 응답 메시지에서도 모두 사용 가능, HTTP 바디에 내용이 존재할 때 이 내용에 대한 세부 정보를 나타내는데 사용합니다.
보통 응답 메세지에 Content 헤더들이 포함되어 있습니다.

  • Content-Encoding : 본문의 리소스 압축 방식
  • Content-type : 본문의 미디어 타입과 문자열 인코딩을 지정하기 위해 사용
  • Content-length : 본문의 길이, 메세지 크기에 따라 자동으로 생성
  • Content-language : 사용자가 선호하는 언어에 따라 사용자를 구분할 수 있게 합니다.

➡️ HTTP 요청 헤더
요청한 URL, 메소드, 요청 생성에 사용된 브라우저 및 기타 정보와 같은 요청에 대한 정보가 포함된다.

  • Host : 요청하려는 서버 호스트 이름과 포트 번호
  • User-agent : 현재 사용자가 어떤 클라이언트를 이용해 요청을 보냈는지 확인

    이 정보를 통해서 서버는 클라이언트 프로그램에 맞는 최적의 데이터를 보내줄 수 있습니다.
  • Accept : 요청을 보낼 때 서버에게 어떤 타입으로 응답을 보내줬으면 좋겠다고 명시합니다. 브라우저가 알아서 생성합니다.
  • Referer : 바로 직전에 머물렀던 웹 링크 주소
  • Accept-charset : 클라이언트가 지원 가능한 문자열 인코딩 방식
  • Accept-language : 클라이언트가 지원가능한 언어 나열
  • Accept-encoding : 클라이언트가 해석가능한 압축 방식 지정
  • Authorization : 사용자가 서버에 인가된 사용자임을 증명할 때 사용
  • Content-location : 해당 개체의 실제 위치
  • Content-disposition : 응답 메시지를 브라우저가 어떻게 처리할지
    ex) inline, attachment; filename="hang-pro.xlsx"
  • Content-Security-policy : 다른 외부 파일을 불러오는 경우 차단할 리소스와 불러올 리소스 명시
    ex) default-src https : https로만 파일을 가져온다
    ex) default-src 'self' : 자기 도메인에서만 가져온다.
    ex) default-src 'none' : 외부 파일은 가져올 수 없다
  • If-Modified-Since : 여기세 쓰여진 시간 이후로 변경된 리소스 취득.
    페이지가 수정되었으면 최신 페이지로 교체하기 위해서 사용
  • Origin : 서버로 Post 요청을 보낼 때 요청이 어느 주소에서 시작되었는지 나타내는 값으로 이 값으로 요청을 보낸 주소와 받는 주소가 다르면 CORS 에러가 난다.
  • Cookie : 쿠키 값 key-value로 표현된다

⬅️ HTTP 응답 헤더

  • Location : 301, 302 상태 코드일 때만 볼 수 있는 헤더로 서버의 응답이 다른 곳에 있다고 알려주며서 해당 URL를 지정
  • Server : 웹 서버의 종류를 나타낸다
  • Age : max-age 시간 내에서 얼마나 흘렀는지 초 단위의 값
  • Referrer-policy : 서버의 정책을 알려주는 값
  • WWW-Authenticate : 사용자 인증이 필요한 자원을 요구할 시 서버가 제공하는 인증 방식
  • proxy-Authenticate : 요청한 서버가 프록시 서버인 경우 유저 인증 위한 값

✅ HTTP 메세지 바디

실제 전송할 데이터를 담는 부분으로 html, 이미지, 영상, JSON 등 byte로 표현할 수 있는 모든 데이터 전송


Socket 통신

소켓이란 프로세스간의 통신에 사용되는 양쪽 끝단을 의미합니다.
서로 멀리 떨어진 두 사람이 통신하기 위해 전화기가 필요한 것처럼
프로세스간의 통신을 위해서는 그 무언가가 필요하고 그것이 바로 소켓입니다.

즉 네트워크상에서 동작하는 프로그램 간 통신의 종착점이며 종착점은 IP와 Port번호의 조합으로 이루어진 최종 목적지를 나타냅니다.

HTTP 통신과 다르게 Server와 Client가 특정 Port를 통해 연결을 성립하고 있어 실시간으로 양방향 통신을 하는 방식입니다.

실시간 streaming 중계나 실시간 채팅과 같이 즉각적으로 정보를 주고 받는 경우에 사용합니다.

자바에서는 java.net 패키지를 통해 소켓 프로그래밍을 지원합니다.

TCP와 UDP

TCP = 📞 = 전화
UDP = 📦 = 소포

TCP와 UDP 모두 TCP/IP 프로토콜에 포함되어 있으며, OSI 7계층의 전송계층에 해당하는 프로토콜입니다.

TCP와 UDP는 전송 방식이 다르며, 각 방식에 따른 장단점이 있습니다.

항목TCPUDP
연결 방식연결기반(connection-oriented)
- 연결 후 통신(전화기)
- 1:1 통신방식
비연결기반(connectionless-oriented)
- 연결없이 통신(소포)
1:1, 1:n, n:n 통신방식
특징데이터의 경계를 구분안함(byte-stream)
신뢰성 있는 데이터 전송
- 데이터의 전송 순서가 보장된다.
-데이터의 수신여부를 확인한다.
(데이터가 손실되면 재전송)
- 패킷을 관리할 필요가 없다.
UDP보다 전송 속도가 느리다.
데이터의 경계를 구분한다(datagram)
신뢰성 없는 데이터 전송
- 데이터의 전송 순서가 바뀔 수 있다.
- 데이터의 수신여부를 확인하지 않는다.
(데이터가 손실되어도 알 수 없다)
- 패킷을 관리해주어야 한다.
TCP보다 전송속도가 빠르다.
관련 클래스Socker
ServerSocket
DatagramSocket
DatagramPacket
MulticastSocket

TCP 소켓 프로그래밍

  1. 서버 프로그램에서는 서버 소켓을 사용해서 서버 컴퓨터의 특정 포트에서 클라이언트의 연결요청을 처리할 준비를 한다.
     ServerSocket serverSocket = new ServerSocket(7777);
  2. 클라이언트 프로그램은 접속한 서버의 IP주소와 포트 정보를 가지고 소켓을 생성해서 서버에 연결을 요청한다.
    Socket socket = new Socket("192.168.45.184", 7777);
  3. 서버소켓은 클라이언트의 연결요청을 받으면 서버에 새로운 소켓을 생성해서 클라이언트 소켓과 연결되로록 한다.
    Socket socket = serverSocket.accept();
  4. 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 서버소켓과 관계없이 일대일 통신을 한다.

즉 서버소켓의 역할은 포트와 결합되어 포트를 통해 원격 사용자의 연결요청을 기다리다가 연결요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결하는 것입니다.

실제 데이터 통신은 서버소케과 관계없이 소켓과 소켓 간에 이루어집니다.

소켓은 두 개의 스트림을 가지고 있습니다.

  • InputStream
  • OutputStream
    이 두 개의 스프림은 상대편 스트림과 교차로 연결됩니다.

예제 1

⏺️ Server 프로그램입니다.

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TcpIpServer {
   public static void main(String[] args) {

      ServerSocket serverSocket = null;

      try{
          //서버 소켓을 생성하여 7777번 포트와 결합
          serverSocket = new ServerSocket(7777);
          System.out.println(getTime()+"서버가 준비되었습니다.");
      } catch (IOException e) {
          e.printStackTrace();
      }

      while(true){
          try{
              System.out.println(getTime()+"연결 요청을 기다립니다.");
              //서버 소켓은 클라이언트의 연결요청이 올 때까지 실행을 멈추고 계속 기다린다.
              //클라이언트의 연결요청이 오면 클라이언트 소켓과 통신할 새로운 소켓을 생성한다.
              Socket socket = serverSocket.accept();
              System.out.println(getTime() + socket.getInetAddress() +"로부터 연결요청이 들어왔습니다.");

              //소켓 출력스트립을 얻는다.
              OutputStream out = socket.getOutputStream();
              DataOutputStream dos = new DataOutputStream(out);

              //원격 소켓에 데이터를 보낸다.
              dos.writeUTF("[Notice] Test Message1 from Server.");
              System.out.println(getTime() + "데이터를 전송했습니다.");

              //스트림과 소켓을 닫아준다.
              dos.close();
              socket.close();
          }catch (IOException e){
              e.printStackTrace();
          }
      }
  }

  static String getTime(){
      SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
      return f.format(new Date());
  }
}

⏺️ Client 프로그램입니다.

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.util.SortedMap;

public class TcpIpClient{
   public static void main(String[] srgs){
       try {
           String serverIp = "192.168.45.184";

           System.out.println("서버에 연결중입니다. 서버IP :" + serverIp);
           //소겟을 생성하여 연결을 요청한다.
           Socket socket = new Socket(serverIp, 7777);

           //소켓의 입력스트림을 얻는다.
           InputStream in = socket.getInputStream();
           DataInputStream dis = new DataInputStream(in);

           //소켓으로부터 받은 데이터를 출력한다.
           System.out.println("서버로부터 받은 메시지 : " + dis.readUTF());
           System.out.println("연결을 종료합니다");

           //스트림 소켓을 닫는다.
           dis.close();
           socket.close();
           System.out.println("연결이 종료되었습니다.");
       }catch (ConnectException ce){
           ce.printStackTrace();
       }catch (IOException ie){
           ie.printStackTrace();
       }catch (Exception e){
           e.printStackTrace();
       }

   }

}

위에서

String serverIp = "192.168.45.184";

은 서버프로그의 IP 주소입니다.!

중요한 것 몇가지를 살펴보겠습니다.

  • 소켓 통신은 양방향이기 때문에 위 예시는 서버가 데이터를 쓰고 클라이언트에서 데이터를 출력합니다.
  • 서버와의 작업이 끝나면 소켓과 스트림을 닫아야 합니다. 그렇지 않으면 계속 연결된 상태이므로 자원이 낭비됩니다.

예시 2 클라이언트의 포트

연결을 요청한 클라이언트 프로그램의 소켓은 사용한 임의의 포트가 선택됩니다. 예시는 서버소켓이 7777번의 포트를 사용합니다. 이 7777번 포트는 서버소켓이 아닌 다른 소켓이 사용할 수 있습니다!

클라이언트 프로그램의 소켓이 7777번일 수도 있습니다.

클라이언트 프로그램으로부터 접속 요청이 오면 서버 프로그램에서 아래와 같이 포트 번호를 출력하는 부분을 추가했습니다.

그 결과는 아래와 같습니다. 보면 클라이언트 프로그램의 포트 번호를 실행할 때마다 임의로 생성된다는 것을 알 수 있습니다.

예시 3 서버소켓의 대기시간 지정

ServerSocket 클래스의 setSoTimeout(int timeout)을 사용해서 서버소켓의 대기시간을 지정할 수 있습니다. 클라이언트 프로그램의 접속 요청이 올 때까지 무한정 기다리는 것이 아니라 제한시간을 두어 제한 시간 이내에 요청이 들어오지 않으면 accept()에서 SocketTimeoutException이 발생합니다.


아래의 결과를 보면 처음에는 5초 이내에 클라이언트 프로그램으로부터 연결 요청이 왔지만 이후 5초가 지나니 서버 프로그램이 종료되었습니다.

예시 4 쓰레드

쓰레드를 생성하여 클라이언트의 요청을 동시에 처리하는 예시입니다.
서버에 접속하는 클라이언트가 많을 때 요청한 순서대로 처리하면 늦게 접속한 클라이언트는 오랜 시간 기다려야 합니다. 따라서 병렬적으로 동시에 처리하는 쓰레드를 적용한 예시를 살펴봅시다.

서버는 배열을 통해서 여러개를 만들었습니다.

TcpServer4의 생성자는 아래와 같습니다. 생성자에서 서버소켓을 생성하여 7777번 포트와 결합합니다.

run()과 start()메서드는 아래와 같습니다.

  public void start() {
        for(int i=0;i<threadArr.length;i++){
            threadArr[i]= new Thread(this);
            threadArr[i].start();
        }
    }

    public void run() {
        while(true){
            try {
                System.out.println(getTime() + "가 연결요청을 기다립니다.");

                Socket socket = serverSocket.accept();
                System.out.println(getTime() + socket.getInetAddress()
                        + "로부터 연결요청이 들어왔습니다.");

                //소켓의 출력스트림을 얻는다.
                OutputStream out = socket.getOutputStream();
                DataOutputStream dos = new DataOutputStream(out);

                //원격 소켓에 데이터를 보낸다.
                dos.writeUTF("[Notice] Test Message1 from Server.");
                System.out.println(getTime() + "데이터를 전송했습니다.");

                //스트림과 소켓을 닫아준다.
                dos.close();
                socket.close();
            }catch (IOException e){
                e.printStackTrace();
            }


        }


    }

아래의 결과를 보면 연결요청을 기다리는 쓰레드 5개가 한번에 같이 연결요청을 기다립니다.

예시 5 1:1 채팅

서버와 클라이언트 프로그램에서는 두 종류의 스트림 InputStream과 OutputStream 모두 존재합니다.
보면 두개의 클래스를 선언하여 socket의 데이터를 쓰는 것과 데이터를 읽는 것을 분리하여 만들었습니다.

socket에 데이터를 쓰는 Sender

socket에 데이터를 읽는 Receiver

그리고 마지막에 클라이언트 프로그램입니다.

신기하게 1:1 채팅이 구현되었습니다.

UDP 소켓 프로그래밍

TCP와 다르게 연결지향적인 프로토콜이 아니기 때문에 서버소켓이 필요하지 않습니다.

사용하는 소켓은 DatagramSocket이며 데이터는 DatagramPacket에 담아서 전송합니다.

DatagramPacket = 헤더(수신할 호스트의 정보) + 데이터

client 예시

import javax.xml.crypto.Data;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class UdpClient {

    public void start() throws IOException, UnknownHostException{

        DatagramSocket datagramSocket = new DatagramSocket();
        InetAddress serverAddress = InetAddress.getByName("127.0.0.1");

        //데이터가 저장될 공간으로 byte배열을 생성한다.
        byte[] msg = new byte[100];

        DatagramPacket outPacket =
                new DatagramPacket(msg,1,serverAddress,7777);
        DatagramPacket inPacket = new DatagramPacket(msg, msg.length);

        datagramSocket.send(outPacket);
        datagramSocket.receive(inPacket);

        System.out.println("current server time :"+
                new String(inPacket.getData()));

        datagramSocket.close();
    }

    public static void main(String[] args){
        try{
            new UdpClient().start();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

Server 예시

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;

public class UdpServer {
    public void start() throws IOException {

        DatagramSocket socket = new DatagramSocket(7777);
        DatagramPacket inPacket, outPacket ;

        byte[] inMsg = new byte[10];
        byte[] outMsg;

        while(true){
            //데이터를 수신하기 위한 패킷을 생성한다.
            inPacket = new DatagramPacket(inMsg, inMsg.length);

            //패킷을 통해 데이터를 수신한다.
            socket.receive(inPacket);

            //수신할 패킷으로부터 client의 IP주소와 Port를 얻는다,
            InetAddress address = inPacket.getAddress();
            int port = inPacket.getPort();

            //서버의 현재 시간을 시분초 형태로 변환한다.
            SimpleDateFormat sdf = new SimpleDateFormat("[hh:mm:ss]");
            String time = sdf.format(new Date());
            outMsg = time.getBytes();//time을 byte배열로 변환한다,

            //패킷을 생성해서 client에게 전송한다.
            outPacket = new DatagramPacket(outMsg, outMsg.length,address,port);
            socket.send(outPacket);


        }


    }

    public static void main(String[] args){
        try{
            new UdpServer().start();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

reference

자바의 정석 chapter 16 네트워킹
https://bentist.tistory.com/35
https://kotlinworld.com/75

profile
꾸준하게 Ready, Set, Go!

0개의 댓글

관련 채용 정보