본문 바로가기
IT 돌고래/Special JAVA

[Java]TCP 소켓통신 프로그램 만들기(1:N)

by IT돌고래 2020. 3. 13.
반응형

안녕하세요.

IT 돌고래입니다.

자바로 TCP의 소켓 1:N 통신이 되는 자바 프로그램을 만들어 보겠습니다.

 

※ 1:N통신을 이해하려면 이전에 썼던 TCP 소켓통신 프로그램 만들기(1:1)에 있는 TCP 통신의 이해를 확실히 이해를 해주시고 봐주세요.

 

1:N통신 프로그램을 만들려면 클라이언트 관리가 중요합니다. 서버에서 클라이언트를 잘 정리를 해놓고 그 이후에 임의 클라이언트에서 데이터가 오면 서버에서 그 데이터를 받아서 모든 클라이언트에게 송신을 해줘야 합니다. 

 

브로드 캐스팅

파란선 : 클라이언트에서 데이터를 송신

빨간선 : 클라이언트에서 받은 데이터를 모든 다른 클라이언트에 데이터를 송신

모든 클라이언트에게 송신하기 때문에 데이터를 보낸 클라이언트에게도 송신

 

소스 분석

 

MainServer

	int port = 7777; // 정수변수 port선언후 7777 대입
	ServerSocket server = null; // ServerSocket타입 server 선언 후 null 대입
	Socket child = null; // Socket타입 child 선언 후 null 대입

	HashMap<String, PrintWriter> hm; 
    //컬렉션인 HashMap타입의 키값을 String 값은 PrintWriter인 hm 변수 선언

	public MainServer() { //ChatServer 생성자

		ChatSverThread sr;
        //ChatServerThread타입에 sr 변수 선언
        //브로드 캐스팅을 하기위한 쓰레드 객체
		Thread t;
        //Thread 타입의 t 변수 선언

		try { //시도하다
			server = new ServerSocket( port ); //서버소켓을 생성해서 server 변수에 대입

			System.out.println( "**************************************" );//출력
			System.out.println( "*              채팅 서버                *" );//출력
			System.out.println( "**************************************" );//출력
			System.out.println( "클라이언트의 접속을 기다립니다." );//출력

			hm = new HashMap<String, PrintWriter>(); //hashMap객체를 생성해서 hm 변수에 대입

			while( true ) { // 무한 반복
				child = server.accept(); 
                //ServerSocket의 변수인 server를 이용하여 accept함수 호출을 하여
                //클라이언트 접속시까지 대기를 합니다.
                //접속시에는 클라이언트와 연결 됩니다.
                //클라이언트의 소켓을 연결받습니다.
				if( child != null ) { 
                //Socket타입에 변수인 child가 null 값이 아니면 실행
                //child에는 클라이언트 소켓과 연결을 할 수 있는 소켓입니다.
					sr = new ChatSverThread( child, hm ); 
                    //ChatSverThread 객체를 Socket과 HashMap을 받아서 생성 후에
                    //ChatSverThread의 변수인 sr에 대입
					t = new Thread(sr); 
                    //Thread객체를 ChatSverThread을 받아서 생성후 
                    //Thread의 변수인 t에 대입
					t.start();//쓰레드 시작
				}
			}
		}
		catch ( Exception e )	{ //예외처리가 발생하면 실행
			e.printStackTrace(); //예외처리 출력
		}
	}



	public static void main(String[] args) 
	{
		new MainServer(); //MainServer 객체 생성
	}

ChatSverThread(Server Broad Casting)

public class ChatSverThread implements Runnable

{
	Socket child; //Socket 클래스 타입의 변수 child 선언
	BufferedReader ois; // BufferReader 클래스 타입의 변수 ois 선언
	PrintWriter oos; // PrintWriter 클래스 타입의 변수 oos 선언

	String user_id; // 문자열 변수 user_id 선언
	HashMap<String/*유저 ID*/, PrintWriter/*유저들 PrintWriter*/> hm; // 접속자 관리
    //컬렉션 HashMap의 키값 String에 값PrintWriter의 변수 hm을 선언
	InetAddress ip; // InetAddress 클래스 타입의 변수 ip 선언
	String msg; // 문자열 변수 msg 선언

