본문 바로가기
JAVA

18. 소켓 통신

by 김마리님 2020. 4. 14.

소켓통신은 통신의 원형이다.

소켓통신은 프로토콜이 없이 단순히 데이터만 주고 받으면 된다. 여기서 프로토콜이 생기면 지그비나 http, mqtt가 된다.

현재 사용하는 주요 통신을 몇가지 보면,

mqtt는 현대 통신에서 제일 많이 쓰이는 방식으로 유튜브와 유사하다. 퍼블리싱(pub)과 구독자(sub), 두 개체로 이루어져 있으며 퍼블리싱이 데이터를 업로드하면 구독자에게 메세지가 가는 형태로 통신이 가능해진다. 구독자가 구독하지 않을 경우 메세지가 가지 않으며, 새로 구독에 참여하게 되면 메세지가 전송되는 방식이다.

블루투스는 1:1 통신이 기본이다. 그러나 브로드 캐스트 방식으로 메시 네트워크를 이용하면 다:다 통신이 가능해지는데, 아이디를 바꿔가면서 통신는 형태이다.

 

소켓 통신으로 돌아와서, 소켓 통신은 OS가 지원(시스템콜)한다. 소켓 통신은 통신마다 포트가 있고, 그 포트를 통해 통신한다.

소켓통신은 다음과 같은 순서로 진행된다.

1. 새로운 소켓 생성

2. 서버 소켓에 연결된 선 연결

3. 새로운 소켓 클라이언트를 소켓에 연결

4. accept 요청 대기

(그 사이에 다른 스레드로 다른 소켓에 통신 => 연결되는 소켓마다 스레드가 필요하게 됨)

대기열은 들어온 순서대로 통신해야하므로 큐로 대기열을 생성한다.

 

1:1 통신을 하는 코드는 다음과 같다

먼저 통신을 하려면 서버를 열어야 한다.

 

- 서버코드

package ch15;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class MySocketServer {
	ServerSocket serverSocket;
	Socket socket;
	BufferedReader br;
	
	public MySocketServer() throws Exception{ //throw : try를 대신함. 전체 try catch 구간에 사용
	
		serverSocket=new ServerSocket(15000); //소켓 생성(연결을 받는)
		socket=serverSocket.accept(); //요청대기, 포트는 자동 설정
		System.out.println("요청이 들어옴");
		
		BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
		String msg="";
		while ((msg=br.readLine())!=null) {
			System.out.println("상대 : "+msg);
		}
		br.close();
		socket.close();
		serverSocket.close();	
	}

	public static void main(String[] args) {
		try {
			new MySocketServer();
		} catch (Exception e) {
			// throw는 catch를 가지고 있지 않아서, catch를 호출자에게 줌
			e.printStackTrace();
		}
	}
}

serverSocket을 이용해서 15000번 포트를 연결한다. 

accept를 이용해 접근요청을 한다. 이 때 accept를 통해 포트가 오버로딩 되며 새 포트를 부여받는다. 

서버는 입력된 값을 읽어들인다. 그렇기 때문에 bufferedReader 매서드를 이용하고, 읽어들이는 내용은 socket을 이용해 들어온 값을 입력받는다.

나머지는 일반적인 웹 정보를 읽어들일때와 같이 while문을 이용해서 입력을 계속해서 받아들이도록 한다.

 

서버가 있으면, 입력할 프로그램도 있어야 한다.

 

- 클라이언트 코드 

