본문 바로가기

Project

[java] Port Scanner

간단한 포트 스캐너를 JAVA로 만들어보았습니다! 

사용자가 지정한 특정 IP에 대해 포트를 스캔하는 프로그램을 포트 스캐너, 혹은 포트 모니터라고도 합니다. 

포트 스캐너 프로젝트에 대해 간단히 설명하고, 그 다음 GUI 구현, 코드에 대한 설명을 포스팅하려고 합니다. 

아래에 나오는 포트 스캐너 구현 코드는 제 깃헙에서도 보실 수 있습니다. 깃헙 url은 아래에 첨부하도록 하겠습니다. 

 

이 프로젝트 뿐만 아니라 제가 한 모든 작업을 보고 싶으시면 이 글 하단이 아니라 홈에서 블로그 하단에 Git Hub 로고를 클릭하시면 제 깃헙 주소로 넘어갑니다.

 


프로젝트 설명

위에서도 말했지만, 이 프로젝트는 포트 스캐너 또는 포트 모니터라고도 불리는 프로그램을 구현한 것입니다.

사용자가 지정한 특정 IP에 대해 포트가 열려있는지 확인하는 프로그램입니다. 주로 게임 서버 프로그래머들이 서버가 제대로 열려있는지 확인하려고 사용한다고 합니다. 저는 다 만들고나서야 알았는데 포트 스캐너 프로그램을 다운로드해서 쓸 수가 있더라고요....

 

제가 만!든! 포트 스캐너에 대해 설명하자면,

사용자가 특정 프로토콜과 IP 주소, 갱신 주기를 입력하고 ‘START’버튼을 누르면 이에 맞게 입력 받은 IP에 대해 열려있는 포트를 모니터링하고 포트 번호, 포트의 현재 상태, 활성화 시간, 마지막 확인 시간을 GUI 상에 테이블로 출력하여 보여줍니다. 

갱신 주기를 설정하면 갱신 주기마다 포트를 다시 스캔합니다.

사용자가 스캔한 포트에 대한 정보를 csv 파일로 저장하고 싶으면 .csv로 테이블에 출력된 정보들을 저장할 수 있도록 구현했습니다.

 


GUI 실행 화면

실행 전 화면입니다!

위에 프로토콜 선택 콤보박스 설명에도 써 놓았지만, UDP 프로토콜을 아직 배우지 않은 상태에서 java로 코딩한거라, 선택지에 UDP 프로토콜이 없습니다. 나중에 네트워크 공부를 하고 UDP를 배우면 그 때 마저 구현해보고 싶네여

 

 

실행 중 화면입니다!

학교 ip로 스캐닝해보니 열려있는 포트가 80번밖에 없더라고요. 443번 포트도 열어놓는다는데 학교는 443번도 막아놨습니다.. 

 

 

실행 완료 화면입니다!

80번을 제외하고 모든 포트를 막아놓은 재미없는 학교 대신 프로그램 돌려보는 재미를 위해 포트가 많이 열려있는 ip로 스캐닝했습니다 :)  (저 ip는 아버지가 가지고 있는 사이트의 ip입니다)

TMI 이 프로젝트를 이틀 전에 완성했는데 이틀 전만 해도 csv 파일은 구분자가 ,(쉼표) 라는 것밖에 몰랐지만 오늘의 저는 R로 csv 파일을 읽고 쓸 수 있습니다! 하핫 나날이 발전하는 나 자신👍

 


코드 설명 

저는 클래스를 Window class, PortScanner class 이렇게 두 개 사용했습니다. 

Window 클래스는 GUI 구성을 위한 클래스입니다. PortScanner 클래스는 스레드를 사용해서 포트에 접속해, 열려있는지 확인하기 위한 클래스입니다. 

아래의 코드 설명은 생성자, 메소드 단위로 설명하도록 하겠습니다.

전체 코드가 보고 싶으시면 하단의 Github url을 눌러서 확인해주세요.

 

Window class

GUI 전체를 구성하기 위한 생성자입니다.

