[Java] 간단한 멀티쓰레드 웹서버 구현하기

학교 과제로 냈었던-_-a Java로 구현한 멀티쓰레드 웹서버입니다. Java로 구현할 수 있는 가장 기본적인 형태의 소켓 프로그래밍을 이용하여 구현하였습니다. 그냥 요청 받으면 해당 파일을 읽어서 내용을 보내는 단순한 어플리케이션입니다. Thread safe한 설계는 하지 않았습니다. 그냥 공부용으로만 사용해주세요.

* WebServer.java

class WebServer
{
    public static void main(String argv[]) throws Exception
    {
        // 서버소켓을 생성한다. 웹서버는 기본적으로 80번 포트를 사용한다.
        ServerSocket listenSocket = new ServerSocket(80);
        System.out.println("WebServer Socket Created");

        Socket connectionSocket;
        ServerThread serverThread;

        // 순환을 돌면서 클라이언트의 접속을 받는다.
        // accept()는 Blocking 메서드이다.
        while((connectionSocket = listenSocket.accept()) != null)
        {
            // 서버 쓰레드를 생성하여 실행한다.
            serverThread = new ServerThread(connectionSocket);
            serverThread.start();
        }
    }
} 

* ServerThread.java

public class ServerThread extends Thread
{
  // 파일 요청이 없을 경우의 기본 파일
  private static final String DEFAULT_FILE_PATH = "index.html";

  // 클라이언트와의 접속 소켓
  private Socket connectionSocket;

  /**
   * <pre>
   * 기본 생성자
   * </pre>
   * 
   * @param connectionSocket 클라이언트와의 통신을 위한 소켓
   */
  public ServerThread(Socket connectionSocket)
  {
    this.connectionSocket = connectionSocket;
  }

  /* (non-Javadoc)
   * @see java.lang.Thread#run()
   */
  @Override
  public void run()
  {
    System.out.println("WebServer Thread Created");
    BufferedReader inFromClient = null;
    DataOutputStream outToClient = null;

    try
    {
      // 클라이언트와 통신을 위한 입/출력 2개의 스트림을 생성한다.
      inFromClient = new BufferedReader(

            new InputStreamReader(connectionSocket.getInputStream()));
      outToClient = new DataOutputStream(
            connectionSocket.getOutputStream());

      // 클라이언트로의 메시지중 첫번째 줄을 읽어들인다.
      String requestMessageLine = inFromClient.readLine();

      // 파싱을 위한 토큰을 생성한다.
      StringTokenizer tokenizedLine = new StringTokenizer(
            requestMessageLine);

      // 첫번째 토큰이 GET으로 시작하는가? ex) GET /green.jpg
      if(tokenizedLine.nextToken().equals("GET"))
      {
        // 다음의 토큰은 파일명이다.
        String fileName = tokenizedLine.nextToken();

        // 기본적으로 루트(/)로부터 주소가 시작하므로 제거한다.
        if(fileName.startsWith("/") == true)
        {
          if(fileName.length() > 1)
          {
            fileName = fileName.substring(1);
          }
          // 파일명을 따로 입력하지 않았을 경우 기본 파일을 출력한다.
          else
          {
            fileName = DEFAULT_FILE_PATH;
          }
        }

        File file = new File(fileName);

        // 요청한 파일이 존재하는가?
        if(file.exists())
        {
          // 존재하는 파일의 MIME타입을 분석한다.
          String mimeType = new MimetypesFileTypeMap()
            .getContentType(file);

          // 파일의 바이트수를 찾아온다.
          int numOfBytes = (int) file.length();

          // 파일을 스트림을 읽어들일 준비를 한다.
          FileInputStream inFile = new FileInputStream(fileName);
          byte[] fileInBytes = new byte[numOfBytes];
          inFile.read(fileInBytes);

          // 정상적으로 처리가 되었음을 나타내는 200 코드를 출력한다.
          outToClient.writeBytes("HTTP/1.0 200 Document Follows \r\n");
          outToClient.writeBytes("Content-Type: " + mimeType + "\r\n");

          // 출력할 컨텐츠의 길이를 출력
          outToClient.writeBytes("Content-Length: " + numOfBytes + "\r\n");
          outToClient.writeBytes("\r\n");

          // 요청 파일을 출력한다.
          outToClient.write(fileInBytes, 0, numOfBytes);
        }
        else
        {
          // 파일이 존재하지 않는다는 에러인 404 에러를 출력하고 접속을 종료한다.
          System.out.println("Requested File Not Found : " + fileName);

          outToClient.writeBytes("HTTP/1.0 404 Not Found \r\n");
          outToClient.writeBytes("Connection: close\r\n");
          outToClient.writeBytes("\r\n");
        }
      }
      else
      {
        // 잘못된 요청임을 나타내는 400 에러를 출력하고 접속을 종료한다.
        System.out.println("Bad Request");

        outToClient.writeBytes("HTTP/1.0 400 Bad Request Message \r\n");
        outToClient.writeBytes("Connection: close\r\n");
        outToClient.writeBytes("\r\n");
      }

      connectionSocket.close();
      System.out.println("Connection Closed");
    }
    // 예외 처리
    catch(IOException ioe)
    {
      ioe.printStackTrace();
    }
  }
}

