본문 바로가기

Web Sever 개발과 CS 기초/자바

Java Chatiing Server 만들기

관련 내용

JSON 개념과 특징 이해

Enum의 개념과 활용 방법 이해

개요 목적

Runnable Thread와 Java ServerSocket 클래스를 사용해서, 여러명이 접속할 수 있는 채팅 서버를 만들어보자.

Thread, ThreadLocal, ReentrantLock을 사용해서 하나의 Server 프로세스에 여러 Client 동작이 동시에 진행되어도 동시성 문제 없이 잘 돌아가는 채팅 서비스를 만드는 것이 목적이다. 

Server에서 구현할 동작들을 소개한다.

  1. 여러 Client가 접속할 수 있는 채팅 서버 생성
  2. Client 입장 시 이름을 먼저 등록해야 채팅 메세지 작성 가능하도록 설정
  3. 프로토콜([Header : [길이][패킷종류]] - [Body])을 사용하여 Client 데이터 구분하기 (이름 등록, 메세지, 이미지파일)
  4. Client 메세지 채팅 서버 참여자에게 전부 보내기
  5. Client 종료 시 보낸 메세지, 받은 메세지 수 남아있는 참여자에게 보내기

여러 Client가 접속할 수 있는 서버 생성

Java Thread를 사용해 여러 명이 동시에 사용할 수 있는 채팅 서버를 만든다.

Server Socket을 열어 놓고, 해당 포트로 들어온 Client Socket을 Thread로 등록하여 개별 작동할 수 있도록 로직을 작성했다.

Server Socket 열기와 Client Socket Thread 등록

ServerSocekt을 통해서 5510 포트로 서버를 열고 while(true) 코딩으로 여러 Client Socket 접속을 허용한다.

개별 Client Socket을 쓰레드로 등록하여 동시에 메세지를 보내는 행동을 해도, 분리된 처리를 할 수 있다.

분리된 처리는 Sever에 모아져서 다른 Client에게 전달하는 등 채팅 서버 기능을 구현할 수 있다.

public class Server {
    public static void main(String[] args) throws IOException {
        //서버 소켓 열기
        ServerSocket serverSocket = new ServerSocket(5510);
        System.out.println(serverSocket + " Creation of Server socket");
        //while true를 통해 여러 소켓이 접속이 가능하도록 설정한다.
        while (true) {
            //소켓을 받아서
            Socket client = serverSocket.accept();
            //Clinet Socket들이 개별 작동할 수 있는 스레드 실행!!
            RunnableServer myServer = new RunnableServer(client);
            Thread severThread = new Thread(myServer);
            severThread.start();
        }
    }
}
public class RunnableServer implements Runnable {
    
    protected Socket sock;

    @Override
    public void run() {
        //Client가 전달하는 데이터를 어떻게 처리할 지 Runnable run()메소드에 구현
        //코드 생략 ...
    }
}

Client 접속하기

Client는 Java Socket을 통해 ServerSocket이 설정한 포트로 접속을 시도한다.

그리고 Socket으로 서버로부터 메세지를 받을 수 있도록 dataInputStream을 가진 Thread를 생성한다.

Client도 서버로 메세지를 전달하기 위해 Socket을 dataoutputStream을 구성한다.

public class Client {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            //local host 5510포트로 연결할 소켓 생성
            socket = new Socket("127.0.0.1", 5510);
            //참여자가 입력한 내용을 받을 수 있는 bufferedReader 생성
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

            //Socket으로 서버로부터 메세지를 받을 수 있는 dataInputStream  생성
            //  fromServer = sock.getInputStream();
            //  dataInputStream = new DataInputStream(fromServer);
            ServerHandler handler = new ServerHandler(socket);
            Thread receiveThread = new Thread(handler);
            receiveThread.start();
            
            //Client가 작성한 내용을 서버로 전달하기 위해
            //  bufferedReader 생성
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            //  dataOutputStream = new DataOutputStream(socket.getOutputStream());
            ClientService clientService = new ClientService(socket);
            //나머지 코드 생략...
        } catch (IOException ex) {
            System.out.println("Connection termination (" + ex + ")");
        } finally {
            try {
            } catch (IOException ex) {
            }
        }
    }
}