	public ChatSverThread(Socket s, HashMap<String, PrintWriter> h ) {
    ChatSverThread의 Socket과 HashMap을 인자로 받는 생성자
		child = s;
        //인자로 받은 변수 s를 child에 대입(Socket)
		hm = h;
        //인자로 받은 변수 h를 hm에 대입(HashMap)

		try	{
        //시도한다.
			ois = new BufferedReader( new InputStreamReader( child.getInputStream() ) );
            //BufferReader 객체를 생성시 InputStreamReader 객체로 인자를 받고
            //InputStreamReader 객체를 생성시에는 child(Socket)에 getInputStream()함수를 호출하면
            //InputStream을 리턴하여 인자로 받고 InputStreamReader 객체를 생성
            // BufferReader로 생성된 객체를 ois에 대입
			oos = new PrintWriter( child.getOutputStream() );
            //PrintWriter 객체를 생성시에는 child(Socket)에 getOutputStream()함수를 호출하면
            //OutputStream을 리턴하여 인자로 받고 PrintWriter 객체를 생성
            //PrintWriter로 생성된 객체를 oos에 대입

			user_id = ois.readLine();
            //ois의 readLine함수를 호출하여 한줄의 문자열을 읽어서 user_id에 대입
			ip = child.getInetAddress();
            //child(Socket)을 통해서 getInetAddress()함수를 통해서
            //Client IP 주소를 문자열로 받아 ip에 대입
			System.out.println( ip + "로부터 " + user_id + "님이 접속하였습니다." );//출력
			broadcast(user_id + "님이 접속하셨습니다.");
            //broadcast 함수 호출 호출을 할때 문자열 인자로 대입

			synchronized( hm ) {
            //임계영역 설정 함 
            //HashMap에 추가시 한 쓰레드만 들어와서 사용 가능함
				hm.put( user_id, oos );
                //HashMap에 키:user_id(String) 값: oos(PrintWriter)를 추가함
			}
		}

		catch (Exception e ) {//예외처리 발생시 실행
			e.printStackTrace();//예외 출력
		}

	}

	public void run() {
		String receiveData; // 문자열 변수 receiveDate 선언

		try//시도하다
		{
			while( (receiveData = ois.readLine()) != null ) {
            //ois의 readLine 함수를 호출하여 문자열 한줄 씩을 receiveDate에 대입을 하면
            //receiveDate가 null이 아니면 계속 반복
				//끝낼때
				if( receiveData.equals( "/quit" ) ) {
                //receiveDate가 /quit이면 아래 명령문 실행
					synchronized( hm ) {
                    //임계영역 설정 함
                     //HashMap에 삭제시 한 쓰레드만 들어와서 사용 가능함
						hm.remove( user_id );
                        //HashMap에 키값이 user_id인 것을 삭제하는 함수
					}

					break;//반복문 탈출
				}
				//귓속말
				else if( receiveData.indexOf( "/to" ) >= 0 ) {
                //receiveDate의 함수 indexOf를 이용한 문자열에 있는지를 탐색
                //만약에 있으면 0이상값을 주기 때문에 아래 명령어 실행
					sendMsg( receiveData );//sendMsg함수를 receiveData를 인자로 받아서 호출
				}
				//전체 메세지 보내기
				else {
                //위에 조건이 모두 아니면 아래명령문 실행
					System.out.println(user_id + " >> " + receiveData );//출력
					broadcast( user_id + " >> " + receiveData );
                    //broadcast 함수 호출 호출을 할때 문자열 인자로 대입
				}

			}

		}

		catch (Exception e ) {//예외처리 발생하면 실행
			e.printStackTrace();//예외처리 출력
		}

		finally {//위에 try catch 어떤상황이든 다끝나면 실행
			synchronized( hm ) {
            ////임계영역 설정 함 
            //HashMap에 삭제시 한 쓰레드만 들어와서 사용 가능함
				hm.remove( user_id );
               //HashMap에 키값이 user_id인 것을 삭제하는 함수  
			}

			broadcast( user_id + "님이 퇴장했습니다." );
             //broadcast 함수 호출 호출을 할때 문자열 인자로 대입
			System.out.println( user_id + "님이 퇴장했습니다." );//출력

			try//시도하다
			{
				if( child != null ) {
                //child(Socket)이 만약에 null이 아니면
					ois.close();
                    //BufferReader 객체 ois close()
					oos.close();
                    //PrintWriter 객체 oos close()
					child.close();
                    //Socket 객체 child close()
				}
			}

			catch ( Exception e) {}//예외처리 발생시
		}
	}



	public void broadcast(String message){
    // 리턴을 하지않고 문자열을 인자로 받는 broadcast 함수
		synchronized( hm ) {
        //임계영역 설정 함 
			try{
				for( PrintWriter oos : hm.values( )){
                //HashMap에서 값만 빼서 PrintWriter oos라는 변수에 대입을 한다.
                //null이 나오기전까지 계속 반복한다.
					oos.println( message ); 
                    //oos(PrintWriter)의 함수 println에 문자열(message)을 넣는다.
                    //message(문자열)이 PrintWriter에 담긴다.
					oos.flush();
                    //oos(PrintWriter)의 함수 flush()를 호출한다.
                    //flush함수를 호출하면 PrintWirter에 담겨있던 
                    //문자열을 연결된 Socket을 통해 전송하게 된다.
				}
                //반복을 하면서 모든 연결된 소켓에 문자열을 송신하게 된다.
			}catch(Exception e){ }//예외처리 발생시 실행
		}
	}


