자바 소켓 프로그래밍(Network Socket Programming)

네트워크에서는 크게 두가지 프로토콜이 있습니다. TCP와 UDP가 그것들이죠. TCP는 연결형에 신뢰성을 바탕을 둔 전송 프로토콜이고, UDP는 비연결성의 비신뢰성의 프로토콜입니다. 여기서 왜 TCP가 신뢰성이 있냐면 패킷 유실시에 다시 재전송을 하기 때문이죠. UDP는 그런게 없이 잃어버리든 말든 재전송따위는 없이 보냅니다. 

여기서는 자바를 통해 TCP를 이용해 서버와 통신하는 프로그램을 구현해보도록 하겠습니다. TCP를 통해서 프로그래밍을 할땐 Socket에 관한 이해가 필요한데, 아래의 링크를 통해서 개념을 잡고 오세요. 설명은 리눅스 C 소켓 프로그래밍을 설명하지만 기본 개념은 같습니다.

reakwon.tistory.com/81

 

[리눅스] 소켓(socket) 개념과 예제(connect, bind, listen, accept,send,recv 사용)

소켓(socket) 네트워크 통신을 하는 표준 방법으로 프로세스간 연결의 종점이라고 볼 수 있습니다. 기본적인 개념은 아래의 그림과 같습니다. 위의 그림은 TCP/IP에서의 인터넷 통신을 보여줍니다.

reakwon.tistory.com

 

서버-클라이언트 모델을 사용하기 때문에 서버용 프로그램과 클라이언트용 프로그램 두가지 메인 프로그램이 필요합니다.

Client는 GUI로 화면에 출력하고 서버는 Console 출력화면을 사용하도록 하겠습니다. 

 

서버 구현

Socket에 대한 개념을 충분히 이해하셨다면 이제 코드만 이해하면 됩니다. 아니, 방법만 알면 됩니다. 서버쪽에는 ServerSocket이라는 클래스를 이용하여 이것으로 클라이언트 연결이 올때까지 대기합니다. accept() 메소드가 바로 클라이언트 연결을 대기하는 메소드이고 연결이 성립될때까지 대기(컴퓨터 과학에서는 Block 된다고합니다.)합니다. accept는 연결이 되면 실제 통신하기 위해 Socket 객체를 넘겨주며 넘겨받은 Socket으로 실제 통신을 한다고 생각하시면 됩니다. 

그러니까 정리하자면 ServerSocket(개인적으로 부모 Socket이라 부릅니다.)은 Client를 받기 위한 소켓이고, 실제 데이터 송수신하는 소켓은 ServerSocket이 accept()하고 넘겨준 Socket(개인적으로 세끼 Socket이라고 합니다.)이라는 것이죠. 

아래의 코드는 포트번호 9999를 사용하며, 연결이 오면 소켓의 정보를 출력해주고 클라이언트에게 "Hello!! From Server"라는 메시지를 보낸 후 통신을 끊는 아주 간단한 서버의 코드입니다.

 

- TCPServer.java

public class TCPServer {
	
	public final static int SERVER_PORT=9999;
	public static void main(String[] ar) {
		ServerSocket ss=null;
		try {
			ss=new ServerSocket(SERVER_PORT);
			
		}catch(IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println("Waiting connection...");
				Socket s=ss.accept();		//새끼 Socket 넘겨줌
				System.out.println("[ Connection Info ]");
				System.out.println("client address:"+s.getInetAddress());	//클라이언트 IP주소
				System.out.println("client port:"+s.getPort());			//클라이언트 포트 번호
				System.out.println("my port:"+s.getLocalPort());		//나(Server, Local)의 포트
				
				PrintWriter pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()));
				pw.println("Hello!! From server");
			
				pw.close();
				s.close();
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
	}
}

 

클라이언트 구현

GUI 설정때문에 코드가 길어보이는 것뿐이니 사실 별거없습니다. 간단하게 클라이언트는 Swing 컴포넌트로 JTextArea에 Socket의 정보와 서버로부터 온 메시지를 출력해줍니다.  .정말 별거없죠? 

public class TCPClient extends JFrame{
	public final static int SERVER_PORT=9999;
	
	private Socket s;
	private JTextField messageField;
	private JTextArea messages;
	
	public TCPClient() {
		super(" TCP Client");
		messageField=new JTextField(40);
		messages=new JTextArea(10,50);
		
		messages.setBackground(Color.black);		//배경 검은색
		JScrollPane scrolledMessages=new JScrollPane(messages);	//스크롤 가능하도록
		
		this.setLayout(new BorderLayout(10,10));
		this.add("North",messageField);
		this.add("Center",scrolledMessages);
		
		messages.setEnabled(false);		//TextArea disactive
		
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(620,400);
		this.setLocationRelativeTo(null);	//창 가운데 위치
		this.setVisible(true);
		
		
		connectServer();
	}
	