Client 입장 시 이름을 먼저 등록해야 채팅 메세지 작성 가능하도록 설정

Client는 채팅 서버에 입장할 대 먼저 이름을 등록해야 한다.

이름을 등록한 후에 채팅 서버에 참여할 수 있도록 설정한다.

1 Client 먼저 ResiterName 보내기

Client 소켓은 프로그래밍 시작하면 Resiter name이라는 메세지를 받게 되고,

이름 등록이라는 타입을 가진 헤더와 실제 데이터가 들어 있는 데이터를 만들어 socket과 연결된 dataoutputStream으로 전달한다.

public class Client {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1", 5510);
						
            //Client가 작성한 내용을 서버로 전달하기 위해
            //  bufferedReader, dataOutputStream 설정
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in))
            ClientService clientService = new ClientService(socket);

            //먼저 이름을 등록할 수 있게 "Register your name: " Client 화면에 제공
            System.out.print("Register your name: ");
	
            //bufferedReader로 받은 이름 데이터를 헤더와 바디로 구성해서
            //(데이터를 헤더와 바디로 구성하는 프로토콜은 아래 목차에서 설명한다)
            byte[] resisterNameJsonBytes = clientService.implementResisterNameJsonBytes(bufferedReader);
            byte[] resisterNameHeader = clientService.implementResisterNameHeader(resisterNameJsonBytes);
            // <코드연결1>서버로 전달
            clientService.sendResisterName(clientService.dataOutputStream, resisterNameHeader, resisterNameJsonBytes);
            while (true) {
                //그 이후 메세지나 사진을 보낼 수 있도록 while(true) 안에 코드 작성
                //코드 생략
            }
        } catch (IOException ex) {
        } finally {
        }
    }
}
public class ClientService {
    private ObjectMapper objectMapper = new ObjectMapper();
    private Socket socket;
    protected DataOutputStream dataOutputStream;
		
    //Client Socket으로 dataOutputStream 생성
    ClientService(Socket socket) throws IOException {
        this.socket = socket;
        this.dataOutputStream = new DataOutputStream(socket.getOutputStream());
    }
		
    //실제 이름 정보가 들어가는 메세지 바디 - 내용을 Json byte[]로 직렬화 구성
    public byte[] implementResisterNameJsonBytes(BufferedReader bufferedReader) throws IOException {
        String name = bufferedReader.readLine();
        ResisterNameMessageBodyDto resisterNameMessageBodyDto = new ResisterNameMessageBodyDto();
        resisterNameMessageBodyDto.setName(name);
        byte[] sendJsonBytes = objectMapper.writeValueAsBytes(resisterNameMessageBodyDto);
        return sendJsonBytes;
    }
		
    //실제 이름 정보 들어 있는 바디 데이터가
    //어떤 타입인지
    //길이는 얼마나 되는 지 메세지 헤더 구성
    //(메세지를 헤더와 바디로 구성하는 설정은 아래 목차에서 설명한다.)
    public byte[] implementResisterNameHeader(byte[] sendJsonBytes) {
        int type = Type.RESISTERNAME.getValue();
        HeaderConverter headerConverter = new HeaderConverter();
        headerConverter.encodeHeader(sendJsonBytes.length,type);
        byte[] clientHeader = headerConverter.bytesHeader;
        return clientHeader;
    }
	
    //<코드연결1>
    //socket으로 연결한 dataoutputStream으로 이름 데이터(헤더+바디) 전송
    public void sendResisterName
        (DataOutputStream dataOutputStream, byte[] resisterNameHeader, byte[] resisterNameJsonBytes)
        throws IOException {
        dataOutputStream.write(resisterNameHeader,0,resisterNameHeader.length);
        dataOutputStream.write(resisterNameJsonBytes, 0, resisterNameJsonBytes.length);
        dataOutputStream.flush();
    }
}

