컴퓨터네트워크

[JAVA] 소캣 프로그래밍 HTTP 통신 구현

begong 2023. 1. 9. 20:00
반응형

HTTP 통신

HTTP Request

형식

image

  • HTTP 통신 전용 포트번호 80
  • 요청 라인이 가장 중요
  • 한 줄이 끝나면, cr lf로 줄을 바꿔주어야 함.
  • System.lineSeparator() 으로 줄바꿈
  • Header 부분이 끝나면 줄바꿈을 한번 더하여 header가 끝났음을 나타냄
  • HTTP method 중 get, head는 개체 몸체가 없음

Client

// ip주소와 포트는 고정
static final String IPADDRESS = "127.0.0.1";
static final int PORTNUM = 80;
static final String requestFormat = "%s / HTTP/1.1" + System.lineSeparator() +
                                    "Host :" + IPADDRESS + System.lineSeparator() +
                                    System.lineSeparator();

//서버로 HTTP Request문 전송
//String.format을 사용하여 requestFormat안에 요청을 넣어줌
//Scanner를 통해 요청을 받음
printWriter.println(String.format(requestFormat, request));

HTTP Response

형식

image

  • 상태 라인이 응답에서 가장 중요

  • 각 줄이 끝나면 줄바꿈을 해주어야 함

  • Header 부분이 끝나면 줄바꿈을 한번 더하여 header가 끝났음을 나타냄

  • Head method의 경우 개체 몸체가 없음.

    static final String resposeFormat = "HTTP/1.1 200 OK" + System.lineSeparator() +
                                      "Date: "+ new Date().toString() +System.lineSeparator() +
                                      System.lineSeparator();
    printWriter.println(responseFormat);

ezgif com-gif-maker
ezgif com-gif-maker

  • HTTP 프로토콜 형식에 맞게 메시지를 보내니 WireShark에서 HTTP 통신으로 인식한다

HTTP Request method

Method 역할
CONNECT 요청한 리소스에 대해 양방향 연결을 시작하는 메소드
DELETE 지정한 리소스를 삭제
GET 특정한 리소스를 가져오도록 요청
HEAD GET 메서드로 요청했을 때 돌아올 헤더를 요청
OPTIONS 목표 리소스와의 통신 옵션을 설명하기 위해 사용
PATCH 리소스의 부분적인 수정을 할 때에 사용
POST 서버로 데이터를 전송
PUT 요청 페이로드를 사용해 새로운 리소스를 생성하거나, 대상 리소스를 나타내는 데이터를 대체

HTTP Response code, message

code message 역할
200 OK 요청이 성공했음을 나타냄
201 Created 요청이 성공적으로 처리되었으며, 자원이 생성되었음을 나타냄
204 No Content 요청이 성공했으나 클라이언트가 현재 페이지에서 벗어나지 않아도 된다는 것을 나타냄
400 Bad Request 서버가 클라이언트 오류를 감지해 요청을 처리할 수 없거나, 하지 않는 것을 나타냄
404 Not Found 서버가 요청받은 리소스를 찾을 수 없다는 것을 의미
505 HTTP Version Not Supported 서버에서 해당 Http 버전을 지원하지 않을 때

구현 내용

HTTP Request

  • Method GET, HEAD, POST, PUT만 사용
  • 연결 완료 후 Scanner를 통해 사용자가 원하는 method를 받음
    • 위의 4개 이외의 값이 들어올 경우 응답으로 400 Bad Request
  • POST, PUT의 경우 추가로 data를 입력받음
    • POST : 데이터 추가
    • PUT : 데이터 대체

HTTP Response Case

Method Code 특징
" " 505 HTTP 1.1이 아니면 발생
" " 400 GET, HEAD, POST, PUT 이외의 Method 입력시 발생
HEAD 404 서버에 데이터 X, Header부분만 전송
HEAD 200 서버에 데이터 O, Header부분만 전송
POST 201 서버에 데이터 X, 데이터 생성 개체몸체 없음
POST 200 서버에 데이터 O, 데이터 생성 데이터를 개체몸체로 보내줌. 기존의 데이터 + 추가데이터
PUT 201 서버에 데이터 X, 데이터 생성 개체몸체 없음
PUT 204 서버에 데이터 O, 데이터 생성 개체몸체 없음. 새 데이터로 교체
GET 404 서버에 데이터 X, 개체몸체로 추가 안내메시지 전송
GET 200 서버에 데이터 O, 개체몸체로 데이터 전송
  • PUT 메소드의 경우 멱등성(동일한 요청을 여러번 연속으로 보내도 한번 보내는 것과 같은 효과를 지님)을 지님
    • 따라서 데이터가 완전 대체되게 구현
  • POST 메소드의 경우 멱등성이 없기 때문에 기존의 데이터에 데이터가 추가되는 형태로 구현
  • 400 Bad Request의 경우 클라이언트로부터 잘못된 HTTP Method를 전달 받았을 때 발생
  • 404 Not Found는 서버에 전역변수 데이터가 없을 시 일어나게 구현