	public void connectServer() {
		String serverIP="127.0.0.1";
		
		try {
			Socket s=new Socket(serverIP,SERVER_PORT);
			messages.append("server port:"+s.getPort()+", my port:"+s.getLocalPort()+"\n");
			BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
			messages.append(br.readLine());
			
			br.close();
			s.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public static void main(String []ar) {
		new TCPClient();	
	}
}

 

위의 IP주소는 127.0.0.1인 이유는 저의 주소를 그대로 사용하게 만들도록 하기 위함입니다. 클라이언트는 포트번호 9999로 서버 소켓에 연결합니다. 아래는 서버와 클라이언트를 실행한 화면입니다. 

서버부터 실행하고 다음 클라이언트를 실행해야합니다. 서버와 클라이언트의 소켓정보가 일치됨을 확인할 수 있고 클라이언트는 서버로부터의 메시지를 잘 받아왔네요.

 

 

에코 서버-클라이언트 구현

클라이언트의 메시지를 그대로 돌려주는 서버를 우리는 에코 서버(Echo Server)라고 합니다. 그것을 구현해볼껀데요. 여기서 서버는 ServerSocket으로 새로운 통신을 확립시키면 새끼 Socket을 새로운 통신 쓰레드에 넘겨주며 작업을 처리하게 할것이고 다음 연결을 대기하게 만듭니다. 쓰레드와 소켓이 보통 같이 구현됩니다.

서버 구현


public class TCPServer {
	
	public final static int SERVER_PORT=9999;
	public static void main(String[] ar) {
		ServerSocket ss=null;
		try {
			ss=new ServerSocket(SERVER_PORT);
			
		}catch(IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println("Waiting connection...");
				Socket s=ss.accept();		//새끼 Socket 넘겨줌
				
				new ServerThread(s).start();
				
			}catch(IOException e) {
				e.printStackTrace();
			}
		}
	}
}

class ServerThread extends Thread{
	
