14일차 Java [ 네트워크 입출력 ]
[ 네트워크 기초 ]
네트워크 ( network )는 여러 컴퓨터들을 통신 회선으로 연결한 것을 말한다. LAN ( Local Area Network )은 가정, 회사, 건물, 특정 영역에 존재하는 컴퓨터를 연결한 것이고, WAN ( Wide Area Network ) 은 LAN을 연결한 것이다. WAN이 우리가 흔히 말하는 인터넷 (Internet )이다.
※ 서버와 클라이언트 : 네트워크에서 유무선으로 컴퓨터가 연결되어 있다면 실제로 데이터를 주고 받는 행위는 프로그램들이 한다. 서비스를 제공하는 프로그램을 일반적으로 서버 ( server ) 라고 부르고, 서비스를 요청하는 프로그램을 클라이언트 ( client ) 라고 부른다. 인터넷에서 두 프로그램이 통신하기 위해서는 먼저 클라이언트가 서비스를 요청하고, 서버는 처리 결과를 응답으로 제공해준다.
※ IP 주소 : 네트워크 상에서 유일하게 식별될 수 있는 컴퓨터 주소 숫자로 구성된 주소 4개의 숫자가 ‘.’으로 연결.
우리들의 집에는 고유한 주소가 있기 때문에 우편물이나 택배물이 정확하게 우리들의 집을 찾아간다. 컴퓨터도 고유한 주소가 있다. 바로 IP ( Internet Protocol ) 주소이다. IP 주소는 네트워크 어댑터 (LAN 카드) 마다 할당된다. 만약 컴퓨터에 두 개의 네트워크 어댑터가 장착되어 있다면, 두개의 IP 주소를 할당받을 수 있다. 네트워크 어댑터에 어떤 IP 주소가 부여되어 있는지 확인하려면 윈도우에서는 ipconfig 명령어를, 맥 OS에서는 ifconfig 명령어를 실행하면 된다. 다음은 윈도우 명령 프롬프트 ( cmd ) 에서 ipconfig 명령어를 실행한 결과로, IP 주소는 xxx.xxx.xxx.xxx와 같은 형식으로 출력된다. 여기서 xxx는 부호 없는 0~255 사이의 정수이다. 연결할 상대방 컴퓨터의 IP 주소를 모르면 프로그램들은 서로 통신할 수 없다. 우리가 전화번호를 모르면 114에 문의하듯이 프로그램은 DNS ( Domain Name System )를 이용해서 컴퓨터의 IP 주소를 검색한다. 여기에서 DNS는 도메인 이름으로, IP를 등록하는 저장소이다. 대중에게 서비스를 제공하는 대부분의 컴퓨터는 다음과 같이 도메인 이름으로 IP를 DNS에 미리 등록해 놓는다.
숫자로 된 주소는 기억하기 어려우므로 www.naver.com과 같은 문자열로 구성된 도메인 이름으로 바꿔 사용. DNS(Domain Name System) 문자열로 구성된 도메인 이름을 숫자로 구성된 IP 주소로 자동 변환!!!
도메인 이름 : IP 주소
---------------------------------------------------
www.naver.com : 222.122.195.5
웹 브라우저는 웹 서버와 통신하는 클라이언트로, 사용자가 입력한 도메인 이름으로 DNS에서 IP 주소를 검색해 찾은 다음 웹 서버와 연결해서 웹 페이지를 받는다.
※ Port 번호 : 한 대의 컴퓨터에는 다양한 서버 프로그램들이 실행 될 수 있다. 예를 들어 웹( Web ) 서버, 데이터베이스 관리 시스템(DBMS), FTP 서버 등이 하나의 IP 주소를 갖는 컴퓨터에서 동시에 실행 될 수 있다. 이 경우 클라이언트는 어떤 서버와 통신해야 할지 결정해야 한다. IP는 컴퓨터의 네트워크 어댑어까지만 갈 수 있는 정보이기 때문에, 컴퓨터 내부에서 실행하는 서버를 선택하기 위해서는 추가적인 Port 번호가 필요하다. Port는 운영체제가 관리하는 서버 프로그램의 연결 번호이다. 서버는 시작할 때 특정 Port 번호에 바인딩한다. 예를 들어 웹 서버는 80번으로, DBMS는 1521번으로 바인딩할 수 있다. 따라서 클라이언트가 웹 서버와 통신하려면 80번으로, DBMS와 통신하려면 1521번으로 요청해야 한다. 클라이언트도 서버에서 보낸 정보를 받기 위해서는 Port 번호가 필요한데, 서버와 같이 고정적인 Port 번호에 바인딩 하는 것이 아니라 운영체제가 자동으로 부여하는 번호를 사용한다. 이 번호는 클라이언트가 서버로 요청할 때 함께 전송되어 서버가 클라이언트로 데이터를 보낼 때 사용된다. 프로그램에서 사용할 수 있는 전체 Port 번호의 범위는 0~65535로, 다음과 같이 사용 목적에 따라 세가지 범위를 가진다.
[ IP 주소 읽기 ]
자바는 IP 주소를 java.net 패키지의 InetAddress로 표현한다. InetAddress를 이용하면 로컬 컴퓨터의 IP 주소를 얻을 수 있고, 도메인 이름으로 DNS에서 검색한 후 IP 주소를 가져올 수도 있다. InetAddress.getLocalHost() 메소드를 다음과 같이 호출하면 된다.
InetAddress ia = InetAddress.getLocalHost();
[ TCP 네트워킹 ]
IP 주소로 프로그램들이 통신할 때는 약속된 데이터 전송 규약기 있다. 이것을 전송용 프로토콜 ( protocol )이라고 부른다. 인터넷에서 전송용 프로토콜은 TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)가 있다.
TCP는 연결형 프로토콜로, 상대방이 연결된 상태에서 데이터를 주고 받는다. 클라이언트가 연결 요청을 하고 서버가 연결을 수락하면 통신 회선이 고정되고, 데이터는 고정 회선을 통해 전달된다. 그렇기 때문에 TCP는 보낸 데이터가 순서대로 전달되며 손실이 발생하지 않는다. TCP는 IP와 함께 사용하기 떄문에 TCP/IP라고도 한다. TCP는 웹 브라우저가 웹 서버에 연결할 때 사용되며 이메일 전송, 파일 전송, DB 연동에도 사용된다.
자바는 TCP 네트워킹을 위해 java.net 패키지에서 ServerSocket과 Socket 클래스를 제공하고 있다. ServerSocket은 클라이언트의 연결을 수락하는 서버 쪽 클래스이고, Socket은 클라이언트에서 연결 요청할 때와 클라이언트와 서버 양쪽에서 데이터를 주고 받을 때 사용되는 클래스이다. ( Socket은 데이터를 전달하는 Input Output과 같이 사용 됨 )
ServerSocket을 생성할 때는 바인딩할 Port 번호를 지정해야 한다. 서버가 실행되면 클라이언트는 Soket을 이용해서 서버의 IP 주소와 Port 번호로 연결 요청을 할 수 있다. ServerSocket은 accept() 메서드로 연결 수락을 하고 통신용 Socket을 생성한다. 크러고 나서 클라이언트와 서버느 양쪽 Socket을 이용해서 데이터를 주고 받게 된다.
소켓은 TCP/IP 네트워크를 이용하여 쉽게 통신 프로그램을 작성하도록 지원 하는 기반 기술. 두 응용프로그램 간의 양방향 통신 링크의 한쪽 끝 단. 소켓끼리 데이터를 주고받음. 소켓은 특정 IP 포트 번호와 결합 자바로 소켓 통신할 수 있는 라이브러리 지원. 소켓 종류 : 서버 소켓과 클라이언트 소켓
※ TCP 서버 : TCP 서버 프로그램을 개발하려면 ① 우선 ServerSocket 객체를 생성해야 한다. 다음은 50001번 Port에 바인딩하는 ServerSocket를 생성하는 코드이다.
ServerSocket serverSocket = new ServerSocket(50001);
ServerSocket을 생성하는 또 다른 방법은 기본 생성자로 객체를 생성하고 Port 바인딩을 위해 bind() 메소드를 호출하는 것이다.
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(50001));
만약 서버 컴퓨터를 여러 개의 IP가 할당되어 있을 경우, 특정 IP에서만 서비스를 하고 싶다면 InetSocketAddress의 첫 번째 매개값으로 해당 IP를 주면 된다.
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("xxx.xxx.xxx.xxx", 50001));
만약 Port가 이미 다른 프로그램을 사용 중이라면 BindException이 발생한다. 이 경우에는 다른 Port로 바인딩하거나 Port를 사용 중인 프로그램을 종료하고 다시 실행하면 된다. ServerSocket이 생성되었다면 연결 요청을 수락하기 위해
② accept() 메소드를 실행해야 한다. accept()는 클라이언트가 연결 요청하기 전까지 블로킹된다. 블로킹이란 실행을 멈춘 상태가 된다는 뜻이다. 클라이언트의 연결 요청이 들어오면 블로킹이 해제되고 통신용 Socket을 리턴한다.
Socket socket = serverSocket.accept();
만약 리턴된 Socket을 통해 연결된 클라이언트 IP 주소와 Port 번호를 얻고 싶다면 방법은 getRemoteSocketAddress() 메소드를 호출해서 InetSocketAddress를 얻은 다음 getHostName()과 getPort() 메소드를 호출하면 된다.
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
String clientIp = osa.getHostName();
String portNo = isa.getPort();
서버를 종료하려면 ServerSocket의 close() 메소드를 호출해서 Port 번호를 언바인딩 시켜야 한다. 그래야 다른 프로그램에서 해당 Port 번호를 재사용할 수 있다.
serverSocket.close();
※ TCP 클라이언트 : 클라이언트가 서버에 연결 요청을 하려면 Socket 객체를 생성할 때 생성자 매개값으로 서버 IP 주소와 Port 번호를 제공하면 된다. 로컬 컴퓨터에서 실행하는 서버로 연결 요청을 할 경우에는 IP 주소 대신 localhost를 사용할 수 있다.
Socket socket = new Socket ("IP", 50001 );
// Client가 IP주소와 Port번호를 Server에 알려줘야함 그래야 연결요청가능
※ 입출력 스트림으로 데이터 주고 받기 : 클라이언트가 연결 요청 ( connect())을 하고 서버가 연결 수락(accept()) 했다면, 다음 그림과 같이 양쪽 Socket 객체로부터 각각 입력 스트림(InputStream)과 출력 스트림(OutputStream)을 얻을 수 있다.
다음은 Socket으로부터 InputStream과 OutputStream을 얻는 코드이다.
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
상대방에게 데이터를 보낼 때는 보낼 데이터를 byte[] 배열로 생성하고, 이것을 매개값으로 해서 OutputStream의 write() 메소드를 호출하면 된다. 다음 코드는 문자열로부터 UTF-8로 인코딩한 바이트 배열을 얻어내고, write() 메소드로 전송한다.
String data = "보낼 데이터";
byte []bytes = data.getBytes("UTF-8");
OutputStream os = socket.getOutputStream();
os.write(bytes);
os.flush();
☆★ 문자열을 좀 더 간편하게 보내고 싶다면 보조 스트림인 DataOutputStream을 연결해서 사용하면 된다.
String data = "보낼 데이터";
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUFT(data);
dos.flush();
※ 소켓 프로그래밍 : 소켓이 만들어지는 과정 ( 3-way handshake )
Client - i/o - Server
①Socket ②Socket // 데이터 통신을 위해서는 Client Server 둘다 Socket 존재 해야함.
1) ServerSocket 생성됨 Server(198.10.2.1) : 9000
2) Client가 Server의 IP, Port 번호로 접속을 시도
3) ServerSocket(accept) -> 4) Socket
5) Socket로부터 Client에 접속
6) Server로 접속하기 위해 내부포트 열림
[ ☆ Server 생성 ]
[ ☆ Client 생성 ]
[ ☆ cmd창을 활용한 출력하기 ]
※ cmd창을 활용한 출력하기 순서 ※
1)이클립스 Java Project에 오른쪽 마우스 클릭 후 Properties 클릭 Location에서 경로 확인 !!
2) cmd 오른쪽 마우스 클릭 후 관리자 권한으로 실행 => 창 2개 띄어주기 Server와 Client용 !!
3) cd.. 으로 최상위 디렉토리로 벗어나기
4) cd Users -> cd bitcamp -> cd eclips-workspace -> cd A0220 차례대로 첫번째~마지막 디렉토리까지 들어가기
5) cd bin bin으로 들어간 후 java 클래스명 입력 !
6) Server 클래스 연결 후 똑같은 작업 다른 cmd 창에 Client 클래스도 해주기
[ UDP 네트워킹 ] 생략
UDP(User Datagram Protocol)는 발신자가 일방적으로 수신자에게 데이터를 보내는 방식으로, TCP처럼 연결 요청 및 수락 과정이 없기 때문에 TCP보다 데이터 전송 속도가 상대적으로 빠르다. UDP는 TCP처럼 고정 회선이 아니라 여러 회선을 통해 데이터가 전송되기 때문에 특정 회선의 속도에 따라 데이터가 순서대로 전달되지 않거나 잘못된 회선으로 인해 데이터 손실이 발생할 수 있다. 하지만 실시간 영상 스트리밍에서 한 컷의 영상이 손실되더라도 영상은 계속해서 수신되므로 문제가 되지 않는다. 따라서 데이터 전달의 신뢰성 보다 속도가 중요하다면 UDP를 사용하고, 데이터의 전달의 신뢰성이 중요하다면 TCP를 사용해야 한다.
자바는 UDP 네트워킹을 위해 java.net 패키지에서 DatagramSocket과 DatagramPacket 클래스를 제공하고 있다. DatagramSocket은 발신점과 수신점에 해당하고 DatagramPacket은 주고 받는 데이터에 해당된다.
※ UDP 서버 : UDP 서버를 위한 DatagramSocket 객체를 생성할 때에는 다음과 같이 바인딩할 Port 번호를 생성자 매개값으로 제공해야 한다.
DatagramSocket datagramSocket = new DatagramSocket (50001);
UDP 서버는 클라이언트가 보낸 DatagramPacket을 항상 받을 준비를 해야 한다. 이 역할을 하는 메소드가 receive()이다. receive() 메소드는 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다.
DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
datagram.Socket.receive(receivePacket);
DatagramPacket 생성자의 첫 번째 매개값은 수신된 데이터를 저장할 배열이고 두 번째 매개값은 수신할 수 있는 최대 바이트 수이다. 보통 첫 번째 바이트 배열의 크기를 준다. receive() 메소드가 실행된 후 수신된 데이터와 바이트 수를 얻는 방법은 다음과 같다.
byte []bytes = receivePacket.getData();
int num = receivePacket.getLength();
읽은 데이터가 문자열이라면 다음과 같이 String 생성자를 이용해서 문자열을 얻을 수 있다.
String data = new String(bytes, 0, num, "UTF-8");
이제 반대로 UDP 서버가 클라이언트에게 처리 내용을 보내려면 클라이언트 IP 주소와 Port 번호가 필요한데, 이것은 receive() 로 받은 DatagramPacket에서 얻을 수 있다. getSocketAddress() 메소드를 호출하면 정보가 담긴 SocketAddress 객체를 얻을 수 있다.
SocketAddress socketAddress = receivePacket.getSocketAddress();
이렇게 얻은 SocketAddress 객체는 다음과 같이 클라이언트로 보낼 DatagramPacket을 생성할 때 네 번째 매개값으로 사용된다. DatagramPacket 생성자의 첫 번째 매개값은 바이트 배열이고 두 번째는 시작 인덱스, 세 번째는 보낼 바이트 수이다.
String data = "처리 내용";
byte []bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket ( bytes, 0, bytes.length, socketAddress );
DatagramPacket을 클라이언트로 보낼 때는 DatagramSocket의 send() 메소드를 이용한다.
datagramSocket.send( sendPacket );
더 이상 UDP 클라이언트의 데이터를 수신하지 않고 UDP 서버를 종료하고 싶을 경우에는 다음과 같이 DatagramSocket의 close(); 메소드를 호출하면 된다.
datagramSocket.close();
※ UDP 클라이언트 : UDP 클라이언트 서버에 요청 내용을 보내고 그 결과를 받는 역할을 한다. UDP 클라이언트를 위한 DatagramSocket 객체는 기본 생성자로 생성한다. Port 번호는 자동으로 부여되기 떄문에 따로 지정할 필요가 없다.
DatagramSocket datagramSocket = new DatagramSocket();
요청 내용을 보내기 위한 DatagramPacket을 생성하는 방법은 다음과 같다.
String data = "요청 내용";
byte []bytes = data.getBytes("UTF-8");
DatagramPacket snedPacket = new DatagramPacket(
bytes, bytes.length, new InetSocketAddress("localhost", 50001);
DatagramPacket 생성자의 첫 번째 매개값은 바이트 배열이고, 두 번째 매개값은 바이트 배열에서 보내고자 하는 바이트 수이다. 세 번째 매개값은 UDP 서버의 IP와 Port 정보를 가지고 있는 InetSocketAddress 객체이다. 생성된DatagramPacket 을 매개값으로 해서 DatagramSocket의 send() 메소드를 호출하면 UDP 서버로 DatagramPacket이 전송된다.
datagramSocket.send(sendPacket);
UDP 서버에서 처리 결과가 언제 올지 모르므로 항상 받을 준비를 하기 위헤 receive() 메소드를 호출한다. receiver() 메소드는 데이터를 수신할 때까지 블로팅되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다. 이부분은 UDP 서버와 동일하다. 더 이상 UDP 서버와 통신할 필요가 없다면 DatagramSocket을 닫기 위해 close() 메소드를 다음과 같이 호출한다.
datagramSocket.close();
[ 서버의 동시 요청 처리 ]
일반적으로 서버는 다수의 클라이언트와 통신을 한다. 서버는 클라이언트들로부터 동시에 요청을 받아서 처리하고, 처리 결과를 개별 클라이언트로 보내줘야 한다. TCP 네트워킹과 UDP 네트워킹에서 다룬 서버 예제는 먼저 연결한 클라이언트의 요청을 처리한 후, 다음 클라이언트 요청을 처리하도록 되어 있다.
이와 같은 방식은 먼저 연결한 클라이언트의 요청 처리 시간이 길어질수록 다음 클라이언트의 요청 처리 작업이 지연될 수 밖에 없다. 따라서 accpet()와 receive()를 제외한 요청 처리 코드를 별도의 스레드에서 작업하는 것이 좋다.
스레드를 처리할 때 주의할 점은 클라이언트의 폭증으로 인한 서버의 과도한 스레드 생성을 방지해야 한다는 것이다. 그래서 스레드풀을 사용하는 것이 바람직하다. 다음은 스레드풀을 이용해서 요청을 처리하는 방식이다.
스레드풀은 작업 처리 스레드 수를 제한해서 사용하기 때문에 갑작스런 클라이언트 폭증이 발생해도 크게 문제가 되지 않는다. 다만 작업 큐의 대기 작업이 증가되어 클라이언트에서 응답을 늦게 받을 수도 있다.
※ TCP EchoServer 동시 요청 처리 : TCP 서버인 EchoServer 를 수정한 것으로, 스레드풀을 이용해서 클라이언트의 요청을 동시에 처리하도록 했다.
※ UDP NewsServer 동시 요청 처리 : UDP 서버인 NewsServer를 수정한 것으로, 스레드풀을 이용해서 클라이언트의 요청을 동시에 처리하도록 하였다.
[ JSON 데이터 형식 ]
네트워크로 전달하는데 데이터가 복잡할수록 구조화된 형식이 필요하다. 네트워크 통신에서 가장 많이 사용되는 데이터 형식은 JSON(JavaScript Object Notation)이다. JSON의 표기법은 다음과 같다.
객체 표기 | { "속성명" : 속성값, "속성명" : 속성값, ... } |
속성명 : 반드시 ""로 감싸야함 속성값으로 가능한 것 - "문자열", 숫자, true/false - 객체 { ... } - 배열 { ... } |
배열 표기 | [ 항목, 항목, ... ] | 항목으로 가능한 것 - "문자열", 숫자, true/false - 객체 { ... } - 배열 { ... } |
두 개 이상의 속성이 있는 경우에는 객체 { } 로 표기하고, 두 개 이상의 값이 있는 경우에는 배열 [ ] 로 표기한다. 예를 들어 어떤 회원 정보를 JSON으로 표기하면 다음과 같다.
{
"id" : "winter",
"name" : "한겨울",
"age" : 25,
"student" : true,
"tel" : { "home", "02-123-1234", "mobile" : "010-1234-5789" },
"skill" : [ "java", "c", "c++" ]
}
JSON을 문자열로 직접 작성할 수 있지만 대부분은 라이브러리를 이용해서 생성한다. 가장 많이 사용하는 라이브러리는 https://github.com/stleary/JSON-java 에서 받을 수 있다. 'Click here if you just want the lastest release jar file' 링크를 클릭하면 다음과 같은 파일을 다운로드 받을 수 있다. json-20220320.jar (다운로드하는 시점에 따라 버전 날짜가 다를 수 있다)
다운로드한 파일을 이클립스 java project의 lib 폴더 안에 복사한다. lib 폴더가 없으면 java project를 마우스 오른쪽 클릭한 후 [ New ] - [ Folder ]를 선택해서 생성한다. JAR 파일 안에는 JSON을 생성하거나 파싱(분석) 하는 클래스들이 들어있다. 이 클래스들을 이클립스에서 사용하려면 다음 화면과 같이 JAR 파일을 Build Path에 추가해야 한다.
다음은 JSON 표기법과 관련된 클래스들이다.
클래스 | 용도 |
JSONObject | JSON 객체 표기를 생성하거나 파싱할 때 사용 |
JSONArray | JSON 배열 표기를 생성하거나 파싱할 때 사용 |
JSON에서 속성 순서는 중요하지 않기 때문에 추가한 순서대로 작성되지 않아도 상관없다. 그리고 줄바꿈 처리가 되지 않는데, 오히려 이것이 네트워크 전송량을 줄여주기 때문에 더 좋다.
[ TCP 채팅 프로그램 ] ☆★
TCP 네트워킹을 이용해서 채팅 서버와 클라이언트를 구현해보자. 다음은 채팅 서버와 클라이언트에서 사용할 클래스 이름을 보여준다.
클래스 | 용도 |
ChatServer | - 채팅 서버 실행 클래스 - ServerSocket을 생성하고 50001에 바인딩 - ChatClient 연결 수락 후 SocketClient 생성 |
SocketClient | - ChatClient와 1:1로 통신 |
ChatClient | - 채팅 클라이언트 실행 클래스 - ChatServer에 연결 요청 - SocketClient와 1:1로 통신 |
※ 채팅 서버 : ChatServer는 채팅 서버 실행 클래스로 클라이언트의 연결 요청을 수락하고 통신용 SocketClient를 생성한느 역할을 한다.
chatRoom()은 통신용 SocketClient를 관리하는 동기화된 Map 컬렉션이다.
start() 메소드는 채팅 서버가 시작할 때 제일 먼저 호출된 것으로, 50001번 Port에 바인딩하는 ServerSocket을 생성하고 작업 스레드가 처리한 Runnable을 람다식 () -> {...}으로 제공한다. 람다식은 accept()을 생성하고 메소드로 연결 수락하고, 통신용 SocketClient를 반복해서 생성한다. addSocketClient() 메소드는 연결된 클라이언트의 SocketClient를 chatRoom(채팅방)에 추가하는 역할을 한다. 키는 "chatName@clientIp"로 하고 SocketClient를 값으로 해서 저장한다. removeSocketClient() 메소드는 연결이 끊긴 클라이언트의 SocketClient를 chatRoom(채팅방)에서 제거하는 역할을 한다.
sendToAll() 메소드는 JSON 메시지를 생성해 채팅방에 있는 모든 클라이언트에게 보내는 역할을 한다.
stop() 메소드는 채팅 서버를 종료시키는 역할을 한다. serverSocket과 threadPool을 닫고 chatRoom에 있는 모든 SocketClient를 닫는다. 그리고 chatRoom.values()로 Collection<SocketClient>를 얻고, 요소 스트림을 이용해서 전체 SocketClient의 close() 메소드를 호출한다.
main() 메소드는 채팅 서버를 시작하기 위해 ChatServer 객체를 생성하고 start() 메소드를 호출한다. 키보드로 q를 입력하면 stop() ㅔ소드를 호출해서 채팅 서버를 종료한다.
SocketClient는 클라이언트와 1:1로 통신하는 역할을 한다. chatServer필드는 ChatServer()의 메소드를 호출하기 위해 필요하다. socket은 연결을 끊을 때 필요하고, dis와 dos는 문자열을 읽고 보내기 위한 보조 스트림이다. clientIp와 chatName은 클라이언트 IP 주소와 대화명이다.
receive() 메소드는 클라이언트가 보낸 JSON 메시지를 읽는 역할을 한다. dis.readUTF()로 JSON을 읽고 JSONObject로 파싱해 command 값을 먼저 얻어낸다. 그 이유는 command에 따라 처리 내용이 달라지기 때문이다. command가 incoming이라면 JSON에서 대화명을 읽고 chatRoom에 SocketClient를 추가한다. command가 message라면 JSON에서 메시지를 읽고 연결되어 있는 모든 클라이언트에게 보낸다. 클라이언트가 채팅을 종료할 경우 dis.readUTF()에서 IOExceptiom이 발생하기 때문에, 예외처리를 해서 chatRoom()에 저장되어 있는 SocketClient를 제거한다.
close() 메소드는 클라이언트와 연결을 끊는 역할을 한다. ChatServer의 stop() 메소드에서 호출된다. ChatServer와 SocketClient 클래스 모두 작성했다면 ChatServer를 실행해보자.
※ 채팅 클라이언트 : 채팅 클라이언트는 ChatClient 단일 클래스이다. ChatClient는 채팅 서버로 연결을 요청하고, 연결 된 후에는, 제일 먼저 대화명을 보낸다. 그리고 난 다음 서버와 메시지를 주고 받는다.
socket은 연결 요청과 연결을 끊을 때 필요하고, dis와 dos는 문자열을 읽고 보내기 위한 보조 스트림이다. chatName은 클라이언트 대화명이다.
connect() 메소드는 채팅 서버(localhost, 50001)에 연결 요청을 하고 Socket을 필드에 저장한다. 그리고 문자열 입출력을 위해 DataInputStream과 DataOutputStream을 생성해서 필드에 저장한다. 만약 다른 PC에 있는 채팅 서버와 연결하고 싶다면 localhost 대신 IP 주소로 변경하면 된다.
receive() 메소드는 서버가 보낸 JSON 메시지를 읽는 역할을 한다. dis.readUTF()로 JSON을 읽고 JSONObject로 파싱해서 clientIp, chatName, message를 얻어낸다. 그리고 Console 뷰에 "<chatName@clientIp> message"로 출력한다. 서버와 통신이 끊어지면 dif.readUTF()에서 IOExeption이 발생하기 때문에, 예외 처리를 해서 클라이언트도 종료되도록 한다.
send() 메소드는 서버로 JSON 메시지를 보내는 역할을 한다. main()에서 키보드로 입력한 메시지를 보낼 때 호출된다.
unconnect() 메소드는 Socket의 close() 메소드를 호출해서 서버와 연결을 끊는다. main() 메소드에서 q가 입력되었을 때 채팅을 종료하기 위해 호출된다.
main() 메소드는 채팅 클라이언트를 시작하기 위해 ChatClient 객체를 생성하고, 채팅 서버와 연결하기 위해 connect() 메소드를 호출한다. 연결이 되면 대화명을 키보드로부터 입력 받고 다음과 같은 JSON 메시지를 서버로 보낸다.
[ ☆ ChatServer 생성 ]
import java.io.*;
import java.net.*;
import java.util.*;
public class ChatServer {
HashMap clients;
ChatServer() {
clients = new HashMap();
Collections.synchronizedMap(clients);
}
public void start() {
ServerSocket serverSocket = null;
Socket socket = null;
try{
serverSocket = new ServerSocket(9999);
System.out.println("서버 기다림");
while(true) {
socket = serverSocket.accept();
System.out.println(socket.getInetAddress()+":"+
socket.getPort()+" connect!");
ServerReceiver thread = new ServerReceiver(socket);
thread.start(); // run()
}
}catch(Exception e) {e.printStackTrace();}
}
void sendAll(String msg) {//브로드캐스팅 기능
Iterator iterator = clients.keySet().iterator();
while(iterator.hasNext()) {
try {
DataOutputStream out =
(DataOutputStream)clients.get(iterator.next());
out.writeUTF(msg);
}catch(IOException e) {e.printStackTrace();}
}
}
public static void main(String[] args) {
new ChatServer().start();
}
//inner class
class ServerReceiver extends Thread {
Socket socket; DataInputStream in; DataOutputStream out;
ServerReceiver(Socket socket) {
this.socket = socket;
try{
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
}catch(Exception e) {e.printStackTrace();}
}
public void run() {
String name = "";
try{
name = in.readUTF();
if (clients.get(name) != null) {//같은 이름 사용자 존재
out.writeUTF("이미 이름 있음 : "+name);
out.writeUTF("다른이름으로 연결해줘");
System.out.println(socket.getInetAddress()+":"+
socket.getPort()+" disconnect!");
in.close();
out.close();
socket.close();
socket = null;
} else {//같은 이름 존재하지 않는 경우
sendAll("#"+name+" 들어옴");
clients.put(name, out);
while(in != null) { sendAll(in.readUTF()); }
}
}catch(IOException e) { e.printStackTrace();
}finally{
if (socket != null) {
sendAll("#"+name+" exit!");
clients.remove(name);
System.out.println(socket.getInetAddress()+":"+
socket.getPort()+" disconnect!");
}
}
}
}
}
[ ☆ ChatClients 생성 ]
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ChatClient {
public static void main(String[] arg) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
Scanner scanner = new Scanner(System.in);
System.out.print("이름:");
String name = scanner.nextLine();
Thread sender = new Thread(new Sender(socket,name));
Thread receiver = new Thread(new Receiver(socket));
sender.start();
receiver.start();
} catch(Exception e) { e.printStackTrace(); }
}
// inner class
static class Sender extends Thread {
Socket socket; DataOutputStream out; String name;
Sender(Socket socket, String name) {
this.socket = socket; this.name = name;
try {
out = new DataOutputStream(socket.getOutputStream());
}catch(Exception e) { e.printStackTrace(); }
}
public void run() {
Scanner s = new Scanner(System.in);
try{
if (out != null) out.writeUTF(name);
while(out != null) {
String msg = s.nextLine();
if (msg.equals("stop")) break;
out.writeUTF("("+name+")"+msg);
}
out.close();
socket.close();
}catch(Exception e) { e.printStackTrace(); }
}
}
static class Receiver extends Thread {
Socket socket; DataInputStream in;
Receiver(Socket socket) {
this.socket = socket;
try{
in = new DataInputStream(socket.getInputStream());
}catch(Exception e) { e.printStackTrace(); }
}
public void run() {
while(in != null) {
try{
System.out.println(in.readUTF());
} catch(Exception e) {
e.printStackTrace();
break;
}
}
try {
in.close();
socket.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
}
[ 관리자 cmd창에서 실행시키기 ]
[ 실행 중인 PID 알아내서 강제 종료 시키는 방법 ]
cmd창에 [ netstat -a -o ] 입력 로컬주고 Port번호에 맞는 PID 번호를 찾기
작업 관리자 창에서 세부목록 보기 PID 보기 편하게 정렬 -> PID 번호 찾기 강제 종료
[ ☆ ChatServer 생성 - 2 ]
import java.io.*;
import java.net.*;
public class Server2 {
public static void main(String[] args) {
//서버 쪽
ServerSocket ser=null;
try {
ser=new ServerSocket(); //1.서버소켓 생성
ser.bind(new InetSocketAddress("localhost",5001));
//2.서버소켓과 서버소켓이 연결될 ip주소와 포트번호
while(true) {
System.out.println("연결이 되기를 기다림");
//3.연결요청을 수락하면서 소켓 생성한다.
Socket so=ser.accept();
byte[]b = null;
String msg=null;
//읽어들인다!!!!
InputStream in=so.getInputStream();
b=new byte[100];
int r=in.read(b);
//메시지 바이트 배열 읽는다.
msg=new String(b,0,r,"UTF-8");
//바이트 배열을 문자열로 바꾼다.
System.out.println("데이터 받기 성공");
/////////////////////////////////////////위에는 읽는 작업 밑은 전송 작업
OutputStream os=so.getOutputStream();
msg="Hi Client";
//문자열을 바이트로 변경해서 전송하겠다.
b=msg.getBytes("UTF-8");
os.write(b);
System.out.println("데이터 보내기 성공");
os.close();
in.close();
so.close();
ser.close();
}
}catch(Exception e) {}
}
}
[ ☆ ChatClients 생성 - 2 ]
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
public class Client2 {
public static void main(String[] args) {
//클라이언트 쪽
Socket s=null;
try {
s=new Socket();
System.out.println("연결 요청");
s.connect(new InetSocketAddress("localhost",5001));
System.out.println("연결 성공");
byte[]b = null;
String msg=null;
OutputStream os=s.getOutputStream();
msg="Hi Server";
b=msg.getBytes("UTF-8");
//문자열을 바이트로 변경해서 전송하겠다.
os.write(b);
System.out.println("데이터 보내기 성공");
//읽어들인다!!!!
InputStream in=s.getInputStream();
b=new byte[100];
int r=in.read(b);
//메시지 바이트 배열 읽는다.
msg=new String(b,0,r,"UTF-8");
//바이트 배열을 문자열로 바꾼다.
System.out.println("데이터 받기 성공");
in.close();
os.close();
s.close();
}catch(Exception e) {}
}
}