해당 프로젝트에서 사용한 Json에 대한 이해는 아래 블로그 글에서 확인할 수 있다.

JSON 필요성과 특징

Server에서 받은 name 데이터로 Client 이름 등록하기

Client 스레드에 등록되어 있는 name 변수에 들어온 이름 데이터를 분석해서 넣어준다.

public class RunnableServer implements Runnable {
    protected Socket sock;
    //Client Socket Thread 별 이름 변수 제공
    private String name = null;
    
    RunnableServer(Socket socket) {
        this.sock = socket;
    }

    @Override
    public void run() {
        RunnableServerService serverService = new RunnableServerService();
        threadLocalClientSendMessageNum.set(0);
        InputStream fromClient;
        DataInputStream dataInputStream;
        DataOutputStreamFactory dataOutputStreamFactory = new DataOutputStreamFactory();
        try {
            System.out.println(sock + ": is connected");
            while (true) {
                //while문으로 계속해서 해당 소켓에 대한 데이터를 받아들인다.

                //Client Socket으로 들어온 데이터를 받기 위해
                //dataInputStream  설정
                fromClient = sock.getInputStream();
                dataInputStream = new DataInputStream(fromClient);
							
                //dataInputStream으로 받은 메세지 헤더와 바디 데이터 획득
                byte[] header = serverService.recieveMessageHeaderFromClient(dataInputStream);
                byte[] messageBodyBytes = serverService.receiveMessageBodyFromClient(dataInputStream, header);
                //헤더에서 메세지 타입을 확인한 후에 이름데이터라면 이름 변수에 등록  
                Type messageType = serverService.readType(header);
                Type serverMessageType;
                switch (messageType) {
                    case RESISTERNAME:
                        this.name = serverService.resisterName(messageBodyBytes);
                        break;
                    case MESSAGETOSERVER:
                        //코드 생략 ...
                    case IMAGETOSERVER:
                        //코드 생략 ...
                }
            }
        } catch (IOException ex) {
        } finally {
        }
    }
}

Header와 Body로 구성된 형태의 프로토콜을 구현하여 다양한 종류와 길이의 데이터를 통신 (이름 등록, 문자 메세지, 이미지파일)

첫 번째 바이트 배열은 이름 정보가 들어 있는 데이터이고, 두 번째 바이트 배열은 채팅 메세지 정보가 들어 있는 데이터이다. 단순 바이트 배열로는 해당 데이터가 이름 등록을 위한 데이터 인지, 메세지 인지, 이미지 인지 서버입장에서 파악할 수 없다. 

그래서 다양한 길이와 종류의 데이터를 통신하려면 위 그림과 같이 서버와 클라이언트사 공통의 규칙 즉 프로토콜을 사용하여, 데이터를 포장하는 작업이 반드시 필요하다. 

채팅 서버에 사용한 프로토콜 형식은 [Header : [길이(4바이트)][메세지 종류(4바이트)]] - [Body] 이다. 먼저 고정된 8바이트 크기의 헤더 데이터를 교환한다. 헤더에는 실제 데이터가 담긴 바디의 길이와 메세지 종류 정보가 담겨 있다. 헤더에 명시된 길이만큼 데이터를 받아오고, 메시지 종류에 맞게 로직을 처리할 수 있다.

1. 프로토콜 형식에 맞춰 데이터 구성하는 방법

클라이언트쪽에서 이름 정보 데이터를 서버에 전달하기 위해 BufferedReader로 들어온 문자열을 프로토콜 형식에 맞는 데이터를 구성하는 방법을 알아보자. 

먼저 BufferedReader로 들어온 데이터를 objectMapper의 도움을 받아서 byte[]로 만든다.

public byte[] implementResisterNameJsonBytes(BufferedReader bufferedReader) throws IOException {
    String name = bufferedReader.readLine();
    ResisterNameMessageBodyDto resisterNameMessageBodyDto = new ResisterNameMessageBodyDto();
    resisterNameMessageBodyDto.setName(name);
    byte[] sendJsonBytes = objectMapper.writeValueAsBytes(resisterNameMessageBodyDto);
    return sendJsonBytes;
}

