WAS = Web Application Server
톰캣과 같은 웹서버를 사용할 때, WAS에 대해 조금이라도 아는 것이 도움이 되기에 정리를 해봅니다.
과정은. 1) 접속 2) 요청 3) 응답 4) 연결close 4단계 과정입니다.
1) 접속
우선 main 메소드를 사용할 Main 클래스를 만들어 줍니다.
public class Main {
public static void main(String[] args) {
MiniWAS was = new MiniWAS();
was.start();
System.out.println("was 를 시작하겠습니다~");
}
}
MiniWAS 클래스에서 Connector 클래스의 생성자를 이용해 포트번호를 connector 객체로 보냅니다.
public class MiniWAS extends Thread{
public MiniWAS() {
}
@Override
public void run() {
Connector connector = new Connector(1472);
connector.run();
}
}
Connector 클래스에서는 ServerSocket 을 생성하고 포트번호를 ServerSocket 에 넣어줍니다.
브라우져에서 localhost:포트번호 를 입력하면 접속을 할 수 있습니다.
브라우져에서 요청을 기다리다가. 요청이 오게 되면 ServerSocket의 메소드인 accept()는 Socket 을 리턴하게 됩니다.
그 Socket 을 이용해 SocketHandler 클래스에서 브라우저의 요청을 읽을 수 있고 다시 브라우져로 값을 전달할 수 있습니다.
여기까지가 웹서버 접속 과정입니다.
public class Connector {
private int port;
public Connector(int port) {
this.port = port;
}
public void run() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
// index.html 을 불러올때
// 여기에서 while 을 해주면 사진도 불러오고.
// while 없으면 그냥 html 문서만 읽고 사진은 안불러온다.
// while(true) {
// Socket socket = serverSocket.accept();
// SocketHandler handler = new SocketHandler(socket);
// handler.start();
//
// }
// 1 번만 호출하면 이미지를 불러오지 못한다. 왤까???
for(int i=0 ; i < 2 ; i++) {
Socket socket = serverSocket.accept();
SocketHandler handler = new SocketHandler(socket);
handler.start();
}
}catch(Exception ex) {
ex.printStackTrace();
}finally {
try {
serverSocket.close();
} catch(Exception e) {}
}
}
}
* while(true) 의 유무에 따라 출력 결과가 다르다. 더 알아봐야겠다.
** accept() 메소드를 한번만 실행할때. 기본적인 문자만 작성된 html 문서는 출력이 된다. 하지만
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>index.html</title>
</head>
<body>
Hello World!
<img src="picture2.jpg" >
</body>
</html>
~
아래와 같이 img src 를 추가하면. 이미지는 출력이 되지 않는다. 왜나하면 img 파일을 불러오는것 또한 요청 작업이기 때문에 한번의 accept() 로는 불가능한것이다.
해서 추가적인 요청이 필요한 문서들은 accept() 문서를 여러번 출력해줘야한다.
JPG 파일의 URL을 보면 localhost:1472/picture2.jpg 인것이 보일것입니다.
accept() 를 한번만 실행했을 때! 그때는 localhost:1472/ 만 작동이 됩니다. 사진의 URL을 얻기 위해선 한번더 accept() 메소드의 동작이 필요합니다.!
2) 요청 ( 요청라인, 헤더, 빈줄, 바디)
브라우저에서 요청을 하면 위와 같은 값들을 가져올 수 있습니다.
요청라인 , 헤더 , 비어있는 줄 , 바디 순으로 나타나는데. 이것은 Http 프로토콜(규약) 이기 때문에 규칙을 지켜야 합니다. --> 아무 의미 없는 빈줄이 아닙니다.
(GET 방식엔 바디가 없습니다. POST 방식엔 존재.)
요청라인의 GET 은 Http 메소드를 타나내고. 그 뒤에 / 은 Path를 말합니다. 뒤에 HTTP/1.1 은 버전을 말합니다.
이 요청 데이터를 Request 객체에 담겠습니다.
public class SocketHandler extends Thread {
private Socket socket;
public SocketHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
in = socket.getInputStream();
BufferedReader br= new BufferedReader(new InputStreamReader(in));
out = socket.getOutputStream();
PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
Request req = new Request(in,br);
Response res = new Response(out,pw);
DefaultServlet servlet = new DefaultServlet();
servlet.service(req,res);
System.out.println("service 메소드 이후에 : " + res.getContentLength());
} catch(Exception ex) {
System.out.println("SocketHandler 에서의 에러 : " + ex.getMessage());
} finally {
try {
in.close();
}catch(Exception e){}
try {
out.close();
}catch(Exception e){}
try {
socket.close();
}catch(Exception e){}
}
}
}
public class Request {
private InputStream in;
private BufferedReader br;
private String httpMethod;
private String httpPath;
private Map<String,String> headers;
public Request(InputStream in , BufferedReader br) {
this.in = in;
this.br = br;
headers = new HashMap<>();
try {
String requestLine = br.readLine();
System.out.println("요청라인 : " + requestLine);
String[] requestLines = requestLine.split(" ");
if(requestLines.length > 2) {
httpMethod = requestLines[0];
httpPath = requestLines[1];
}
String headerLine = null;
String key = null;
String value = null;
while((headerLine = br.readLine() ) != null) {
if(headerLine.equals("")) {
break;
}
System.out.println("Header : " + headerLine);
int idx = headerLine.indexOf(':');
if(idx != -1) {
key = headerLine.substring(0,idx);
value = headerLine.substring(idx+1).trim();
}
headers.put(key,value);
}
}catch(Exception ex) {
throw new RuntimeException();
}
}
public InputStream getIn() {
return in;
}
public BufferedReader getBr() {
return br;
}
public String getHttpMethod() {
return httpMethod;
}
public String getHttpPath() {
return httpPath;
}
public Map<String, String> getHeaders() {
return headers;
}
}
3) 응답 ( 응답라인, 헤더, 빈줄, 바디)
브라우저의 요청에 서버가 응답하는 코드입니다.
위에 SocketHandler 클래스의 servlet.service() 부분을 구현하는 코드 입니다. Response 클래스에서는 일단 out,pw 만 이용했지만 다른 변수들도 이용할 수 있게
바꿔보시길 바랍니다.
public class Response {
private OutputStream out;
private PrintWriter pw;
private String contentType;
private long contentLength;
private int statusCode;
private String statusMessage;
public Response(OutputStream out, PrintWriter pw) {
this.out = out;
this.pw = pw;
}
public OutputStream getOut() {
return out;
}
public void setOut(OutputStream out) {
this.out = out;
}
public PrintWriter getPw() {
return pw;
}
public void setPw(PrintWriter pw) {
this.pw = pw;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
public void setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
}
}
pw.println 부분이 응답라인과 헤더를 입력하는 겁니다. 요청과 마찬가지로 응답 역시 Http 프로토콜을 지켜야 합니다. 그래서 pw.println("") 을 넣어주는 겁니다.
( 규칙이기 때문에 왜 넣어줘야는 의문은 안가져도 됩니다.)
Body 부분을 브라우져로 보내줄때는 stream 타입으로 보내야 합니다. 해서 FileInputStream 을 이용하는 겁니다.
public class DefaultServlet {
public void service(Request req , Response res) {
// 응답!
String webpath = req.getHttpPath();
if("/".equals(webpath)) {
webpath = webpath + "index.html";
}
String path = "/tmp/wasfolder" + webpath;
File file = new File(path);
res.setContentLength(file.length());
OutputStream out = res.getOut();
PrintWriter pw = res.getPw();
if(file.exists()) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html; charset=UTF-8");
pw.println("Content-Length: " + file.length());
pw.println("");
pw.flush();
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
byte[] buffer = new byte[1024];
int readCount =0;
while((readCount = fis.read(buffer)) != -1) {
out.write(buffer, 0, readCount );
out.flush();
}
}catch(Exception ex) {
System.out.println("DefaultServlet Error : " + ex.getMessage());
}finally {
try {
fis.close();
}catch(Exception e){}
}
} else {
pw.println("HTTP/1.1 404 NOT FOUND");
pw.println("Content-Type: text/html; charset=UTF-8");
pw.println("");
pw.flush();
}
}
}
응답과정이 끝나면 SocketHandler 및 Connector 에서 close를 하며 마지막 단계를 진행하게 됩니다.