안녕하세요.
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를 계속 진행해 나가겠습니다.
'IT 돌고래 > Special JAVA' 카테고리의 다른 글
[Java]GUI를 활용한 숫자야구게임 (0) | 2020.03.17 |
---|---|
[Java] TCP 소켓통신 프로그램 만들기(1:1) (2) | 2020.03.09 |
[JAVA] 파일을 이용한 전화번호부 만들기(콘솔 출력) (6) | 2020.02.22 |
댓글