방금 만든 byte[] 배열의 길이와 데이터 형식(이름등록) 정보가 담긴 헤더 byte[]를 만든다

public byte[] implementResisterNameHeader(byte[] sendJsonBytes) {
    int type = Type.RESISTERNAME.getValue(); //int type = 1111;
    HeaderConverter headerConverter = new HeaderConverter();
    headerConverter.encodeHeader(sendJsonBytes.length,type);
    byte[] clientHeader = headerConverter.bytesHeader;
    return clientHeader;
}

정수인 type 정보와 메세지 길이 정보를 8바이트 배열로 만들기 위해 HeaderCoverter 클래스의 메소드를 사용한다.

right shift 연산자를 통해서 각 숫자 데이터를 4바이트로 형태로 변형하여 8바이트 헤더 데이터를 완성한다.

public void encodeHeader(int length, int type) {
    byte[] lengthBytes = intToByteArray(length);
    byte[] typeBytes = intToByteArray(type);
    bytesHeader[0] = lengthBytes[0];
    bytesHeader[1] = lengthBytes[1];
    bytesHeader[2] = lengthBytes[2];
    bytesHeader[3] = lengthBytes[3];
    bytesHeader[4] = typeBytes[0];
    bytesHeader[5] = typeBytes[1];
    bytesHeader[6] = typeBytes[2];
    bytesHeader[7] = typeBytes[3];
}

private static byte[] intToByteArray(int value) {
    byte[] byteArray = new byte[4];
    byteArray[0] = (byte)(value >> 24);
    byteArray[1] = (byte)(value >> 16);
    byteArray[2] = (byte)(value >> 8);
    byteArray[3] = (byte)(value);
    return byteArray;
}

2. 프로토콜 형식대로 데이터 분석하기

서버에서 프로토콜 형식대로 들어온 데이터를 분석하는 방법에 대해서 알아보자.

먼저 DataInpuStream을 통해서 고정된 8바이트 헤더 데이터를 획득한다.

public byte[] recieveMessageHeaderFromClient(DataInputStream dis) throws IOException {
    byte[] header = new byte[8];
    dis.readFully(header, 0, header.length);
    return header;
}

헤더에 명시된 바디 데이터 길이 만큼 DataInputStream을 통해 바디 데이터 배열을 획득할 수 있다.

public byte[] receiveMessageBodyFromClient(DataInputStream dis, byte[] header) throws IOException {
    HeaderConverter headerConverter = new HeaderConverter();
    headerConverter.decodeHeader(header);
    int length = headerConverter.messageLength;
    byte[] receiveBytes = new byte[length];
    dis.readFully(receiveBytes, 0, length);
    return receiveBytes;
}

8바이트 배열로 되어 있는 헤더 정보(길이, 메세지 종류)를 leftShift를 사용하여 정수형 형식의 데이터를 얻을 수 있다.

public void decodeHeader(byte[] header) {
    byte[] lengthBytes = Arrays.copyOfRange(header, 0, 4);
    byte[] typeBytes = Arrays.copyOfRange(header, 4, 8);
    messageLength = byteArrayToInt(lengthBytes);
    messageType = byteArrayToInt(typeBytes);
}
private static int byteArrayToInt(byte bytes[]) {
    return ((((int)bytes[0] & 0xff) << 24) |
        (((int)bytes[1] & 0xff) << 16) |
        (((int)bytes[2] & 0xff) << 8) |
        (((int)bytes[3] & 0xff)));
}

Client 메세지 채팅 서버 참여자에게 전부 보내기

Client가 작성한 메세지를 Server 채팅 방에서 참여한 모든 Client에게 보낸다.

Client에서 넘어온 메세지를 가공해 Server의 Client 해시 목록을 사용하여 전체에 보낼 수 있다.

Client가 메세지 보내기

Client 데이터 받아서 해당 데이터가 메시지라고 판단되면 Type과 길이 헤더에 담고 메세지 데이터 바디에 담아 전달한다.