HTTP Response Header

  • Date
  • Content-length (개체 몸체가 없을 때 추가되지 않음)
  • Content-type (개체 몸체가 없을 때 추가되지 않음)

Client

client.java

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

public class client {
    static final String IPADDRESS = "127.0.0.1";
    static final int PORTNUM = 80;
    static final String requestFormat = "%s / HTTP/1.1" +System.lineSeparator()+
                                        "Host :"+IPADDRESS+System.lineSeparator() +
                                        System.lineSeparator();

    public static void main(String[] args) throws IOException {
        try(Socket socket = new Socket(IPADDRESS, PORTNUM)) {
            System.out.println("서버연결완료");

            PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
            Scanner scanner = new Scanner(System.in);

            //사용자 요청받기
            System.out.println("요청을 입력하시오. GET/HEAD/POST/PUT");
            String request = scanner.next().toUpperCase();
            //요청 구별해 서버에 전송
            switch (request) {
                case "POST":
                case "PUT":
                    // Post, put 요청시 사용자가 원하는 데이터 받기
                    System.out.println("data를 입력하시오");
                    String dataRequest = scanner.next();
                    printWriter.println(String.format(requestFormat, request) + dataRequest);
                    break;
                default:
                    printWriter.println(String.format(requestFormat, request));
            }
            printWriter.flush(); //flush를 해야 데이터가 전송됨.

            //응답
            InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            //헤더부분 읽기
            while (true) {
                String str = bufferedReader.readLine();
                if (str.isEmpty()) {
                    break;
                }
                System.out.println(str);
            }

            //Data 부분이 있다면 읽기 (\r\n\r\n 때문에 data부분이 한번에 안읽힘.
            String str= bufferedReader.readLine();
            if(str != null){
                System.out.println(str);
            }

        }
    }
}
  • bufferReader.readLine()이 줄바꿈이 두번 연속 있을 시 개체 몸체를 인식 못해서 추가로 출력하게 코드를 작성
  • System.lineSeparator()를 사용해 운영체제 상관없이 줄바꿈

Server

server.java


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

public class server {
    public static void main(String[] args) throws IOException {
        //서버 오픈
        try(ServerSocket serverSocket = new ServerSocket(80)){
            while (true){
                //클라이언트 연결
                try(Socket socket = serverSocket.accept()){
                    System.out.println("client connected");

                    //Client에서 메시지 받아옴
                    InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
                    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                    // 전송 메시지 담아 처리할 배열
                    ArrayList<String> dataList = new ArrayList<>();

                    //Header 부분 읽기,배열에 담기
                    System.out.println("받은 메시지----------------");
                    try{
                        while (true) {
                            String str = bufferedReader.readLine();
                            dataList.add(str);
                            if (str.isEmpty()) {
                                break;
                            }
                            System.out.println(str);
                        }
                        //Data 부분 읽기, 배열에 담기 (\r\n\r\n 때문에 data부분이 한번에 안읽힘).
                        if(dataList.get(0).contains("POST") || dataList.get(0).contains("PUT")){
                            String str = bufferedReader.readLine();
                            System.out.println(str);
                            dataList.add(str);
                        }
                    }catch (Exception e){
                        //소켓 연결은 되었지만, 클라이언트가 메시지 전송을 하지 않고 접속이 비정상적으로 끝났을때.
                        System.out.println("비정상적인 접속종료");
                        continue;
                    }

                    //요청 분석 후 메시지 생성
                    RequestHttp header = new RequestHttp(dataList);
                    PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
                    printWriter.println(header.getMessage());
                    System.out.println("보낸 메시지:-----------");
                    System.out.println(header.getMessage());

                    //전송
                    printWriter.flush();
                }

            }
        }catch (IOException e){
            e.printStackTrace();
        }


    }
}
  • Client로 부터 전송된 HTTP Request를 한줄씩 terminal에 띄워주며 ArrayList에 저장
  • ArrayList를 RequestHttp class에 넘겨 각종 데이터를 처리
  • 예외처리를 통해 비정상적으로 client의 접속이 끊겼을 때 서버가 종료되지 않게 처리.

RequestHttp.java

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

public class RequestHttp {
    private ArrayList<String> dataList;
    final private static String[] METHOD_LIST= {"GET", "POST", "PUT", "HEAD"};

    private String method;
    private String url;
    private String HTTPversion;
    private String date;

    private String contentLength;
    private String contentType;
    private String content;
    private String stateCode;
    private String stateMessage;

