#19. 네트워크 입출력

19.5 서버의 동시 요청 처리

일반적으로 서버는 다수의 클라이언트와 통신을 핟나.
서버는 클라이언트들로부터 동시에 요청을 받아 처리하고 처리결과를 개별 클라이언트로 보내줘야한다.
이전 예제들은 먼저 연결한 클라이언트의 요청을 처리한 후 다음 클라이언트의 요청을 처리하도록 되어있다.

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

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

19.5.1 TCP EchoSercer동시요청처리

10개의 스레드로 요청을 처리하는 스레드풀 생성 후 작업처리 부분을 넣는다.

private static ExecutorService executorService = Executors.newFixedThreadPool(10);

executorService.execute(() -> {
    try {
        // 연결된 클라이언트 정보얻기
        InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
        System.out.println("[서버]" + isa.getHostName() + "의 연결 요청을 수락함");

        // -------------------------------------------------------
        // 데이터 받기
        InputStream is = socket.getInputStream();
        byte[] receivebytes = new byte[1024]; // 1kbyte
        int readByteCount = is.read(receivebytes);
        String message = new String(receivebytes, 0, readByteCount, "UTF-8");

        // 데이터 보내기
        OutputStream os = socket.getOutputStream();
        byte[] sendbyte = message.getBytes("UTF-8");
        os.write(sendbyte);
        os.flush();
        System.out.println("[서버] 받은데이터를 다시 보냄: " + message);

        // 연결끊기

        socket.close();
        System.out.println("[서버]" + isa.getHostName() + "의 연결을 끊음");
    } catch (IOException e) {
        System.out.println("[서버]" + e.getMessage());
    }
});

19.6 JSON데이터 형식

네트워크로 전달하는 데이터가 복잡할수록 구조화된 형식이 필요하다.
네트워크 통신에서 가장많이 사용되는 데이터 형식은 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을 문자열로 직접 작성할 수 있지만 대부분은 라이브러리를 이용해서 사용한다.

다음은 JSON표기법과 관련된 클래스들이다.

JSONObject | JSON객체 표기를 생성하거나 파싱할때 사용
JSONArray | JSON배열 표기를 생성하거나 파싱할때 사용

JSON을 만들기도하고 JSON을 해석해서 데이터를 얻는데도 사용한다.

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);

        //객체 속성추가
        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);

        //파일로저장
        Writer writer = new FileWriter("C:/Temp/member.json", Charset.forName("UTF-8"));
        writer.write(json);
        writer.flush();
        writer.close();
    }
}

->출력결과

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

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

다음은 member.json파일을 읽고 JSON을 파싱하는 방법을 보여준다.
db에서 값을 얻어와서 getString등등하는 거랑 같다.

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 root = new JSONObject(json);

        // 속성정보읽기
        System.out.println("id: " + root.getString("id"));
        System.out.println("name: " + root.getString("name"));
        System.out.println("age: " + root.getInt("age"));
        System.out.println("student: " + root.getBoolean("student"));

        // 객체 속성 정보 읽기
        JSONObject tel = root.getJSONObject("tel");
        System.out.println("home: " + tel.getString("home"));
        System.out.println("mobile: " + tel.getString("mobile"));

        // 배열 속성정보얻기
        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채팅 프로그램

TCP네트워킹을 이용해서 채팅서버와 클라이언트를 구현해보자.
ChatServer | 채팅 서버 실행클래스 ServerSocket을 생성하고 50001에 바인딩 ChatClinet 연결 수락 후 SocketClient생성
SocketClient | ChatClient와 1대1 통신(Socket)
ChatClinet | 채팅클라이언트 실행 클래스 ChatServer에 연결요청 SocketClient와 1대1로 통신

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

기능별로 하나씩 추가해나가면된다.

19.7.1 채팅서버

1. 필드선언

서버소켓, 스레드풀
Map<String, SocketClient> chatRoom = Collections.synchronizedMap(new HashMap<>());
chatRoom이라는 맵에 클라이언트 식별 문자를 키로 SocketClient 클라이언트와 통신하는역할 객체가 들어가있음.
내부의 소켓으로 클라이언트와 통신
클라이언트별로 통신하는 소켓을 맵에서 관리하겠다.
<클라이언트 식별 문자, 클라이언트와 통신하는역할>