	public void sendMsg(String message){
    // 리턴을 하지 않고 문자열을 인자로 받는 sendMsg 함수
		int begin = message.indexOf(" ") + 1; 
        //처음 스페이스 그 다음 인덱스 숫자를 정수형 변수 begin에 대입
		int end   = message.indexOf(" ", begin);
        //begin에서 시작해 그 그다음 스페이스 있는 인덱스를 정수형 변수 end에 대입

		if(end != -1){
        //정수형 변수 end가 -1이 아니면 실행
			String id = message.substring(begin, end);
            //문자열 변수 message을 begin에서 end만큰 잘라서 문자열 변수 id에 대입
			String msg = message.substring(end+1);
            //문자열 변수 message을 end에서 1을 더한 인덱스부터 끝까지 있는 문자열을
            //문자열 변수 msg에 대입
			PrintWriter oos = hm.get(id);
            //PrintWriter의 변수 oos 에 HashMap에서 get 함수의 인자를 문자열 변수 id를 넣어서
            //Value이 PrintWriter을 빼서 oos에 대입

			try{//시도하다
				if(oos != null){
                //PrintWriter oos의 값이 널이 아니면 아래 명령문 실행
					oos.println( user_id + "님이 다음과 같은 귓속말을 보내셨습니다. : " + msg );
                    //oos(PrintWirter)에 println함수에 문자열을 담는다. 
					oos.flush();
                    //oos(PrintWriter)의 함수 flush()를 호출한다.
                    //flush함수를 호출하면 PrintWirter에 담겨있던 
                    //문자열을 연결된 Socket을 통해 전송하게 된다.
				}

			}catch(Exception e)//예외처리가 발생하면 실행
			{ 

			}
		}
	}

}

MainClient

public class  MainClient {
	String ipAddress; //문자열 변수 ipAdress 선언
	static final int port=7777; // static final 정수 변수 port 선언후 7777 초기화
	Socket client=null; // Socket 변수 client 선언 후 null 값 초기화
	BufferedReader read; // BufferReader 변수 read 선언
	PrintWriter oos; //PrintWriter 변수 oos 선언
	BufferedReader ois; //BufferReader 변수 ois 선언
	String sendData; // 문자열 변수 sendDate 선언
	String receiveData; // 문자열 변수 receiveDate 선언

	String user_id; // 문자열 변수 user_id 선언
	ReceiveDataThread rt; // ReceiveDateThread 변수 rt 선언
	boolean endflag=false; // 참거짓 변수 endflag 선언 후 false로 초기화

