기초단계/JAVA

2022.12.10 JAVA 네트워크 입출력

춘핑이 2022. 12. 10. 19:51

19. 네트워크 입출력

19.5 서버의 동시 요청 처리

말그대로 서버에서 동시에 요청을 처리하는 것
이전예제들은 하나만 받고 하나만 처리했다.
요청-> 연결수락 및 외장처리 ->연결수락및 요청처리를 위한 반복
서버는 불특정다수가 처리를 요청할 경우 처리를 여러번 해줘야한다.

먼저 연결한 클라이언트의 요청처리 시간이 길어질수록 다음 클라이언트의 요청처리 작업이 지연될 수밖에없다.
따라서 accept()와 receive()를 제외한 요청처리 코드를 별도의 스레드에서 작업하는 것이 좋다.
스레드를 처리할때 주의할점은 클라이언트의 폭즈으로 인한 서버의 과도한 스레드 생성을 방지해야한다는 것이다.
그래서 스레드풀을 사용하는 것이 바람직하다.


스레드풀은 작업처리 스레드 수를 제한해서 사용하기 때문에 갑작스런 클라이언트 폭증이 발생해도 크게 문제가 되지않는다.
다만 작업 큐의 대기 작업이 증가되어 클라이언트에서 응답을 늦게 받을 수 있다.
작업하나하나가 Runnable객체 or Callable객체인것

19.5.1 TCP EchoServer 동시요청처리

interface Runnable{ void run()} 으로 되있는데
이 run을 구현하기위해 구현객체를 넣음 () ->{}
동시에 서버 요청을 받고자 한다면 스레드풀을 구현하는 것은 필수불가결하다.
메인스레드에서 서버 실행 및 종료 등 메소드사용
스레드1에서 키보드 입력받아 클라이언트 요청기다리기
스레드풀에서 요청들을 받아 처리하기
이전예제들에 스레드풀 추가
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(() -> {실행내용});

package ch19.sec05.exam01;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EchoServer {
private static ServerSocket serverSocket = null; 
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
//고정된 스레드풀을 가지고 처리하는 방법

public static void main(String[] args) {
    System.out.println("-------------------------------------------------------");
    System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter 키를 입력하세요.");
    System.out.println("-------------------------------------------------------");

    //TCP서버시작
    startServer(); //이부분에서 새로운 스레드 만들어서 서버 시작 실행됨

    //키보드 입력
    Scanner scanner = new Scanner(System.in);
    while (true) {
        String key = scanner.nextLine();
        if(key.toLowerCase().equals("q")) { //toLowerCase()다 소문자로 바꾸고 비교
            break;
        }
    }
    scanner.close(); // 원래는 시작하면 닫히는데 서버 24시간 돌아가는 객체는 안쓰면 닫아주는게 좋음.

    //TCP서버 종료
    stopServer();


}
//키보드에서 입력받아 서버를 종료하는 것 -메인 + 클라이언트의 요청을받아 수락하는 작업하나필요 -작업스레드
//그래서 멀티스레드 필요
public static void startServer() {
    //작업스레드 정의
    Thread thread = new Thread() {
        @Override
        public void run() {
            //ServerSocet생성 및 Port바인딩
            try {
                serverSocket = new ServerSocket(50001); //위에 선언해둔 필드사용
                System.out.println("[서버] 시작됨");

                while (true) {
                    System.out.println("\n[서버] 연결요청을 기다림\n");
                    //연결 수락
                    Socket socket = serverSocket.accept();//서버의 소켓객체 이것은 항상 되게먼들기

                    //executorService로 스레드풀을 만든후 execute()메소드실행 구현객체는 람다식으로
                    executorService.execute(() -> {
                        try {
                        //연결된 클라인어트 정보얻기
                        InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
                        String clientIp = isa.getHostName();
                        System.out.println("[서버] " + clientIp + "의 연결 요청을 수락함");
                        //------------------------------------------------------

                        //클라이언트로부터 데이터 받기 
                        InputStream is = socket.getInputStream();
                        //DataInputStream dis = new DataInputStream(is)이렇게하거나 밑에서 바로 넣기로 보기
                        byte[] bytes = new byte[1024];
                        int readByteCount = is.read(bytes); 
                        String message = new String(bytes, 0, readByteCount, "UTF-8");

                        //클라이언트에게 데이터 보내기
                        OutputStream os = socket.getOutputStream();
                        bytes = message.getBytes("UTF-8"); //바이트배열 얻기
                        os.write(bytes); 
                        os.flush();
                        System.out.println( "[서버] 받은 데이터를 다시 보냄: " + message);
                        /*
                        //데이터 받기
                        DataInputStream dis = new DataInputStream(socket.getInputStream());
                        String message = dis.readUTF();

                        //데이터 보내기
                        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                        dos.writeUTF(message);
                        dos.flush();
                        System.out.println( "[서버] 받은 데이터를 다시 보냄: " + message);
                        */

                        //-------------------------------------------------------
                        //연결 끊기
                        socket.close();
                        System.out.println("[서버] " + clientIp + "의 연결을 끊음");
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }

                });    

                } 

            } catch (Exception e) {
                System.out.println("[서버] " + e.getMessage());
            } 

        }
    };
    //스레드 시작
    thread.start();

}
public static void     stopServer() {
    //ServerSocket을 닫고 Port 언바인딩
    try {
        serverSocket.close();
        executorService.shutdownNow(); //스레드풀 다 닫아줘야함.
        System.out.println("[서버] 종료됨");
    } catch (IOException e) {}
}}