Window(){
		
	Instance = this;
		
	setTitle("Port Monitoring System");
	// 상, 중, 하 부분으로 나누고 각 부분에 부착할 패널 생성
    JPanel panel1 = new JPanel();
	JPanel panel2 = new JPanel();
	JPanel panel3 = new JPanel();


	// 패널1 (상 부분)에 들어갈 컴포넌트 생성
	String protocol[] = {"프로토콜 선택", "TCP"};
	String time[] = {"갱신주기 선택 (s)", "갱신 안함", "5", "10", "15", "20", "25", "30"};
	proto_combo = new JComboBox<String>(protocol);  // 프로토콜 선택을 위한 콤보 박스 
	time_combo = new JComboBox<String>(time);		// 갱신 주기 선택을 위한 콤보 박스
	inputIP = new JTextField(16);					// ip를 입력받기 위한 텍스트필드
	inputIP.setText("모니터링 하고자 하는 IP를 입력하세요");
	btn_start = new JButton("START");				// 포트 스캔 시작을 위한 버튼 생성
	btn_start.addActionListener(new StartAction());

	// panel1에 컴포넌트 부착
	panel1.add(proto_combo);
	panel1.add(inputIP);
	panel1.add(btn_start);
	panel1.add(time_combo);


	// panel2 (중 부분)에 들어갈 컴포넌트 생성
	column = new Vector<String>();	// table 맨 위 열에 들어갈 제목 저장
	column.add("Port 번호");
	column.add("현재 상태");
	column.add("활성화 시간");
	column.add("마지막 확인 시간");

	// 스캔 정보 출력을 위한 테이블 생성 
	tableview = new JTable();				
	model = new DefaultTableModel(0, 0);	
	model.setColumnIdentifiers(column);
	tableview.setModel(model);
	// 테이블에 스크롤 연결 
	scroll = new JScrollPane(tableview);
		
	// panel2에 컴포넌트 부착
	panel2.add(scroll, BorderLayout.CENTER);
		

	// panel3 (하 부분)에 들어갈 컴포넌트 생성
	btn_csv = new JButton("CSV 파일로 저장");	// csv 파일에 정보를 저장하기 위한 버튼
	text = new JLabel("마지막 업데이트 시간");	// 마지막 업데이트 시간 출력을 위한 텍스트필드
	updateTime = new JTextField(20);
	updateTime.setText("HH:mm:ss");
	updateTime.setEnabled(false);
	btn_csv.addActionListener(new WriteCSV());

	// panel3에 컴포넌트 부착
	panel3.add(btn_csv);
	panel3.add(text);
	panel3.add(updateTime);


	// JFrame에 패널 부착
	add(panel1, BorderLayout.NORTH);
	add(panel2, BorderLayout.CENTER);
	add(panel3, BorderLayout.SOUTH);
		
	setSize(600, 500);
	setVisible(true);
	setDefaultCloseOperation(EXIT_ON_CLOSE);

}

panel을 중심으로 GUI를 대략적으로 구상한 그림입니다.

위의 코드는 왼쪽과 같이 GUI를 구성하기 위한 코드입니다. 

 

 

 

 

 

 

 

 

 

'START' 버튼에 걸려있는 이벤트 리스너는 다음과 같습니다. 

START버튼을 누르면 스레드가 생성이 되고 PortScanner class의 생성자에 있는 코드를 실행하고 포트를 모니터링하기 시작합니다. 또한 STOP으로 바뀌고 ip입력을 못받도록 텍스트필드가 비활성화되고 콤보박스도 비활성화됩니다.

다시 버튼을 누르면 비활성화 됐던 컴포넌트들을 다시 활성화시키고 스레드 실행을 중지합니다. 

// START 버튼을 눌렀을 때 리스너
class StartAction implements ActionListener {

	private ArrayList<Thread> activeThread;

	public StartAction()
	{
		activeThread = new ArrayList<Thread>();
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
		
		// START 버튼을 누르면 STOP으로 바뀌고 ip 입력을 못받으며 콤보박스가 모두 비활성화
		if (btn_start.getText().equals("START"))
		{
			btn_start.setText("STOP");
			inputIP.setEnabled(false);
			proto_combo.setEnabled(false);
			time_combo.setEnabled(false);

			activeThread.clear();

			// START 버튼을 누르면 스레드 생성 후 포트 모니터링 
			Thread t = new Thread(new PortScanner(1, 9999));
			t.start();

			activeThread.add(t);
		}
		else {
			// STOP 버튼을 누르면 스레드 중지 
			for (Thread t : activeThread)
			{
				t.interrupt();
			}
			activeThread.clear();

			// STOP 버튼을 누르면 START 버튼으로 바뀌고 텍스트 필드, 콤보박스 활성화 
			btn_start.setText("START");
			inputIP.setEnabled(true);
			proto_combo.setEnabled(true);
			time_combo.setEnabled(true);
		}
	}
}

처음에 포트 스캐너를 만들 때 멀티 스레드로 구현해보았으나 에러 발생 원인을 찾기 어려웠고 멀티 스레드를 다루는 게 능숙하지 않아 멀티 스레드 구현을 위한 틀만 코딩하고 단일 스레드로 구현했습니다

 

 