	public MainClient(String id, String ip) {
    //MainClient 생성자로 인자로 문자열 2개를 받는다.
		user_id=id; // user_id에 id 대입
		ipAddress=ip; // ipAddress에 ip 대입
		try{//시도하다.
			System.out.println("**** 클라이언트*****");//출력
			client = new Socket(ipAddress, port); 
            //client(Socket)에 Socket 객체를 생성하여 대입
            //Socket객체를 생성 할때 인자로 문자열 2개를 담아서 생성자 호출

			read= new BufferedReader(new InputStreamReader(System.in));
            //BufferReader 객체를 생성을 하는데 InputStrreamReader 객체를 만들어서 인자로 받는다.
            //InputStreamReader는 System.in을 인자로 받아서 객체를 만든다.
            //BufferReader객체가 생성되면 read에 대입
			ois = new BufferedReader( new InputStreamReader( client.getInputStream() ) );
            //BufferReader 객체를 생성시 InputStreamReader 객체로 인자를 받고
            //InputStreamReader 객체를 생성시에는 child(Socket)에 getInputStream()함수를 호출하면
            //InputStream을 리턴하여 인자로 받고 InputStreamReader 객체를 생성
            // BufferReader로 생성된 객체를 ois에 대입
			oos = new PrintWriter( client.getOutputStream() );
			//PrintWriter 객체를 생성시에는 child(Socket)에 getOutputStream()함수를 호출하면
            //OutputStream을 리턴하여 인자로 받고 PrintWriter 객체를 생성
            //PrintWriter로 생성된 객체를 oos에 대입
			oos.println( user_id );
            //oos(PrintWriter)의 println함수를 user_id(문자열)을 넣어서 호출
            //PrintWriter에 user_id(문자열)이 담긴다.
			oos.flush();
            //oos(PrintWriter)의 함수 flush()를 호출한다.
            //flush함수를 호출하면 PrintWirter에 담겨있던 
            //문자열을 연결된 Socket을 통해 전송하게 된다.

			rt= new ReceiveDataThread(client, ois);
            //ReceiveDataThread 객체를 생성할때 client(Socket)과 ois(BufferReader)를
            //인자로 넣어줘서 객체를 생성하고 rt에 대입
			Thread t = new Thread(rt);
            //Thread 객체를 생성할때 ReceiveDateThread를 인자로 넣어주고 생성 후에
            //Thread 변수 t에 대입
			t.start(); 
            //쓰레드 시작

			while(true){
            //무한 반복
				sendData = read.readLine();
                //키보드 입력을 받아서 sendData에 대입

				oos.println( sendData );
               //oos(PrintWriter)의 println함수를 sendData(문자열)을 넣어서 호출
               //PrintWriter에 sendDate(문자열)이 담긴다.
				oos.flush();
                //oos(PrintWriter)의 함수 flush()를 호출한다.
            	//flush함수를 호출하면 PrintWirter에 담겨있던 
            	//문자열을 연결된 Socket을 통해 전송하게 된다.

				if(sendData.equals( "/quit") ) { 
                //sendData(문자열)dl /quit이면 아래 명령어 실행
					endflag = true; //참거짓 변수 endflag에 true를 대입
					break;// 반복문 탈출
				}
			}
			System.out.print("클라이트의 접속을 종료합니다. ");//출력
			System.exit( 0 );//프로그램 종료
		} catch(Exception e){  //예외처리시 실행
			if(!endflag) e.printStackTrace();//예외처리시 출력
		}
		finally{
        //위에 try catch 어떤상황이든 다끝나면 실행
			try{//시도하다.
				ois.close();
                //BufferReader 객체 ois close()
				oos.close();
                //PrintWriter 객체 oos close()
				client.close();
                //Socket 객체 client close()
                
				System.exit(0); //프로그램을 종료합니다.
                
			}catch(IOException e2){//예외처리시 실행(IOException시)
				e2.printStackTrace();//예외처리시 출력
			}
		}
	}

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in); //스캐너 입력
		System.out.print("아이디를 입력하세요 : ");//출력
		String id = sc.next();//입력 받기
		new MainClient(id, "서버 IP");
        //MainClient객체 생성시 문자열 2개를 넣어서 객체를 생성(id, ip)
	}

ReceiveDataThred(Client)

class ReceiveDataThread implements Runnable{
		Socket client; //Socket 변수 client 선언
		BufferedReader ois; // BufferReader 변수 ois 선언
		String receiveData; // 문자열 변수 receiveData 선언

		public ReceiveDataThread(Socket s, BufferedReader ois){ 
        //ReceiveDataThread 생성자 이고 Socket과 BufferReader을 인자로 받는다.
			client = s;
            //인자 s를 client에 대입
			this.ois = ois;
            //인자 ois를 인스턴스 변수 ois에 대입
		}

	  public void run(){
      //받는게 없고 주지도 않는 run 함수
		try{//시도하다
			while( ( receiveData = ois.readLine() ) != null ) 
            //ois의 readLine 함수를 호출하여 문자열 한줄 씩을 receiveDate에 대입을 하면
            //receiveDate가 null이 아니면 계속 반복
				System.out.println( receiveData );
                //receiveData 출력
			
		}catch(Exception e){//예외처리 발생시 실행
			e.printStackTrace();  //예외처리시 출력
	    }
	    finally{
        //위에 try catch 어떤상황이든 다끝나면 실행
			try{//시도하다.
                ois.close();
                //BufferReader 객체 ois close()
				client.close();
                //Socket 객체 client close()
			}catch(IOException e2){ //예외처리 발생시 실행(IOException 시)
				e2.printStackTrace();//예외처리 출력
			}
	    }
	  }
}

위에 소스를 보시면 Receive관련은 무조건 쓰레드를 만들고 있습니다. 이유는 데이터를 받는 것은 언제 받아질지 모르기 때문에 쓰레드를 켜서 받는 일만 담당하도록 하는 게 좋기 때문입니다.

서버 쓰레드 같은 경우에는 클라이언트가 출력 한 내용을 받아서 다시 모두에게 출력을 하게 구현이 되어 있습니다.

다시 한번 말씀드리지만 먼저 이전 글을 충분히 이해를 하시고 소스를 분석하시기 바랍니다.

 

※ 긴 글을 읽어 주셔서 감사합니다. 틈틈이 Special Java를 계속 진행해 나가겠습니다.


반응형

댓글