	private Socket s;
	private BufferedReader br;
	private PrintWriter pw;
	public ServerThread(Socket s) {
		this.s=s;
		try {
			br=new BufferedReader(new InputStreamReader(s.getInputStream()));	//Socket으로 Read용 Stream
			pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()));	//Socket으로 Write용 Stream
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void run() {
		
		while(true) {
			String received;
			try {
				received = br.readLine();	//1. 받고
				System.out.println("server received :"+received);
				if(received==null||received.equals("quit")) {	//quit 또는 q가 오면 종료
					if(br!=null) br.close();
					if(pw!=null) pw.close();
					if(s!=null) s.close();
					return;
				}
				
				pw.println("Server Got Your Message : "+received);	//2. 보냄
				pw.flush();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

 

ServerThread의 생성자에서는 소켓을 넘겨받고 사용할 인풋,아웃풋 스트림을 생성합니다. 그리고 run() 메소드에서 이런작업을 진행하지요. 

 

1. 우선 클라이언트로부터 메시지를 읽어옵니다. 이때 BufferedReader를 사용하죠.

2. 메시지를 검사하고 다시 클라이언트로 서버의 메시지를 첨가해 다시 넘겨줍니다. 이때 PrintWriter를 사용합니다. 여기서 한가지 하는 실수가 뭐냐면 PrintWriter는 내부적으로 버퍼를 사용하기 때문에 버퍼가 다 차기전까지 내용을 출력하지 않습니다. 그래서 강제로 비워줘야하는데, 그 메소드가 flush()입니다. 이전의 맨 처음 코드에서 flush()를 하지 않은 이유는 PrintWriter의 close()메소드가 버퍼를 비우고 스트림을 닫기 때문에 굳이 flush()를 호출할 필요가 없었죠. flush()는 변기물을 내리는 것처럼 버퍼를 흘려보내서 비워준다는 뜻입니다.

 

혹은 이렇게 이렇게 꼭 flush()를 하기 귀찮고 자동으로 flush()해주기를 원한다면 아래의 생성자를 사용하세요.

pw=new PrintWriter(new OutputStreamWriter(s.getOutputStream()),true);
PrintWriter(OutputStream out, boolean autoFlush)
Creates a new PrintWriter from an existing OutputStream.

두번째 인자는 auto flush를 하느냐 마냐를 정해줍니다. true면 auto flush활성화하는것입니다.

 

클라이언트는 이와 반대의 순서로 메시지를 보내고, 읽어야겠죠? 

클라리언트 구현

public class TCPClient extends JFrame implements KeyListener{
	//...생략...//
	@Override
	public void keyTyped(KeyEvent e) {}

	@Override
	public void keyPressed(KeyEvent e) {}

	@Override
	public void keyReleased(KeyEvent e) {
		int keyCode = e.getKeyCode();
		if(keyCode==KeyEvent.VK_ENTER) {
			try {
				if(br==null||pw==null) return;
				String msgText=messageField.getText();
				if(msgText==null||msgText.length()==0) return;
				
				if(msgText.equals("quit")) {	//종료
					if(br!=null) br.close();
					if(pw!=null) pw.close();
					if(s!=null) s.close();
					return;
				}
				
				
				pw.println(msgText);	//1. 보내고
				pw.flush();
				messages.append(br.readLine()+"\n");	//2. 받음
				
				//뒷정리
				messageField.setText("");
				messageField.requestFocus();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}	
	}

 

나머지 불필요한 코드는 넣지 않았습니다. 사실 별로 어려운 소스코드는 아닙니다. 단지 소켓으로 인풋, 아웃풋 스트림을 생성한 후에 println()과 flush()로 문자열을 전송한 후에 다시 에코된 메시지를 readLine으로 읽는 겁니다.

 

이제 결과를 보시면 아래와 같이 잘 echo됨을 확인할 수 있습니다.

 

 

여기까지 Socket을 생성하여 연결하는 방법과 네트워크 전송에 사용하는 reader인 BufferedReader, writer인 PrintWriter에 대한 짤막한 사용법도 알아보았습니다.

 

이런 개념을 바탕으로 여러분들이 응용하기 나름이겠지만 채팅 어플리케이션도 만들 수 있고, 심지어 패인트 객체의 정보를 네트워크상으로 주고 받음으로써 캐치마인드까지도 구현해볼 수도 있습니다. 컴퓨터 프로그램에서 네트워크는 절대 빠질수 없는 개념이니 자바로 꼭 익혀두시길 바랍니다.

 

뭐, 시간되면 채팅 어플리케이션 만드는 포스팅도 진행해보도록 할게요. 그럴 시간이 나오려나...

반응형
블로그 이미지

REAKWON

와나진짜

,

스트림

자바에서도 여러 입출력을 지원하지만 이번에 우리의 관심사는 바로 자바에서 제공하는 파일 입출력입니다.


그 전 우리는 스트림에 대한 이야기를 잠깐 간략하게 하고 넘어가겠습니다. 우선 파일에서 입력과 출력이라는 동작을 하려면 파일로 데이터를 전달하거나 파일로부터 전달 받는 길을 열어주어야합니다.


그러한 길을 스트림이라고 하지요.





파일로부터 입력을 받는 스트림을 입력스트림, 출력을 보내는 스트림을 출력스트림이라고 합니다.

그리고 바이너리 형태로 데이터를 입출력하는 스트림을 이진스트림, 문자형태로 입출력하는 스트림을 텍스트스트림이라고 합니다.




스트림을 알았으니 파일입출력을 알아보도록 합시다.


FileReader와 FileWriter


import java.io.*; public class Main { public static void main(String[] args) throws IOException{ File file=new File("test.txt"); if(!file.exists()) file.createNewFile(); FileWriter fw=new FileWriter(file); char []buf= {'m','e','s','s','a','g','e','\r','\n'}; for(int i=0;i<buf.length;i++) fw.write(buf[i]); fw.close(); FileReader fr=new FileReader(file); int EOF=-1; int c; while((c=fr.read())!=EOF) { System.out.print((char)c); } fr.close(); } }


이 코드는 test.txt파일에 "message"라는 문자열을 기록하고 읽어오는 프로그램이에요. 한줄 한줄씩 보도록 하지요.


우선 File 입출력시에는 IOException이 발생할 수 있고 처리가 귀찮으니 저 멀리 보내버리도록 합시다. 가버렷!


일단 file을 열어줘야겠지요? 절대 경로로 지정하지 않는다면 현재 프로젝트 디렉토리에 파일을 엽니다.


하지만 그 파일이 없다면 새로 생성합니다. 

이것을 다음의 라인이 나타냅니다.


File file=new File("test.txt"); if(!file.exists()) file.createNewFile();


그 후 파일에 "message"라는 문자열을 기록합니다. 그때 사용하는 클래스가 바로 FileWriter라는 클래스이지요.

FileWriter의 메소드 write를 통해서 char 배열의 문자열을 하나씩 기록한 후 스트림을 닫습니다.




닫아주어야 파일에 문자열이 입력이 됩니다! 파일을 닫지 않고 파일에 입력하려면 그렇지 않으면 flush함수를 사용하세요. 


FileWriter fw=new FileWriter(file);
char []buf= {'m','e','s','s','a','g','e','\r','\n'};
for(int i=0;i<buf.length;i++)
	fw.write(buf[i]);
fw.close();

이후 한문자씩 읽어오는데 그 역할을 수행하는 클래스가 바로 FileReader입니다. 파일의 끝은 int형의 -1입니다. 그래서 -1을 만날때까지 한문자 한문자 출력합니다.

int형태로 읽어왔으니 char로 바꿔줘야겠지요?


FileReader fr=new FileReader(file);
int EOF=-1;
int c;
while((c=fr.read())!=EOF) {
	System.out.print((char)c);
}
fr.close();


이제 수행을 해보도록 할게요. 어떤 변화가 있는지..


test.txt라는 파일이 생겼네요! 



이것을 까보면!




그안에 우리가 집어넣은 문자열이 존재하는 군요.


eclipse상의 결과 역시 "message"라는 문자열을 출력하는 군요.


파일에 기록한 문자열을 자바프로그램이 읽어온 것이랍니다.



한글자 한글자 읽어오는 것이 여간 불편한 것이 아니죠?

문자열을 사용해서 쓰는 것이 훨씬 더 편할텐데요.

그리고 읽어올때도 배열을 써서 읽어오는 것이 훨씬 편하구요.




그런 방법이 아래에 나와있습니다.




import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		FileWriter fw=new FileWriter(file);
		fw.write("Hello, world!!\r\n");
		
	
		fw.close();
		
		FileReader fr=new FileReader(file);
		
		while(true) {
			char []buf=new char[4];
			int ret=fr.read(buf);
			if(ret==-1) break;
			System.out.print(String.valueOf(buf));
		}
		fr.close();
	}
}


FileWriter는 String을 받는 write메소드가 있어서 문자열로 그대로 파일에 기록할 수 있습니다.


중요한건 FileReader인데요. 우선 buf라는 char형 4개에 문자열을 계속 입력받는 거지요. 만약 파일에서 더 읽어올 것이 없다면 -1을 리턴하게 되니까 그 반환형이 -1이면 while루프를 탈출하면 됩니다.


이제 실행 후 파일을 확인해보고 이클립스에서도 확인해 봅시다.





파일에 제대로 적혀있고 이클립스 출력도 이 문자열이 나오는 것을 확인할 수 있죠?



Line 단위 입출력


아직도 불편하긴 합니다. /r/n을 통해서 개행하는 것도 별로구요. 보통 문자 입출력시에는 라인 단위로 입출력을 하기 때문에 라인별로 입력을 할 수 있었으면 좋겠습니다.


그래서 나온것이 버퍼단위의 입출력을 담당하고 있는 BufferedReader, BufferedWriter입니다.



import java.io.*;


public class Main {
	public static void main(String[] args) throws IOException{
		File file=new File("test.txt");
		if(!file.exists())
			file.createNewFile();
		
		BufferedWriter bw=new BufferedWriter(new FileWriter(file));
		
		bw.write("Hello, world!");
		bw.newLine();
		bw.write("Hello, world!!");
		bw.newLine();
		bw.write("Hello, world!!!");
		bw.newLine();
		bw.close();
		
		BufferedReader br=new BufferedReader(new FileReader(file));
		String line=null;
		while((line=br.readLine())!=null)
			System.out.println(line);
			
		br.close();
	}
}


BufferedWriter와 BufferedReader는 버퍼의 사이즈를 지정할 수도 있습니다. 그렇지 않으면 Default 사이즈로 버퍼에 담습니다.


BufferedWriter의 newLine메소드는 개행을 말합니다. 쉽죠? 별거 없어요.


여기서는 Hello, world!를 3라인에 걸쳐 출력합니다. (느낌표 갯수만 다르고요)


이제 BufferedReader를 통해서 읽어옵니다. 

readLine은 String 형태의 문자열을 반환하는데, 만약 더이상 출력할 문자열이 없으면 null을 반환하죠.


그래서 null을 만나게 되면 while루프를 탈출합니다.


이제 실행후 파일과 이클립스화면을 보면 둘의 결과는 같다는 것을 알 수 있습니다.


test.txt




이클립스 실행결과


Hello, world!

Hello, world!!

Hello, world!!!



어떻습니까?


BufferedReader와 BufferedWriter 정말 편리하죠?

이 두 클래스는 조금 빈번하게 쓰입니다. 기억해두세요.


아직 파일입출력에 대해서는 더 할 이야기가 나왔습니다. 나중에 더 이야기 해보도록 하지요.

반응형
블로그 이미지

REAKWON

와나진짜

,