'CSV파일로 저장' 버튼에 걸려있는 이벤트 리스너는 다음과 같습니다. 

버튼을 누르면 JTable에 있는 정보들을 csv 파일로 저장합니다. 그 후, 파일에 저장되었다는 팝업창을 띄웁니다. 

// CSV 파일로 저장 버튼을 눌렀을 때 리스너
class WriteCSV implements ActionListener {
	Writer writer = null;

	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
			
		// csv 파일로 포트 모니터링 정보 저장 
		try {
			writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Port Monitor.csv"), "EUC-KR"));
			StringBuffer bufferHeader = new StringBuffer();
			for(int i=0; i<tableview.getColumnCount(); i++){
				bufferHeader.append(tableview.getColumnName(i));
				if(i != tableview.getColumnCount())
					bufferHeader.append(",");
			}
			writer.write(bufferHeader.toString() + "\r\n");
			for(int j=0; j<tableview.getRowCount(); j++){
				StringBuffer buffer = new StringBuffer();
				for(int k=0; k<tableview.getColumnCount(); k++){
					buffer.append(tableview.getValueAt(j, k));
					if(k != tableview.getColumnCount())
						buffer.append(",");
				}
				writer.write(buffer.toString() +"\r\n");
			}
			writer.close();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		// 저장되었다는 팝업창 띄우기
		JOptionPane.showMessageDialog(null, "포트 모니터링 정보가 CSV 파일로 저장되었습니다.");
		}
}

 

PortScanner class

이 클래스에서는 포트를 모니터링하고 그 정보를 JTable에 쓰는 일을 합니다. 

PortScanner의 생성자는 포트 스캔할 시작 번호와 끝 번호를 받아서 멤버 변수에 저장하는 역할을 합니다. 

 

JTable에 포트 모니터링 정보를 쓰는 메소드 

포트 정보가 이미 존재하면 update를 하고, 없으면 쓰는 기능을 합니다. update할 때는 행 전체를 다시 update합니다. 

model은 JTable에 쓰기 위한 DefaultTableModel의 변수입니다. 

// 테이블에 포트 모니터링한 정보를 쓰는 메소드
private boolean addOrUpdateRow(Vector<String> data){
		
	String port = data.get(0);

	synchronized (Window.Instance.model)
	{
		// 포트 정보가 이미 테이블에 있으면 update
		for (int i = 0; i < Window.Instance.model.getRowCount(); i++)
		{
			if (Window.Instance.model.getValueAt(i, 0).equals(port) == false)
				continue;

			System.out.println("UPDATE ROW: " + port);
			for (int j = 1; j < data.size(); j++)
				Window.Instance.model.setValueAt(data.get(j), i, j);
			return false;
		}

		// 포트 정보가 테이블에 없으면 add 
		System.out.println("ADD ROW: " + port);
		Window.Instance.model.addRow(data);
	}
	return true;
}

 

 

스레드를 실행하고 포트 연결을 확인하는 메소드입니다. 

'START'버튼을 누르면 스레드를 실행하기 위해 start()를 실행하는데, 이 때 자동으로 run() 코드가 실행됩니다. run()에 있는 코드를 스레드가 실행하는데, run() 내에서 포트에 연결이 가능한지 확인하고 그 정보들(포트 번호, 현재 상태, 활성화 시간, 마지막 확인 시간)을 제네릭 컬렉션 Vector로 선언된 변수, info에 씁니다. 갱신 주기가 설정되어 있으면 갱신 주기만큼 sleep 했다가 다시 do-while문을 실행하고 갱신 주기가 0이거나 설정되어있지 않으면 do-while문을 종료(스캔 완료)하고 마지막 업데이트 시간을 쓰게 됩니다. 

아래 주석이 명확하게 표시되어있지 않아 덧붙이자면, 갱신 주기가 설정 되어있지 않을 경우 혹은 강제로 STOP 버튼을 눌러 종료 시켰을 경우, catch로 들어가 스레드가 종료되기 전에 버튼의 상태를 START로 돌려놓고 텍스트 필드와 콤보 박스 활성화합니다. 그리고 마지막으로 마지막 업데이트 시간을 updateTime 텍스트 필드에 출력 후 스레드를 종료합니다. 