public class Client {
    public static void main(String[] args) {
        Socket socket = null;
        try {
           //이름을 등록한 이후
            while (true) {
                //while문으로 넘어가 계속해서 메세지와 이미지 파일을 보낼 수 있도록 설정
                //1 clientService.storeInputStringAndSetType(bufferedReader) 메소드를 통해
                //데이터와 Type을 받게 된다.
                InputStringAndType inputStringAndType = clientService.storeInputStringAndSetType(bufferedReader);
                Type type = inputStringAndType.type;
                switch (type) {
                    case MESSAGETOSERVER :
	                    //타입이 메세지라는 것을 알게되면
                        //2 endStringMessage(clientService.dataOutputStream, inputStringAndType);
                        //를 통해 헤더(문자타입정보, 데이터길이) 와 메세지 바디를 서버로 전송
                        clientService.sendStringMessage(clientService.dataOutputStream, inputStringAndType);
                        break;
                    case IMAGETOSERVER:
                        clientService.sendImageMessage(clientService.dataOutputStream, inputStringAndType);
                        break;
                }
            }
        } catch (IOException ex) {
        } finally {
        }
    }

//1 2 상세 메소드 
public class ClientService {
    private ObjectMapper objectMapper = new ObjectMapper();
		//1	
    public InputStringAndType storeInputStringAndSetType(BufferedReader bufferedReader) throws IOException {
        String inputString = bufferedReader.readLine();
        Type type;
        //이미지 데이터가 아니라고 판단되면 Type.MESSAGETOSERVER 을
        //inputStringAndType에 담아준다.
        if (inputString.length() >= 8 && inputString.substring(0, 8).equals("image://")) {
            type = Type.IMAGETOSERVER;
        } else {
            type = Type.MESSAGETOSERVER;
        }
        InputStringAndType inputStringAndType = new InputStringAndType(inputString, type);
        return inputStringAndType;
    }
		
//2
		public void sendStringMessage(DataOutputStream dataOutputStream, InputStringAndType inputStringAndType)
        throws IOException {
        byte[] inputStringtobytes = inputStringAndType.inputString.getBytes("UTF-8");
        StringMessageBodyDto stringMessageBodyDto = new StringMessageBodyDto();
        stringMessageBodyDto.setStringMessageBytes(inputStringtobytes);
        //메세지를 byte[]를 담은 dto를 json으로 직렬화 바이트로 변경
        byte[] sendJsonBytes = objectMapper.writeValueAsBytes(stringMessageBodyDto);
        HeaderConverter headerConverter = new HeaderConverter();
        //헤더 컨버터를 이용해서 메세지 타입과 길이를 담은 헤더를 만들기
        headerConverter.encodeHeader(sendJsonBytes.length, inputStringAndType.type.getValue());
        byte[] header = headerConverter.bytesHeader;
        //dataOutputStream을 통해서 헤더를 먼저 보내고 바디를 그다음에 전송하기
        dataOutputStream.write(header, 0, header.length);
        dataOutputStream.write(sendJsonBytes, 0, sendJsonBytes.length);
        dataOutputStream.flush();
    }
}

Server 메세지 받아서 전체 Client에게 보내기

헤더에 타입과 길이를 분석해 메세지라고 판단되면 Client 해시 목록 전체에게 데이터를 보낸다.

public class RunnableServer implements Runnable {
    protected Socket sock;
   