19.6 JSON 데이터형식

네트워크로 전달하는 데이터가 복잡할수록 구조화된 형식이 필요하다.
현재까지는 단순히 문자열 숫자만 보냈다.
예를들어 회원을 보낼때 이름 전화번호 이런걸 하나씩 보내는 것보다는 한번에 보내는 것이 좋다.
JSON 데이터형식은 데이터의 의미까지 포함하여 데이터를 구조화시켜서 이것은 가장 많이 사용되는 형식 중 하나임
JavaScript Object Notation으로 자바스크립트에서 처음 사용했지만 장점이 많아 데이터 전달 포맷으로 사용한다.


JSON 데이터형식은 형식을 따르지 않으면 오류가 난다.
NTT= 객체 속성명=자바의 필드명 "속성명" 큰따옴표로 반드시 감싸야함. 속성간의 구분은 ','넣어줘야하고 마지막엔 생략해야함.
배열표기 [항목,항목] 자바의 배열x JSON의 배열임.
회원의 정보를 JSON으로 표기한것

{
    "id": "winter",
    "name": "한겨울",
    "age": 25,
    "student": true,
    "tel": { "home": "02-123-1234", "mobile": "010-123-1234"} //객체 표기
    "skill": ["java", "c", "c++"]
}

자바에서 JSON을 문자열로 직접 작성할 수 있지만 대부분은 라이브러리를 이용해서 사용한다.
https://github.com/stleary/JSON-java jar파일 받아서 사용필요
https://mvnrepository.com/ 그런데 보통 여기 들어가서 다운받음(?) 여기서 깃허브주소잇어서 들어가서 다운
다운후 lib에 넣고 빌드패스에 추가하기 lombok처럼

다음은 JSON표기법과 관련된 클래스들이다.
JSONObject JSON객체 표기를 생성하거나 파싱할때 사용
JSONArray JSON배열 표기를 생성하거나 파싱할때 사용
JSON을 만들기도하고 JSON을 해석해서 데이터를 얻는데도 사용함.

package ch19.sec06;

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import org.json.JSONArray;
import org.json.JSONObject;

public class CreateJsonExample {
public static void main(String[] args) throws IOException {
    //JSON 객체 생성
    JSONObject root = new JSONObject();

    //속성 추가
    root.put("id", "winter");
    root.put("name", "한겨울");
    root.put("age", 25);
    root.put("student", true);

    //객체 속성 추가 
    //tel을 JSONObject의 형태로 만들기
    //root에 저장하였으니 속성의 값으로 tel객체를 넣는것 "tel":{객체표기법}으로 넣을 수 있음.
    JSONObject tel = new JSONObject();
    tel.put("home", "02-123-1234");
    tel.put("mobile", "010-123-1234");
    root.put("tel", tel);

    //배열 속성 추가
    JSONArray skill = new JSONArray();
    skill.put("java");
    skill.put("c");
    skill.put("c++");
    root.put("skill", skill);

    //만들고나서 실습
    //JSON 얻기
    String json = root.toString();

    //콘솔에 출력
    System.out.println(json);

    //파일로 저장
    // json은 보통 네트워크에서 사용하기때문에 이걸 사용하는 경우는 설정을 저장할경우에 파일로 만든다.
    Writer writer = new FileWriter("C:/Temp/member.json", Charset.forName("UTF-8"));
    writer.write(json);
    writer.flush();
    writer.close();
}}


추가한 순서와 출력되는 순서는 다름. 순서는 중요하지 않다.
또한 줄바꿈처리가 되지않는데 오히려 네트워크 전송량을 줄어줄기 때문에 좋다.