package ch15;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class MySocketClient {
	
	Socket socket;
	BufferedWriter bw;
	BufferedReader br;
	
	public MySocketClient() throws Exception{
		socket=new Socket("192.168.0.80",15000); //서버소켓의 accept() 함수 호출
		bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		br=new BufferedReader(new InputStreamReader(System.in));
		String msg="";
		while((msg=br.readLine())!=null) {
			bw.write(msg+"\n");
			bw.flush();
		}
		bw.close();
		br.close();
		socket.close();
	}
	public static void main(String[] args) {
		try {
			new MySocketClient();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

소켓에서 내가 통신하고 싶은 컴퓨터의 IP 주소와(여기서, ip에 "localhost"를 입력하면 입력한것을 본인의 서버로 입력받을 수 있다.), 서버에서 입력한 포트를 넣어 accept 함수를 호출한다.

bufferreader 은 내가 키보드에서 입력한 값을 읽어들이고, bufferwriter은 입력받은 값을 서버에 작성하는 매서드이다.

(그래서 reader에는 변수로 system.in, writer에는 socket이 들어가있다.)

이후에는 마찬가지로 작성을 하되, readline은 한줄씩 읽어들이므로 반드시 띄어쓰기를 하여 한줄씩 만들어줘야 한다. 따라서 \n이라는 값을 추가로 요구하게 되고, 우리가 한두줄 적는다고 8100byte를 넘을 확률은 적으므로 오토플러시를 사용할 수 없을 확률이 높다. 따라서 수동으로 플러시 되도록 해준다.

마지막에 try-catch 를 사용하는 이유는 서버가 끊김을 대비해서 try-catch 구문을 반드시 작성해주어야 한다.

 

채팅을 시작할땐 반드시 서버가 열려있어야 하므로, 서버가 있는 자바 파일부터 먼저 실행한다.

이 때 방화벽이 뜨는데,

이 포트는 외부와 통신기능이 있으므로, 모든 소켓을 연 경우에는 DMZ(비무장지대)라고 하여 연결에는 용이하지만 보안이 아주 취약하다. 따라서 os는 기본적으로 포트를 모두 막고 있다. 이 포트를 막는 것을 방화벽이라고 한다. 따라서 액세스를 허용하도록 한다.

(다른 IP를 가진 사람과 1:1 통신하는 결과물. 이 때 Client에 상대의 IP를 입력해야한다. 서버를 둘 다 열고 클라이언트를 실행해야하므로 주의해야한다.)

 

 

다음 다 : 다 통신하는 코드를 보자(미완성임)

다 대 다 통신의 경우 각각의 스레드가 돌아가야 하기 때문에 서브 스레드가 필수적으로 들어가야만 한다.

 

- 서버 코드

package chat;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Vector;

public class MySockerServer {

	ServerSocket serverSocket;
	Vector<NewSocketThread>vt;
	

	public MySockerServer() throws Exception {
		vt=new Vector<>();
		serverSocket = new ServerSocket(3000);
		while (true) {
			Socket socket = serverSocket.accept();
			System.out.println("요청이 들어옴");
			NewSocketThread nt = new NewSocketThread(socket);
			Thread newWorker = new Thread(nt);
			newWorker.start();
			vt.add(nt);
		}
	}

	// 새로운 스레드에게 버퍼를 연결할 수 있게 socket을 전달.
	class NewSocketThread implements Runnable {

		Socket socket;
		BufferedReader br;
		BufferedWriter bw;

		public NewSocketThread(Socket socket) {
			this.socket = socket;
		}

		@Override
		public void run() {
			try {
				br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				bw= new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
				
				String msg="";
				while((msg=br.readLine())!=null) {
					System.out.println("클라이언트 : "+msg);
					for (NewSocketThread newSocketThread : vt) {
						if(newSocketThread !=this) {
							newSocketThread.bw.write(msg+"\n");
							newSocketThread.bw.flush();
						}
					}	
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		try {
			new MySockerServer();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

메인 스레드는 accept에서 프로세스가 멈춰있으며, 접근요청이 올 때까지 멈춰있는 스레드이다. 따라서, 단순히 서버의 요청만을 받으며, 서버에서 요청을 받을 경우 새로 들어온 스레드에게 버퍼를 연결할 새로운 스레드를 안내해주는 역할을 한다.

이 때, 사람들의 소켓은 제각각이고, while 내의 공간은 stack 이기 때문에 한바퀴 돌아 종료되면 값이 사라지는 지역변수이다. 따라서, stack값이 종료될 때마다 accept는 가장 최근에 연결을 요청한 소켓만을 연결해줄 것이다. 따라서 둘 이상의 소켓을 연결할 수 없게 된다. 따라서, 그 소켓들을 저장해서 연결해줄, 소켓 저장소를 전역변수로 설정해야하는데, 이 때 사용되는 것이 VECTOR 이다.

 

서브 스레드에서는 서버의 역할을 봐야한다.

서버는 두가지 역할을 해야한다.

1. 받은 데이터를 전체에게 전달하는 역할

2. 입력받는 데이터를 출력하는 역할

그렇기 때문에 bufferreader, writer, 두 역할이 다 필요하게 된다.

입 출력 받는 데이터는 (서버의 입장에서) 둘 다 소켓에서 진행된다. 따라서, reader과 writer의 매개변수는 둘 다 socket이 되어야 할 것이다.

socket에서 받은 데이터는 msg로 들어가게 되고, 이 데이터는 readline을 통해 읽어들인다.

이 때, 내가 이후 Client를 통해 입력받는 데이터를 다시 채팅창에 반복되는 것을 막기 위해, 현재 내가 입력하는 객체(this)를 제외하는 값을 출력할 수 있도록 if로 제외시켜준다.

 

*VECTOR 과 ARRAYLIST의 차이

벡터와 리스트의 가장 큰 차이는 데이터에 접근할 때 지역의 설정 차이이다. arraylist의 경우는 현재 사용자가 접근하고 있는 공간에 다른 사용자가 접근하여 데이터를 바꾸는 것이 가능하지만 vector의 경우 현재 사용자가 접근하고 있는 구역을 critical area(임계구역)으로 지정하여 다른 사용자가 접근하는 것이 불가능하게 한다. 이렇게 vector로 설정하면, 예를 들어

arraylist 특정구역에 3이 있을 때,

첫번째 접근한 사람이 3이라는 것을 확인하고

두번째 접근한 사람이 5로 바꾸었을 때,

세번째 접근한 사람은 이것을 5라고 본다. 동시에 접근하고 있음에도 데이터에 혼선이 온다. 따라서 vector내에 소켓이 있는 구역이 사용중에 있다면, 이 구역을 사용하지 못하도록 하기 위해 이용한다.

 

- 클라이언트 코드

package chat;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class MySocketClient {

	Socket socket;

	public MySocketClient() throws Exception {
		socket = new Socket("192.168.0.80", 3000);
		new Thread(new ReadThread()).start();

		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		BufferedReader keyboardin = new BufferedReader(new InputStreamReader(System.in));
		String outputMsg = "";
		// 메인 스레드는 여기서 무한루프
		while ((outputMsg = keyboardin.readLine()) != null) {
			bw.write(outputMsg + "\n");
			bw.flush();
		}
	}

	class ReadThread implements Runnable {
		@Override
		public void run() {
			try {
				BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				String inputMsg = "";
				while ((inputMsg = br.readLine()) != null) {
					System.out.println("상대방 : " + inputMsg);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
	}

	public static void main(String[] args) {
		try {
			new MySocketClient();
		} catch (Exception e) {
		
			e.printStackTrace();
		}
	}
}

클라이언트 코드도 마찬가지로 내가 입력을 하며 동시에 출력을 받아야만 한다. 그렇기 때문에 출력받는 스레드를 서브 스레드로 지정한다.

메인 스레드에서는 접근할 IP주소를 지정하고(한 아이피에서 모여 다 : 다 통신을 진행할 예정임)

읽어들여야 할 것이 두 개다. 하나는 키보드로 입력하는, system 에서 입력받는 데이터, 다른 하나는 서버에서 읽어들어야 할 데이터. 이렇게 총 2개이다. 그래서 bufferreader이 두 개가 되어야 한다.

따라서, 메인 스레드는 키보드에서 데이터를 입력받고, 서버로 출력하는 코드이다. while문을 통해 프로세스가 종료되기 전까지 회전하며 끊임없이 입력되는 정보를 서버로 보낸다.

서브스레드는 소켓에서 입력받은 정보를 받아들여 모니터로 출력한다. 이제, 문제를 보자.

입력을 받는데에는 문제가 없으나, 사람이 둘 이상 들어오게 될 경우, 클라이언트의 System.out.println("상대방 : " + inputMsg); 코드에 의해 상대가 누구인지 알아챌 수 없게 되는 단점이 있다. 

또한 클라이언트와 서버가 연결됐을 때, 클라이언트에서 서버쪽으로 연결되는 값이 끊겼다고 해서 서버에서 클라이언트 쪽으로 연결되는 선이 끊기지 않았기 때문에, 누군가가 나가게 되면 오류가 생성된다.

또한 VECTOR 에 남은 클라이언트의 데이터 정보는 이후 죽은 데이터로 남게 되는데, 이 데이터를 캐치해서 삭제하지 않으면 죽은 데이터로 유지되며 메모리 낭비를 가져온다.

반응형

'JAVA' 카테고리의 다른 글

20. 이벤트  (0) 2020.04.20
19. GUI  (0) 2020.04.20
17. 멀티 스레드  (0) 2020.04.13
16. 익명 클래스  (0) 2020.04.13
JAVA 실습 10. 공공데이터를 이용하여 코로나 공공마스크 지원 약국 주소찾기.  (0) 2020.04.10