19.7.1 필드

서버소켓, 스레드풀
Map<String, SocketClient> chatRoom = new Hashtable<>();
맵타입의 컬렉션 생성 멀티스레드환경이라 hashtable사용
내부의 소켓으로 클라이언트와 통신
클라이언트별로 통신하는 소켓을 Map으로 관리한다.
<클라이언트 식별 문자, 클라이언트와 통신하는역할>

ServerSocket serverSocket;
ExecutorService threadPool = Executors.newFixedThreadPool(100);
Map<String, SocketClient> chatRoom = new Hashtable<>();

19.7.2 서버 시작 메소드 작성

포트 바인딩
스레드만드는데 runnable객체 구현해서 thread클래스로부터 직접만들기
생성자 매개값으로 runnable객체를 람다식으로 제공함.
SocketClient가 ChatServer에서 만들어진 class를 사용하기 위해서 생성자 만들기
SocketClient내부에서 ChatServer객체 Socket객체를 사용하기때문임
SocketClient 필드에 저장해놓고 쓰겟다.
socket을 바로사용하는게 아니라 SocketClient라는 객체를 만들어서 이 내부사용하겠다.
그래서 SocketClient을 만들때 ChatServer에 대한 참조와 통신객체에 대한 참조를 넘겨 사용하겟다.

accept으로 연결수락하고 통신용 SocketClient를 반복해서 생성한다.

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

    Thread thread = new Thread(() -> {
        try {
            while (true) {
                Socket socket = serverSocket.accept();
                SocketClient sc = new SocketClient(this, socket);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
    thread.start();
}

19.7.3 서버에 인원 추가 제거 메소드 작성

채팅방안에는 많은 사람이 이 있음.
SocketClient가 chatClient를 대변하는 통신객체이다.
클라이어트만큼 소켓클라이언트를 만들어줘야함. (소켓끼리 1대1로 소통하기때문)
얘를 챗서버에서 관리를 해야한다.
이것들을 맵타입인 chatRoom에서 관리를 한다.

위에서 생성된 sc를 넣는거이다.
이sc의 chatName clientIp를 키로 사용

19.7.3.1 addSocketClient메소드

사람 추가하기 키값을 받고 맵에 추가한다.
매개변수는 start()에서 생성한 객체를 넣음
입장시 키 출력, 엔트리수 출력해주기
채팅 SocketClient.chatName + @ + SocketClient.clientIP
다 달라야하기 때문에 IP를 추가해서 키값으로 구분하기
맵이 가진 사이즈수로 채팅방 사람수 표시 = 엔트리 수로 표시

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");
}

19.7.3.2 removeSocketClient 메소드

키값을 받고 맵에서 제거하기
키값은 똑같이 받고 map에서 제거해주면된다.

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

19.7.4 sendToAll()메소드

모든 클라이언트에게 메시지 보내는 sendToAll()메소드 작성해보자.
1.보낼메시지 만들기 2.보낼사람들 정하고 보내기

매개변수에는 SocketClient에서 보낸사람, 뭐를 message를 내용을 JSON에 담아서 보냄
chatRoom : Map
저장된 모든 값을 담는 map의 value()메소드 사용
리턴타입은 Collection<T>인데
Map<String, SocketClient> 애초의 맵값이 SocketClient엿음.
밸류값만 뽑은거니 SocketClient를 넣어줌

이후 for (SocketClient sc : socketCleints)
반복문에서 Map의 밸류들 즉, 각 SocketClient 객체들을 얻어서
send()메소드 사용해서 보냄 SocketClient == ChatClient각 1대1대응이니 각각에게 다보내는것임.
socketCleints를 하나씩 받아서 sc에 보내는 것임.

SocetClient에서 ChatClient에 바바박 보냄
SocetClient의 밸류만 얻어서 컬렉션 타입을 얻어서 보내기

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> socketClients = chatRoom.values();
    for (SocketClient sc : socketClients) {
        if(sc==sender) {
            continue;
        }
        sc.send(json);
    }
}

19.7.5 서버종료

1.소켓닫기 2.스레드풀닫기 3.채팅룸 밸류 얻어서 = SocketClient 각각 닫기
chatRoom.values().stream().forEach(sc -> sc.close());
흘러오는 객체 chatRoom.values = SocketClient
SocketClient의 close메소드를 호출해서 닫는다.

public void stop() {
    try {
        serverSocket.close();
        threadPool.shutdownNow();
        chatRoom.values().stream().forEach(sc -> sc.close());
        System.out.println("[서버] 종료됨");
    } catch (IOException e) {
        System.out.println("[서버]" + e.getMessage());
    }
}

19.7.6 메인메소드

메소드 시작 q누르면 종료

public static void main(String[] args) throws IOException {
ChatServer chatServer = new ChatServer();
// TCP서버시작
chatServer.start();

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

// 키보드 입력
Scanner sc = new Scanner(System.in);
while (true) {
    String key = sc.nextLine();
    if (key.toLowerCase().equals("q")) {
        break;
    }
}
sc.close();
// TCP서버종료
chatServer.stop();

}

19.7.7 SocketClient 필드

클라이언트와 통신하는 소켓
1.소켓으로부터 인풋스트림 아웃스트림 얻어서 필드에 넣기
2.클라이언트의 IP얻기
Socket socket = serverSocket.accept();
socket은 accept햇기때문에 클라이언트의 정보를 가지고있음.
이 socket을 생성자에 대입했기때문에 알수잇음.
ChatServer 에서 SocketClient를 생성자 호출해서 객체를 생성하면서
serverSocket.accept();이 accept한거를 socket로 넣어서 다시 SocketClient에 넣어줌

통신용 객체 생성하면서 receive()메소드도 같이 시작

public class SocketClient {
    //필드
    ChatServer chatServer;
    Socket socket;

    DataInputStream dis;
    DataOutputStream dos;

    String chatName;
    String clientIp;

    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());
            InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
            this.clientIp = isa.getHostName();
            receive();
        } catch (IOException e) {
            System.out.println("[서버]" + e.getMessage());
        }
    }