JSON 파싱

member.json파일을 읽고 JSON을 파싱하기
파싱 = 읽어들인다.
기호가 {}면 getJSONObject객체얻는 메소드 사용
기호가 []면 getJSONArray 배열얻는 메소드 사용
구분해서 사용하기

package ch19.sec06;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import org.json.JSONArray;
import org.json.JSONObject;

public class ParseJsonExample {
public static void main(String[] args) throws IOException {
    //파일로부터 JSON 읽기
    //한행을 읽을수 있는 메소드 제공하니 편리하게 사용하자.
    BufferedReader br = new BufferedReader(
        new FileReader("C:/Temp/member.json", Charset.forName("UTF-8"))
    );
    String json  = br.readLine();
    br.close();

    //JSON 파싱
    //JSONObject을만들고 해성하기 위해 해석해야할 것을 생성자에 넣어줌.
    JSONObject root = new JSONObject(json);

    //속성 정보 읽기 getString속성이름을 주면 속성을 뱉어냄.
    System.out.println("id: " + root.getString("id"));
    System.out.println("name: " + root.getString("id"));
    System.out.println("age: " + root.getInt("age"));
    System.out.println("student: " + root.getBoolean("student"));

    //객체 속성 정보 읽기
    //getJSONObject객체얻는 메소드 사용
    JSONObject tel = root.getJSONObject("tel");
    System.out.println("home: " + tel.getString("home"));
    System.out.println("mobile: " + tel.getString("mobile"));

    //배열 속성 정보 읽기
    //getJSONArray 배열얻는 메소드 사용
    JSONArray skill = root.getJSONArray("skill");
    System.out.print("skill: ");
    for(int i=0; i<skill.length(); i++) {
        System.out.print(skill.get(i) + ", ");
    }}}

19.7 TCP 채팅프로그램


1.서버가 다수의 클라이언트지원
2.받은걸 나도보고 상대한테도 보내줘야함.
3.상대방이 연결끊엇을때 알기

기능별로 하나씩 추가해나가면서 코드를 짠다.


사람 추가 제거하기 키값을 받고 맵에 추가 키값을 받고 맵에서 제거하기
채팅 SocketClient.chatName + @ + SocketClient.clientIP 다 달라야하기 때문에 IP를 추가해서 키값으로 구분하기
맵이 가진 사이즈수로 채팅방 사람수 표시 = 엔트리 수로 표시

메시지 보내는 sendToAll()메소드 작성
내용을 JSON에 담아서 보냄
chatRoom : Map
SocetClient에서 ChatClient에 바바박 보냄
SocetClient의 밸류만 얻어서 컬렉션 타입을 얻어서 보내기
sender에겐 메시지가 안감

서버종료
1.소켓닫기 2.스레드풀닫기 3.채팅룸 밸류 얻어서 각각 닫기

SocketClient 만들기 서버측과 통신하는 클라이언트
소켓으로부터 인풋스트림 아웃스트림 얻어서 필드에 넣기
클라이언트의 IP얻기
receive()메소드 주고받고 하는 일을 하는 메소드
receive()메소드는 클라이언트가 보낸 JSON메시지를 읽는 역할을한다. dis.readUTF로 JSON을 읽고 파싱해서 command값을 먼저 얻어낸다.
command가 incoming이라면 대화명을 읽고 챗룸에 SocketClinet를 추가
command가 message라면 JSON에서 메시지를 읽고 연결되어 있는 모등 클라이언트에게 보내기
클라이언트가 채팅종료하면 dis.readUTF 가 대기중에 연결이 끊어져 예외가 발생ㅎ아기 때문에 예외처리에서 클라이언트를 제거하는 메소드 실행하기

send()메소드 작성하기
send()메소드는 JSOM메시지를 보내는 역할을한다. ChatServer의 sendToAll()메소드에서 호출된다.

채팅클라이언트
채팅 클라이언트는 ChatClient 단일클래스로 1.채팅서버로 연결을 요청하고 연결된 후에는 제일먼저 대화명을 보낸다. 그리고 난다음 서버와 메시지를 주고받는다.
필드 소켓 인풋스트림 아웃풋스트림 챗이름

연결할때 호출되는 connect()메소드 만들기
채팅서버에 연결 요청을하고 Socket필드에 저장한다.
문자열입출력을 위해 DataInputStream DataOutputStream을 생성해서 필드에 저장한다.