// 스레드 실행을 위한 메소드
public void run()
{
	// TODO Auto-generated method stub
	String ip = Window.Instance.inputIP.getText();				// Window에서 입력받은 ip를 가져옴 

	Vector<String> info = null;									// 포트 스캔한 정보를 Vector에 저장  
	SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");	// 시간 저장을 위한 format

	try {
		do {
			for(int i = startPort; i < endPort; i++)
			{
				Date now = new Date();

				synchronized (Window.Instance.updateTime)
				{
					Window.Instance.updateTime.setText("SCANNING..." + i);	// 스캐닝 중일 떄 출력하는 텍스트 
				}

				// 해당되는 포트에 접속 가능한지 확인 
				try {
					Socket socket = new Socket();
					socket.connect(new InetSocketAddress(ip, i), 200);
					socket.close();
					if (Window.Instance.dataSet.containsKey(i))
						info = Window.Instance.dataSet.get(i);
					else
					{
						// info에 포트 번호, 현재 상태, 활성화 시간, 마지막 확인 시간에 대한 정보 저장 
						info = new Vector<String>();

						info.setSize(4);

						info.set(0, Integer.toString(i));
						info.set(2, format.format(now));

						Window.Instance.dataSet.put(i,  info);
					}
					info.set(1, "OPEN");
					info.set(3, format.format(now));
				}
				catch (IOException e)
				{
					if (Thread.currentThread().isInterrupted())
						return;

					if (Window.Instance.dataSet.containsKey(i) == false)
						continue;
					// 포트가 닫혀있을 때 정보 저장 
					else
					{
						info = Window.Instance.dataSet.get(i);
						info.set(1, "CLOSE");
						info.set(3, format.format(now));
					}
				}
				addOrUpdateRow(info);
			}

			Window.Instance.updateTime.setText(format.format(new Date()));	// 스캔을 완료하고 마지막 업데이트 시간 텍스트로 출력 
			// 갱신 주기 저장 
			int retryTime = time[Window.Instance.time_combo.getSelectedIndex()];

			if (retryTime == 0)
				throw new InterruptedException();

			// 갱신 주기가 설정되어 있으면 스레드 sleep 후 다시 스캔 
			try {
				Thread.sleep(retryTime * 1000);
			} catch (InterruptedException e) {
				return;
			}
		} while (true);
	} catch (Exception e)	// 스레드 종료 
	{
		System.out.println("STOP: " + e);
		synchronized (Window.Instance)
		{
			// 스레드 종료하면 자동으로 START 버튼으로 바뀌고 텍스트 필드와 콤보박스 활성화 
			Window.Instance.btn_start.setText("START");
			Window.Instance.inputIP.setEnabled(true);
			Window.Instance.proto_combo.setEnabled(true);
			Window.Instance.time_combo.setEnabled(true);
		}
	}
	finally
	{
		synchronized (Window.Instance.updateTime)
		{
			Window.Instance.updateTime.setText(format.format(new Date()));	// 스레드 종료 시 마지막 업데이트 시간 텍스트로 출력 
		}
	}
}

 


 

전체 코드는 아래 url로 들어가시면 보실 수 있습니다! 

 

DayeonKang99/Port-Scanner

implement port scanner using JAVA. Contribute to DayeonKang99/Port-Scanner development by creating an account on GitHub.

github.com

 


 

거의 C 언어로만 코딩하다가 java로 코딩해서 처음에는 적응이 잘 안됐는데 하다보니까 점점 익숙해지더라구요! 조금이나마 능숙해진 언어가 하나 더 늘어난 것 같았습니다!! 진짜 처음에는 다른 클래스를 어떻게 갖다 써야하는지 감도 잘 안잡혔었는데 확실히 프로젝트 한 번 하니까 실력이 느네여 

포트 스캐너 만들면서 너무 아쉬웠던 건 정말 기초, 완전 기초의 네트워크만 배우고 만들다보니까 UDP 프로토콜 구현을 못했다는 점과 멀티 스레드로 구현을 못했다는 점이었습니다. START 버튼에 걸려있는 이벤트 리스너랑 run() 메소드를 보시면 아시겠지만 진짜 멀티 스레드로 구현을 하고자 한 흔적이 보이시져... 멀티 스레드로 구현해서 코딩했는데 Vector에 포트 정보가 안담기고 JTable에 쓰는 게 안되서 시간에 쫓겨 멀티 스레드를 포기했습니다.... 멀티 스레드랑 단일 스레드에 큰 차이도 없을텐데 단일 스레드로 하니까 조금 쉽게 느껴지는 건 왜 일까요🤔

아무튼 저는 'JAVA프로그래밍및실습' 강의를 들으면서 배운 모든 걸 적용해 볼 수 있었던 면에서 이 프로젝트에 너무 만족합니다 :)