* index.html

<html>
<head>
<title>웹서버 테스트</title>
</head>
<body>
<p>http://theeye.pe.kr</p>
<img src="sooji.jpg" />
</body>
</html>

 

사용자 삽입 이미지

테스트를 해보니 정상적으로 파일을 전송하고 해당 html에 딸려있는 객체들역시 정상적으로 전송됨을 알 수 있습니다. 잘 되네요~^^b

[샘플코드 다운로드]

[Android] 화면 회전시에 Activity onCreate() 방지하기

안드로이드는 참으로 신기한점이 많습니다. 개발자 편의를 봐주기 위한 노력이 군데군데 묻어나는 OS입니다. 그리고 그것을 이동통신사에서 커스터마이징하면서 자신들의 철학대로 바꾸곤 합니다. 구글에서 어느정도의 가이드라인을 잡아서 어느정도 이상은 커스터마이징을 할 수 없도록 하면 어떨까요?

다음의 경우는 이통사탓은 아니고-_-a 안드로이드의 이상한 철학쯤으로 보여지는 부분입니다. 정확히는 개발을 위한 편의를 생각했던 것이겠죠. 화면 회전시에 현재 보여지는 액티비티를 재생성 해버립니다. 아이폰의 경우 단순히 화면 회전만을 했다면 안드로이드는 액티비티를 제거후에 회전된 방향에 맞게 액티비티를 다시 생성하는군요.

기본적으로 상태값을 저장하도록 되어있어서 대부분의 위젯들은 본래의 상태값으로 복구가 됩니다. 하지만 그 과정이 매끄럽지 못하고 값을 잃어버리는 경우도 생깁니다.

[code]public class PreventLossActivity extends Activity
{
    private EditText mEditText;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mEditText = (EditText) findViewById(R.id.editText);

        Toast.makeText(this, “onCreate()”, Toast.LENGTH_SHORT).show();
    }
}[/code]

사용자 삽입 이미지

기본적으로 위와같은 코드를 작성하고 화면을 회전시켜보면 위와같이 onCreate()가 또 호출되는것을 알 수 있습니다. onCreate()의 모든 코드들이 재실행 된다는 것을 의미합니다.

[code]<activity
    android:name=”.PreventLossActivity”
    android:label=”@string/app_name”
    android:configChanges=”keyboardHidden|orientation”>[/code]

AndroidManifest.xml에서 위와 같이 configChanges설정을 해줍니다. 화면이 회전하거나 하드웨어 키보드를 닫을때에 해당 관련된 처리를 자동으로 처리하지 않고 액티비티 자체에서 알아서 하겠다는것을 알려주는 설정입니다.

실제로 위의 두가지 이벤트가 발생할 때 onConfigurationChanged()가 호출됩니다. 하지만 위의 예제 소스에서는 이 메서드를 구현하지 않았고 결과적으로 아무런 일이 일어나지 않습니다.

사용자 삽입 이미지

1370370599.zip