데이터 받을 receive()메소드
서버가 보낸 JSON메시지를 읽는 역할을 한다. dis.readUTF()로 JSOM을 읽고 파싱해서 clientIpm chatName, message 를 얻어낸다.
그리고 콘솔뷰에 <chatName@clientIp> message로 출력한다.
역시 서버와 통신이끊어지면 예외가발생하니 예외처리를 해서 클라이언트도 종료도되록하기

나도 보내야하니 send()메소드 작성하기

메인메소드
1.q가 입력되엇을때 채팅을 종료하기 2.connect()호출해서 서버와 연결하기 3.연결되면 대화명을 입력하기
1.들어갈때 메시지 + 2.다른참가자에게 메시지 보내기

package ch19.sec07;

import java.io.IOException;
import java.net.*;
import java.util.*;
import org.json.JSONObject;

public class ChatServer {
//필드
ServerSocket serverSocket;
ExecutorService threadPool = Executors.newFixedThreadPool(100);
//맵타입의 컬렉션 셍성 간단하게 멀티스레드환경이기때문에 hashtable사용해도됨. <클라이언트 식별 문자, 클라이언트와 통신하는        역할>
//synchronizedMap동기화 안된걸 동기화 시킨 맵으로 만들기 사용
Map<String, SocketClient> chatRoom = Collections.synchronizedMap(new HashMap<>());
// Map<String, SocketClient> chatRoom = new Hashtable<>();

//서버 시작 메소드
public void start() throws Exception {
    serverSocket = new ServerSocket(50001);
    System.out.println("[서버] 시작됨");

    Thread thread = new Thread(() -> {
        try {
            //클라이언트가 연결요청하면 연결수락하고 socket 연결객체를 만듬.
            //그런데 바로 사용하는 것이아니라 SocketClient 내부에서 사용하려고함. 
            // SocketClient 객체를 만들때 서버에대한 참조와 통신객체의 참조를 넘겨서 사용하기
            while (true) {
            Socket socket = serverSocket.accept();
            //this = 객체 자신 ChatServer 람다식에서 this를하면 익명객체가아닌 이것을 실행하는 객체
            //ChatServer.this꼭의미하는 것을 나타내기 위해 이렇게 표시
            SocketClient sc = new SocketClient (ChatServer.this, socket);
            }

        } catch (Exception e) {

        }
    } );
    thread.start();
}
public void addSocketClient(SocketClient socketClient ) {
    String key = socketClient.chatName + "@" + socketClient.clientIp;
    chatRoom.put(key, socketClient);
    System.out.println("입장: " + key);
    System.out.println("현재 채팅자 수:" + chatRoom.size() + "\n");
}

public void removeSocketClient(SocketClient socketClient ) {
    String key = socketClient.chatName + "@" + socketClient.clientIp;
    chatRoom.remove(key, socketClient);
    System.out.println("나감: " + key);
    System.out.println("현재 채팅자 수:" + chatRoom.size() + "\n");
}

//모든 클라이언트에게 메시지 보내는 메소드
// 제이슨에 담아서 보냄
public void sendToAll(SocketClient sender, String message) {
    JSONObject root = new JSONObject();
    root.put("clientIp", sender.clientIp);
    root.put("chatName", sender.chatName);
    root.put("message", message);
    String json = root.toString();

    Collection<SocketClient> socketClient = chatRoom.values();
    for(SocketClient sc : socketClient) {
        if(sc == sender) continue;
        sc.send(json);
    }
}

public void stop(){
    try {
        serverSocket.close();
        threadPool.shutdownNow();
        //각각 다 닫아줘야함.
        /*
        Collection<SocketClient> socketClient = chatRoom.values();
        for(SocketClient sc : socketClient) {
            sc.close();
        }
        */
        chatRoom.values().stream().forEach(sc -> sc.close());
        System.out.println("[서버] 종료됨");
    } catch (IOException e) {
    }


}
//실제 실행하는 메인 메소드
public static void main(String[] args) {
    try {
        ChatServer chatServer = new ChatServer();
        chatServer.start();

        System.out.println("-------------------------------------------------------");
        System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter 키를 입력하세요.");
        System.out.println("-------------------------------------------------------");


        //키보드 입력
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String key = scanner.nextLine();
            if(key.toLowerCase().equals("q")) { 
                break;
            }
        }
        scanner.close();

        //TCP서버 종료
        chatServer.stop();


    }catch (Exception e) {
        System.out.println("[서버]" + e.getMessage());
    }
}
}

package ch19.sec07;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.json.JSONObject;