    @Override
    public void run() {
        RunnableServerService serverService = new RunnableServerService();
        threadLocalClientSendMessageNum.set(0);
        InputStream fromClient;
        DataInputStream dataInputStream;
        DataOutputStreamFactory dataOutputStreamFactory = new DataOutputStreamFactory();
        try {
            System.out.println(sock + ": is connected");
            while (true) {
                //while문으로 계속해서 해당 소켓에 대한 데이터를 받아들인다.
                fromClient = sock.getInputStream();
                dataInputStream = new DataInputStream(fromClient);
                //dateinputStream으로 프로토콜대로 데이터를 헤더와 바디로 받기!!!
                byte[] header = serverService.recieveMessageHeaderFromClient(dataInputStream);
                byte[] messageBodyBytes = serverService.receiveMessageBodyFromClient(dataInputStream, header);
                Type messageType = serverService.readType(header);
                Type serverMessageType;
                switch (messageType) {
                    case RESISTERNAME:
                        //생략...
                    case MESSAGETOSERVER:
                        //메세지 타입인 것을 확인 했으면
                        //클라이언트에서 받은 데이터를 데이터 타입만 수정후에 헤더 바디 구성
                        byte[] stringMessageJsonBytes
                            = serverService.implementStringMessageJsonBytes(messageBodyBytes, this.name);
                        byte[] stringMessageServerHeader
                            = serverService.implementStringMessageServerHeaderBytes(stringMessageJsonBytes);
                        serverMessageType = serverService.checkMessageType(stringMessageServerHeader);
                        //1
                        //변환한 데이터를 모든 Client에게 보내기
                        serverService.broadcastAllUser(serverMessageType, clients, dataOutputStreamFactory
                            , sock, stringMessageJsonBytes, stringMessageServerHeader, lockForClientsConcurrency);
                        break;
                    case IMAGETOSERVER:
                        //생략...
                }
            }
        } catch (IOException ex) {
        } finally {
        }
    }
}

//1
public class RunnableServerService {

    public void broadcastAllUser(Type messageType, HashMap<Socket, Integer> clients
        , DataOutputStreamFactory dataOutputStreamFactory, Socket socket
        , byte[] sendJsonBytes, byte[] serverHeader, ReentrantLock lockForClientsConcurrency)
        throws IOException {
        if (messageType == Type.MESSAGETOCLIENT || messageType == Type.CLIENTCLOSEMESSAGE) {
            lockForClientsConcurrency.lock();
            try {
                //Client Socket을 저장해둔 static hash에서 모든 소켓들을 가져와
                //dataoutputStream을 통해 모든 Client에게 메세지를 보내게 된다.
                for (Socket client : clients.keySet()) {
                    DataOutputStream dataOutputStream = dataOutputStreamFactory.createDataOutputStream(client);
                    dataOutputStream.write(serverHeader, 0, serverHeader.length);
                    dataOutputStream.write(sendJsonBytes, 0, sendJsonBytes.length);
                    dataOutputStream.flush();
                }
            } finally {
                lockForClientsConcurrency.unlock();
            }
        } else if (messageType == Type.IMAGETOCLIENT) {
              //생략...
        }
    }
}

Client 종료 시 보낸 메세지, 받은 메세지 수 남아있는 참여자에게 보내기

서버에서 Client가 보낸 메세지 수와 받은 메세지 수 정보를 가지고 있다.

Client가 나가게 되면 계속 접속해 있는 유저들에게 Client가 나갔다는 메세지를 보낸다.

종료 메세지와 함께 나간 Client가 보낸 메세지 수와 받은 메세지 수를 함께 전송한다.

받은 메세지 수를 여러 스레드 상황에서 안전하게 보장하기 위해서 ReetrantLock과 HashMap을 사용하고 보낸 메세지 수를 안전하게 보장하기 위해서 ThreadLocal을 설정한다.

Server -Client가 받은 메세지 수 저장 후 전달

모든 client의 소켓과 받은 메세지 수 전부를 저장하는 스태틱 해시를 설정한다.

클라이언트에게 메세지나 사진이 오면, 해시의 리시브 넘을 모두 +1 증가한다.

public class RunnableServer implements Runnable {
    protected Socket sock;
    
    //Client 소켓과 받은 메세지 수를 저장하는 Integer 정보를 가진 
    //HashMap을 Static으로 설정한다.
    protected static HashMap<Socket, Integer> clients = new HashMap<>();

    private static ReentrantLock lockForClientsConcurrency = new ReentrantLock();

