오늘은 Network 응용 심화편을 알아보도록 할 겁니다. 코드가 매우 길고 Nested class를 이용하여 다소 복잡할 수 있으니 꾹 참고 내용 확인해 보세요.
그래서 오늘 만들 프로그램은 바로 채팅서버를 만들어 보는 것입니다. 물론 실생활에서 사용할 수 없는 채팅 프로그램이고 기능도 현저히 떨어지는 프로그램이지만 통신이 어떻게 되고 객체가 어떻게 움직이는 지 더 확실히 알 수 있습니다.
채팅 서버와 채팅 클라이언트 서버를 제작해서 여러명의 클라이언트가 들어왔을 때도 채팅방에 내가 친 채팅을 다른 클라이언트에게 보여주는 프로그램을 만들 것입니다. 그냥 카카오톡 비슷하게 만든다고 보시면 됩니다.
이전 글 보고 기본편을 학습하고 오시면 더 이해가 잘 될 것입니다.
https://pabeba.tistory.com/129
채팅 서버
public class ChattingServer {
private ArrayList<ServerWorker> list = new ArrayList<>();
public void go() throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(5432);
System.out.println("채팅서버 시작");
while (true) {
Socket socket = serverSocket.accept();
ServerWorker sw = new ServerWorker(socket);
list.add(sw);
Thread swThread = new Thread(sw);
swThread.start();
}
} finally {
if (serverSocket != null)
serverSocket.close();
}
}
// list 의 요소를 이용해 접속한 모든 클라이언트에게 메세지 전송
public void broadCast(String message) {
for (int i = 0; i < list.size(); i++) {
ServerWorker sw = list.get(i);
sw.pw.println(message);
}
}
public static void main(String[] args) {
try {
new ChattingServer().go();
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* Nested Class 직원 스레드 : 실제 클라이언트에게 채팅 서비스를 제공하는 역할
*/
public class ServerWorker implements Runnable {
private Socket socket;
private PrintWriter pw;
private BufferedReader br;
private String IpName;
public ServerWorker(Socket socket) {
this.socket = socket;
this.IpName = socket.getInetAddress().toString();
}
@Override
public void run() {
try {
chatting();
} catch (UnsupportedEncodingException e) {
System.out.println(IpName + "님 강제종료했음 " + e.getMessage());
} catch (IOException e) {
System.out.println(IpName + "님 강제종료했음 " + e.getMessage());
}
}
/*
* client가 보낸 메세지를 입력받아 broadcast() 를 이용해 접속한 모든 client들에게 메세지를 전송한다 null or
* exit 가 전송되면 실행 마무리하고 list 에서 자신(직원)을 삭제( 누구님 나가셨습니다 메세지 출력 )
*/
public void chatting() throws UnsupportedEncodingException, IOException {
try {
br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"), true);
broadCast(IpName + "님이 입장하셨습니다.");
while (true) {
String clientMsg = br.readLine();
if (clientMsg == null || clientMsg.equalsIgnoreCase("exit"))
break;
broadCast(IpName + "님 : " + clientMsg);
}
} finally {
list.remove(this);
broadCast(IpName + "님이 퇴장하셨습니다.");
if (socket != null)
socket.close();
}
}
}
}
너무나도 긴 코드이지만 하나하나 메소드 별로 확인해보면 금방 이해할 수 있습니다.
처음 go()메소드 부터 볼까요.
public void go() throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(5432);
System.out.println("채팅서버 시작");
while (true) {
Socket socket = serverSocket.accept();
ServerWorker sw = new ServerWorker(socket);
list.add(sw);
Thread swThread = new Thread(sw);
swThread.start();
}
} finally {
if (serverSocket != null)
serverSocket.close();
}
}
1. 서버 소켓을 생성해서 소켓이 들어오기를 기다립니다.
2. 소켓이 들어오면 서버에서 자동으로 일하게 할 serverworker를 생성합니다.
3. 일꾼을 List에 넣어 저장해줍니다.
4. Thread를 생성해서 실행해줍니다.
5. while문이 종료되면 서버소켓을 close해줍니다. (특이한 에러가 안나면 안꺼질 겁니다.)
다음으로 ServerWorker의 chatting() 메소드를 보겠습니다.
public void chatting() throws UnsupportedEncodingException, IOException {
try {
br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"), true);
broadCast(IpName + "님이 입장하셨습니다.");
while (true) {
String clientMsg = br.readLine();
if (clientMsg == null || clientMsg.equalsIgnoreCase("exit"))
break;
broadCast(IpName + "님 : " + clientMsg);
}
} finally {
list.remove(this);
broadCast(IpName + "님이 퇴장하셨습니다.");
if (socket != null)
socket.close();
}
}
}
}
1. 소켓에 있는 값을 읽어내고 작성해 놓습니다.
2. 소켓의 주인 IP를 broadcast 함수에 적용합니다.(조금 있다가 broadcast도 설명 예정)
3. 메세지를 읽어내는 함수들을 while문에 넣어 반복시키고 메세지의 내용이 없거나 exit 이면 while 문 종료합니다.
4. 메세지를 받으면 계속 broadcast함수에 적용합니다.
5. 만일 쓰레드가 끝나면 list에서 자신을 지우고 모두에게 퇴장 메세지를 보내고 소켓을 close합니다.
broadCast함수를 알아보겠습니다.
public void broadCast(String message) {
for (int i = 0; i < list.size(); i++) {
ServerWorker sw = list.get(i);
sw.pw.println(message);
}
}
1. Server 일꾼 리스트를 for문을 이용해서 각각의 소켓의 printWriter에 저장하여 보내줍니다.
서버에서 한 클라이언트의 메세지를 받아서 그 메세지를 채팅방에 들어온 클라이언트들에게 보내주는 프로그램입니다.
다음으로 클라이언트 서버를 알아볼까요.
클라이언트 서버
public class ChattingClient {
private Socket socket;
private Scanner sc;
/*
* 소켓 생성 -> ReceiverWorker Thread 생성 , setDaemon(true) -> 출력작업 sendMessge() :
* 스캐너에서 입력받아 서버로 출력 exit 를 입력하면 서버로 보낸 후 종료
*/
public void go() throws IOException {
try {
socket = new Socket(IP.MY_IP, 5432);
System.out.println("채팅을 시작합니다.");
ReceiverWorker rw = new ReceiverWorker();
Thread rwThread = new Thread(rw);
rwThread.setDaemon(true);
rwThread.start();
sendMessge();
} finally {
if (sc != null)
sc.close();
if (socket != null)
socket.close();
}
}
public void sendMessge() throws UnsupportedEncodingException, IOException {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"), true);
while (true) {
sc = new Scanner(System.in);
String clientMsg = sc.nextLine();
if (clientMsg == null || clientMsg.equalsIgnoreCase("exit")) {
System.out.println("채팅이 종료되었습니다.");
break;
}
pw.println(clientMsg);
}
}
public static void main(String[] args) {
try {
new ChattingClient().go();
} catch (IOException e) {
e.printStackTrace();
}
}
// Nested Class
public class ReceiverWorker implements Runnable {
/*
* 서버로부터 친구들의 메세지를 입력받아 콘솔에 출력하는 역할
*/
@Override
public void run() {
try {
receiveMessage();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void receiveMessage() throws UnsupportedEncodingException, IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
while (true) {
String msg = bf.readLine();
if (msg == null)
break;
System.out.println(msg);
}
}
}
}
클라이언트 서버는 조금 더 간단합니다. 보내주고 받는 메소드를 따로 저장하여 진행하였습니다. 받는 메소드를 진행하는 class를 Nested class로 생성하여 진행했습니다.
go()함수부터 알아보죠.
public void go() throws IOException {
try {
socket = new Socket(IP.MY_IP, 5432);
System.out.println("채팅을 시작합니다.");
ReceiverWorker rw = new ReceiverWorker();
Thread rwThread = new Thread(rw);
rwThread.setDaemon(true);
rwThread.start();
sendMessge();
} finally {
if (sc != null)
sc.close();
if (socket != null)
socket.close();
}
}
1. 소켓을 나의 아이피, 포트번호 5432로 생성
2. 받는 일꾼을 생성하고 스레드로 만들어 줍니다.
3. 생성된 스레드는 채팅 클라이언트 class의 go함수가 끝날 때 같이 꺼집니다.
4. sendMessage() 실행
5. go함수가 마무리되면 scanner 와 socket이 close 됩니다.
여기까지는 별 내용이 없어서 다음 함수를 보겠습니다.
public void sendMessge() throws UnsupportedEncodingException, IOException {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"), true);
while (true) {
sc = new Scanner(System.in);
String clientMsg = sc.nextLine();
if (clientMsg == null || clientMsg.equalsIgnoreCase("exit")) {
System.out.println("채팅이 종료되었습니다.");
break;
}
pw.println(clientMsg);
}
}
1. 소켓에 보낼 메세지를 저장하는 코드
2. scanner를 이용하여 직접 메세지 작성
3. 메세지의 내용이 없거나 exit이면 while문 종료 및 go 함수도 종료
마지막으로 receiveMessage()메소드를 확인해보겠습니다.
public void receiveMessage() throws UnsupportedEncodingException, IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
while (true) {
String msg = bf.readLine();
if (msg == null)
break;
System.out.println(msg);
}
}
1. 메세지를 받아오는 메소드로 while문을 이용해서 계속 받아옵니다.
2. 만약 메세지가 없으면 while문을 종료합니다.(client가 나간 것)
3. 메세지를 받아와서 출력합니다.
클라이언트 쪽은 딱히 특별히 많이 변한 것은 없고 Nested class에 대한 이해를 조금 더 넓힐 수 있다 입니다.
소감
처음 보는 것과 두 번째 보는 것은 느낌이 너무나도 다르게 느껴집니다. 복습을 통해서 더 깊이 알 수 있고, 처음에는 소켓을 어디 부분에서 close 해야하는지 감이 안와서 소켓이 꺼지지 않아 퇴장 문구가 5만번 정도 찍히고 나서야 서버를 껐습니다....
다음에 소켓으로 통신을 하거나 그냥 통신을 하게 된다면 이런일이 발생할 것 같은데, 시작과 끝 맺음을 잘 하여 서버가 터지지 않게 해야겠습니다.
'코딩 개발 > Java' 카테고리의 다른 글
Java & ORACLE - TRANSACTION (0) | 2023.05.06 |
---|---|
Java - DB연결을 위한 Cloud Service (feat. AWS EC2, ORACLE) (4) | 2023.04.30 |
Java - Network (Socket 통신) (2) | 2023.04.24 |
Java - Thread, Runnable (4) | 2023.04.23 |
Java - ObjectInputStream & ObjectOutputStream (2) | 2023.04.20 |