public class SocketClient {
//필드
ChatServer chatServer;
Socket socket;
String chatName;
String clientIp;
DataInputStream dis;
DataOutputStream dos;

//생성자
public SocketClient(ChatServer chatServer, Socket socket) {
    try {
        this.chatServer = chatServer;
        this.socket = socket;
        this.dis = new DataInputStream(socket.getInputStream());
        this.dos = new DataOutputStream(socket.getOutputStream());
        //클라이언트의 ip얻기
        InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress(); 
        this.clientIp = isa.getHostName();
        receive();
    } catch (IOException e) {
    }
}

//클라이언트 메시지 받기
private void receive() {
    chatServer.threadPool.execute(() -> {
        try {
        while(true) {
            //{"command": "incoming", "data": "chatName"}
            //{"command": "message", "data": "xxxx"}
            String receiveJson = dis.readUTF();

            JSONObject jsonObject = new JSONObject (receiveJson);
            String command = jsonObject.getString("command");

            switch(command) {
                case "incoming":
                    this.chatName = jsonObject.getString("data");
                    chatServer.sendToAll(this, "들어오셨습니다."); //여기서 this 람다식이니 
                    chatServer.addSocketClient(this);
                    break;
                case "message":
                    String message = jsonObject.getString("data");
                    chatServer.sendToAll(this, message); //여기서 this 람다식이니 
                    chatServer.addSocketClient(this);
                    break;
            }
        } 
    } catch(Exception e) {
        //연결준비중에 끊겨버리면 readUTF예외발생함. 클라이언트가 종료해버리면 발생
        chatServer.sendToAll(this, "나가셨습니다.");
        chatServer.removeSocketClient(this);
    }
    });
}
//클라이언트에게 메시지 보내기
public void send(String json) {
    try {
        dos.writeUTF(json);
        dos.flush();
    } catch (IOException e) {
    }
}

void close() {
    try {
        socket.close();
    } catch (IOException e) {
        }
}
}

 package ch19.sec07;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

import org.json.JSONObject;

public class ChatClient {
// 필드
Socket socket;
DataInputStream dis;
DataOutputStream dos;
String chatName;

//서버연결 메소드
public void connect() throws Exception {
    socket = new Socket("localhost", 50001);
    dis = new DataInputStream(socket.getInputStream());
    dos = new DataOutputStream(socket.getOutputStream());
    System.out.println("클라이언트 서버에 연결됨");
}

//데이터 받기 메소드
public void receive() {
    Thread thread = new Thread(()-> {
        try {
        while(true) {
                String json = dis.readUTF();
                JSONObject root = new JSONObject(json);
                String clientIp = root.getString("clientIp");
                String chatName = root.getString("chatName");
                String message = root.getString("message");
                System.out.println("<" + chatName + "@" + clientIp + "> " + message);
                }
            } catch (IOException e) {
                System.out.println("[클라이언트] 서버 연결 끊기");
                System.exit(0);
        }
    });
    thread.start();
}

public void send(String json) throws IOException {
    dos.writeUTF(json);
    dos.flush();
}

public void unconnect() throws IOException {
    socket.close();
}

public static void main(String[] args) {
    try {
        ChatClient chatClient = new ChatClient();
        chatClient.connect();

        Scanner scanner = new Scanner(System.in);
        System.out.print("대화명 입력:  ");
        chatClient.chatName = scanner.nextLine();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("command", "incoming");
        jsonObject.put("data", chatClient.chatName);
        String json = jsonObject.toString();
        chatClient.send(json);

        chatClient.receive();

        System.out.println("-------------------------------------------------------");
        System.out.println("보낼 메시지를 입력하고 Enter");
        System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter");
        System.out.println("-------------------------------------------------------");

        while(true) {
            String message = scanner.nextLine();
            if (message.toLowerCase().equals("q")) break;
            else {
                jsonObject = new JSONObject();
                jsonObject.put("command", "message");
                jsonObject.put("data", message);
                json = jsonObject.toString();
                chatClient.send(json);
            }

        }
        scanner.close();
        chatClient.unconnect();

    } catch (Exception e) {
        System.out.println("[클라이언트] 서버 연결 안됨");
    }
    System.out.println("[클라이언트] 종료");
    }
}

2022.12.10

이전과는 전혀 채팅 프로그램을 만들었다.
JSON을 보낼때의 방법 등이 이해가 잘 안됬다.
코드를 리뷰하면서 왜 쓰이는지는 알게 되었지만 혼자 짜야한다고 생각하면
힘들 것 같다.
그렇지만 하나씩 진도를 나아가며 다시 돌아왔을 땐 할 수 있을 것이다.
여러개가 나뉘어있을 뿐 결국 내용은 같기 때문이다.
발전을 위해 나아가야한다.