    public static  void clientReceiveMessageNumPlus1(Socket socket) {
        clients.put(socket, clients.getOrDefault(socket, 0) + 1);
    }
}

//해당 메소드를 호출하면 
//Lock을 걸어서 메세지가 오게 될 때, 순차적으로 해시 모든 값이 +1 할 수 있도록 처리한다.
//만약 Lock 없다면 값이 누락되는 상황이 올 수 있다.
public class RunnableServerService {
    public void treatReceiveNumPlus(Type messageType, HashMap<Socket, Integer> clients
        , Socket socket, ReentrantLock lockForClientsConcurrency) {
        if (messageType == Type.MESSAGETOCLIENT || messageType == Type.CLIENTCLOSEMESSAGE) {
            lockForClientsConcurrency.lock();
            try {
                for (Socket client : clients.keySet()) {
                    RunnableServer.clientReceiveMessageNumPlus1(client);
                }
            } finally {
                lockForClientsConcurrency.unlock();
            }
        } else if (messageType == Type.IMAGETOCLIENT) {
            lockForClientsConcurrency.lock();
            try {
                for (Socket clinet : clients.keySet()) {
                    if (clinet == socket) {
                        continue;
                    }
                    RunnableServer.clintReceiveMessageNumPlus1(clinet);
                }
            } finally {
                lockForClientsConcurrency.unlock();
            }
        }
    }
}

이제 위에서 만들어 놓은 메소드를 가지고 client에게 메세지나 이미지를 받게 되면 받은 메세지 수를 안전하게 증가한다.

그리고 Client가 종료하게 되면 해시에서 해당 클라이언트 정보를 가져온 후 나머지 접속자에게 받은 메세지 수를 보내준다.

public class RunnableServer implements Runnable {
    protected Socket sock;
    
    protected static HashMap<Socket, Integer> clients = new HashMap<>();

    @Override
    public void run() {
        threadLocalClientSendMessageNum.set(0);
        try {
            System.out.println(sock + ": is connected");
            while (true) {
                //while문으로 계속해서 해당 소켓에 대한 데이터를 받아들인다.
                //생략...
                switch (messageType) {
                    case RESISTERNAME:
                        //생략
                    case MESSAGETOSERVER:
                        //메세지나
                        serverService.treatReceiveNumPlus(serverMessageType, clients, sock, lockForClientsConcurrency);
                        serverService.broadcastAllUser(serverMessageType, clients, dataOutputStreamFactory
                            , sock, stringMessageJsonBytes, stringMessageServerHeader, lockForClientsConcurrency);
                        break;
                    case IMAGETOSERVER:
                        //이미지가 오게 되면 모든 접속자 받은 메세지 수를 1 증가한다.
                        serverService.treatReceiveNumPlus(serverMessageType, clients, sock, lockForClientsConcurrency);
                        serverService.broadcastAllUser(serverMessageType, clients, dataOutputStreamFactory
                            , sock, imageMessageJsonBytes, imageMessageServerHeader, lockForClientsConcurrency);

                        break;
                }
            }
        } catch (IOException ex) {
        } finally {
            try {
                //finally 절에서
                //소켓이 나가게 되면
                //해당 소켓을 해시에서 지우고
                //받은 메세지 수와 보낸 메세지수 정보를 헤더와 바디로 담은 후
                int clientRecieveMessageNum = clients.get(sock);
                removeClientInClients(sock);
                byte[] closeMessageJsonBytes
                    = serverService.implementCloseBody
                    (this.name, threadLocalClientSendMessageNum.get(), clientRecieveMessageNum);
                byte[] closeMessageServerHeader = serverService.implementCloseHeader(closeMessageJsonBytes);
                Type serverMessageType = serverService.checkMessageType(closeMessageServerHeader);
                //아직 접속해 있는 client에게 해당 정보를 보낸다. 
                serverService.broadcastAllUser(serverMessageType, clients, dataOutputStreamFactory,
                    sock, closeMessageJsonBytes, closeMessageServerHeader, lockForClientsConcurrency);
                serverService.treatReceiveNumPlus(serverMessageType, clients, sock, lockForClientsConcurrency);
            } catch (IOException ex) {
            }
        }
    }
}