19.7.8 receive()메소드 주고받고 하는 일을 하는 메소드

SocketClient의 Socket에서 주고받고를 해야하는데 여기서 받는게 receive()메소드이다.
클라이언트가 언제 보낼지 모르니 항상 받을 준비를 해야해서 스레드를 이용하는데 스레드풀로 추가될때마다 받는다.
receive()메소드는 클라이언트가 보낸 JSON메시지를 읽는 역할을한다.

dis.readUTF로 JSON을 읽고 파싱해서 command값을 먼저 얻어낸다.
클라이언트가 보내느 데이터 형태가 이런식 일것임. {"command": "xxx", ...}
명령 command가 뭘 하고싶은지를 파악하고 하고싶은 내용을 처리한다.

command가 incoming이라면 대화명을 읽고 챗룸에 SocketClinet를 추가
들어오려면 chatName을 제공해야함. 파싱한 JsonObject에 속성은 data인 chatName을 받음

들어왔으니 sendToAll메소드 실행 sendToAll(this(SocketClient), message)니까 이 소켓클라이언트, 들어왔습니다.
그리고 chatServer.addSocketClient(this);해서 추가해주기

들어올때 json에 chatName을 넣어서 보내주고 socket생성시에 각종 정보를 얻어서 key에 넣고
값에 이 SocketClient를 넣어서 객체가 생성됨

command가 message라면 JSON에서 메시지를 읽고 연결되어 있는 모든 클라이언트에게 보내기
클라이언트가 채팅종료하면 dis.readUTF 가 대기중에 클라이언트의 연결이 끊어져
예외가 발생하기 때문에 예외처리에서 클라이언트를 제거하는 메소드 실행하기
누가 나간지 알게 sendToAll()과 removeSocketClient()메소드 실행해주자.

public void receive() {
    // 필드에 챗서버 객체를 만들엇으니 가능
    chatServer.threadPool.execute(() -> {
        try {
            while (true) {
                String receiveJson = dis.readUTF();

                // JSON파싱
                JSONObject jsonObject = new JSONObject(receiveJson);
                String command = jsonObject.getString("command");

                switch (command) {
                case "incoming": {
                    this.chatName = jsonObject.getString("data");
                    chatServer.sendToAll(this, "들어오셨습니다.");
                    chatServer.addSocketClient(this);
                    break;
                }
                case "message": {
                    String message = jsonObject.getString("data");
                    chatServer.sendToAll(this, message);
                    break;
                }
                }
            }
        } catch (Exception e) {
            chatServer.sendToAll(this, "나가셨습니다.");
            chatServer.removeSocketClient(this);
        }
    });
}