    //전역 변수로 저장해 서버가 실행되는 동안 유지됨
    private static String data;

    String message;

    public RequestHttp(ArrayList<String> dataList){
        this.dataList = dataList;
        //요청 라인 나누기, 필요한 데이터 추출
        String[] requestLine = dataList.get(0).split(" ");
        this.method = requestLine[0];
        this.url = requestLine[1];
        this.HTTPversion = requestLine[2].trim(); //끝 개행문자 제거
        this.date = new Date().toString();
        doRequest();
    }

    // 요청라인 바탕으로 response메시지에 담길 요소 생성
    private void doRequest(){
        //버전 불일치
        if (!HTTPversion.equals("HTTP/1.1")){
            stateCode = "505";
            stateMessage = "HTTP Version Not Supported";
            setContent("HTTP Version Not Supported");
            return;
        }
        //잘못된 method가 왔을때.=> 400 BAD Request.
        if (!Arrays.asList(METHOD_LIST).contains(method)){
            stateCode = "400";
            stateMessage = "BAD REQUEST";
            setContent("bad request. check your request");
            return;
        }

        //head : 응답본문 없음
        if(method.equals("HEAD")){
            if(data == null){
                //Data 없을때
                stateCode = "404";
                stateMessage = "Not Found";
                return;
            }else{
                //data 있을 때.
                stateCode = "200";
                stateMessage = "OK";
                setContent(data);
                return;
            }
        }

        if (method.equals("POST")) {
            if (data == null) {
                //데이터 추가. 응답 본문 x
                stateCode = "201";
                stateMessage = "Created";
                data = dataList.get(dataList.size() - 1);
                return;
            } else {
                //데이터 수정(기존+새 데이터), 응답 본문 o
                stateCode = "200";
                stateMessage = "OK";
                data += dataList.get(dataList.size() - 1);
                setContent("post done : "+data);
            }
        }

        if (method.equals("PUT")){
            //put은 멱등성을 가짐 =>한번을 보내도 여러번을 연속으로 보내도 같은 효과를 지님
            if(data == null){
                //데이터 생성. 응답 본문 x
                stateCode = "201";
                stateMessage = "Created";
                data = dataList.get(dataList.size()-1);
                return;
            }else{
                //데이터 수정(새 데이터)
                //응답 본문 x
                stateCode = "204";
                stateMessage = "No content";
                data = dataList.get(dataList.size()-1);
                return;
            }
        }

        if (method.equals("GET")){
            //전역변수 data 없을 때 안내 메시지 생성
            if(data == null){
                stateCode = "404";
                stateMessage = "Not Found";
                setContent("Not found please post or put first");
                return;
            }else{
                stateCode = "200";
                stateMessage = "OK";
                setContent(data);
                return;
            }
        }
    }

    private void setMessage(){
        message = HTTPversion+" "+ stateCode+ " "+ stateMessage +System.lineSeparator()+
                "Date: "+date+ System.lineSeparator();

        // 개체 몸체 없을때는 불필요한 헤더 전송안함.
        if (content != null){
            message +="Content-length: "+contentLength + System.lineSeparator()+
                    "Content-type: "+contentType+System.lineSeparator();
        }
        message += System.lineSeparator();
        //head 메소드, 응답코드 201, 204 일 경우 entity body 없음.
        if (!method.equals("HEAD")  && !stateCode.equals("201")&& !stateCode.equals("204")){
            message +=content;
        }
    }

    //개체몸체 및 헤더 설정
    public void setContent(String content){
        this.content = content;
        this.contentLength = String.valueOf(content.getBytes(StandardCharsets.UTF_8).length);
        this.contentType = "Text";
    }

    public String getMessage(){
        setMessage();
        return message;
    }
}
  • Client에서 전송된 메시지를 기반으로 동작
  • 요청라인과 개체몸체 사용
    • 헤더라인들은 dataList안에 있음
  • Http/1.1 이외의 버전 사용불가
    • 505 HTTP Version Not Supported 발생

실행결과

data 없을때 GET, HEAD

ezgif com-gif-maker

  • 404 not found 응답

PUT

ezgif com-gif-maker (1)

  • 사용자가 입력한 데이터가 전역변수에 저장
  • 저장 후 GET 입력시 데이터 받아옴
  • 데이터가 있을 때 PUT 입력시 데이터 대체

POST

ezgif com-gif-maker

  • 사용자가 입력한 데이터가 전역변수에 저장
  • 데이터 있을 때 Post시 데이터 뒤에 추가

400 Bad Request

ezgif com-gif-maker

  • 잘못된 method 입력 시 400 bad Request

비정상적 client 종료

ezgif com-gif-maker

  • 예외처리를 통해 client가 비정상적으로 종료되어도 서버에 문제 없음.
반응형