19.7.9 send()메소드 작성하기

클라이언트로 보내는 것
send()메소드는 JSOM메시지를 보내는 역할을한다.
ChatServer의 sendToAll()메소드에서 호출된다.
sendToAll에서 작성된 json을 담아 보내는거임

public void send(String json) {
    try {
        dos.writeUTF(json);
        dos.flush();
    } catch (IOException e) {
    }
}

19.7.10 연결종료

socket을 닫는다.

//연결 종료
public void close() {
    try {
        socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

19.8 채팅클라이언트

채팅 클라이언트는 ChatClient 단일클래스로
1.채팅서버로 연결을 요청하고 연결된 후에는
2.제일먼저 대화명을 보낸다.
3.그리고 난다음 서버와 메시지를 주고받는다.

19.8.1 필드선언

필드 Socket DataInputStream DataOutputStream chatName

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

19.8.2 연결할때 호출되는 connect()메소드 만들기

socket 서버와 연결 서버 ip와 포트번호 제공
채팅서버에 연결 요청을하고 Socket필드에 저장한다.
문자열입출력을 위해 DataInputStream DataOutputStream을 생성해서 필드에 저장한다.

public void connent() {
    try {
        socket = new Socket("localhost", 50001);
        dis = new DataInputStream(socket.getInputStream());
        dos = new DataOutputStream(socket.getOutputStream());
        System.out.println("[클라이언트] 서버에 연결됨");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

19.8.3 데이터 받을 receive()메소드

서버가 언제 데이터를 보낼지 모르니 항상 켜져있어야함 스레드 필요.
단일클래스니 하나의 작업 스레드만 만들기 스레드풀 사용x

1.서버의 sendToAll의 Json을 받는거니 이것을 파싱하기
서버가 보낸 JSON메시지를 읽는 역할을 한다.
dis.readUTF()로 JSON을 읽고 파싱해서 clientIp, chatName, message 를 얻어낸다.
그리고 콘솔뷰에 <chatName@clientIp> message로 출력한다.
계속반복적으로 읽고 출력 읽고 출력하기
역시 서버와 통신이끊어지면 예외가발생하니
예외처리를 해서 클라이언트도 종료도되록하기
System.exit(0);으로 프로그램 자체 종료해버리기

public void receive() {
    Thread thread = new Thread(()-> {
        try {
            while (true) {
                //문자읽어오기
                String json = dis.readUTF();
                //json파싱
                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 (Exception e){
            System.out.println("[클라이언트] 서버 연결 끊김");
            System.exit(0);
        }
    });
    thread.start();
}

19.8.4. send()메소드 작성하기

json을 보낸다.
main메소드에서 키보드로 입력한 메시지를 보낼때 호출된다.

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

19.8.5 unconnect()메소드

Socket.close()를 이용해서 서버와 연결을끊는다. 메인에서 q누르면 끊게한다.

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

19.8.6 main메소드

1.connect()메소드 호출해서 서버와 연결하기
2.대화명입력받기

Scanner sc= new Scanner(System.in);
System.out.println("대화명 입력");
chatClient.chatName = sc.nextLine();

3.Json객체이 입력받은 대화명을 담아서 보내기
처음이니 command incoming으로 보냄.

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

4.메시지를 json에 담아서 보내기

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

서버연결 -> recieve로 받을준비 & 메시지로 담아서 보내기

20023.03.10 예외

클라이언트 측에서 받을때 오타가 잇어서 json을 못받아서 예외가 발생햇엇다.
json은 예민하기때문에 잘 해야한다는 점을 다시깨달앗다.
String clientIp = root.getString("cleintIp");
->
String clientIp = root.getString("clientIp");

'기초단계 > JAVA' 카테고리의 다른 글

2023.03.13 Java 복습  (0) 2023.03.13
2023.03.10 Java 복습  (0) 2023.03.10
2023.03.07 Java 복습  (0) 2023.03.07
2023.03.03 Java 복습  (0) 2023.03.06
2023.02.28 -2 Java 복습  (0) 2023.02.28

+ Recent posts