Socket Programming 의 개념

강정우·2024년 1월 30일
0

네트워크

목록 보기
24/32
post-thumbnail

Socket

프로그램이 네트워크에서 데이터를 송수신할 수 있도록, "네트워크 환경에 연결할 수 있게 만들어진 연결부"가 바로
"네트워크 소켓(Socket)"이다.

그런데 네트워크에 연결하기 위한 소켓 또한 정해진 규약,
즉, 통신을 위한 프로토콜(Protocol)에 맞게 만들어져야 한다.

보통은 OSI 7 Layer(Open System Interconnection 7 Layer)의 네 번째 계층인 TCP(Transport Control Protocol) 상에서 동작하는 소켓을 주로 사용하는데, 이를 "TCP 소켓" 또는 "TCP/IP 소켓"이라고 부른다.

클라이언트 소켓(Client Socket)

  1. 소켓 생성

  2. 서버 측에 연결 요청

    • API는 블럭(Block) 방식으로 동작한다.
      즉, 연결 요청에 대한 결과(성공, 거절, 시간 초과 등)가 결정되기 전에는 connect()의 실행이 끝나지 않는다.
  3. 서버 소켓에서 ok 하면 데이터를 송수신

    • API가 블럭(Block) 방식으로 동작한다.
    • 즉, 두 API(send, recv) 모두 실행 결과(성공, 실패, 종료)가 결정되기 전까지는 API가 리턴되지 않는다.
      이는 통신 대상이 언제, 어떤 데이터를 보낼 것인지를 특정할 수 없기 때문에 recv() API가 한번 실행되면 언제 끝날지 모르는 상태가 된다.
    • 그래서 데이터 수신을 위한 recv() API는 별도의 스레드에서 실행한다.
      소켓의 생성과 연결이 완료된 후, 새로운 스레드를 하나 만든 다음 그곳에서 recv()를 실행하고 데이터가 수신되길 기다린다.
  4. 끝나면 close

구현

1) 클라이언트 소켓 생성을 통한 서버접속

Socket socket = new Socket("127.0.0.1", 8000);  // IP주소, 포트번호

2) 데이터 송수신을 위한 input/output 스트림 생성 

InputStream in = socket.getInputStream( );
OutputStream out = socket.getOutputStream( );

3) output 스트림을 통한 데이터 송신 (클라이언트 → 서버)

String outputMessage = "보낼메시지";
out.write(outputMessage.getBytes( ));
out.flush( );

4) input 스트림을 통한 데이터 수신 (서버 → 클라이언트)

byte[ ] inputData = new byte[100];
int length = in.read(inputData);
String inputMessage = new String(inputData, 0, length);

5) 통신 종료

socket.close( );

서버 소켓(Server Socket)

  1. 소켓 생성

  2. 생성한 소켓에 서버 ip, port 번호 바인딩 (정보 입력)

    • 이때, 만약 지정된 포트 번호를 다른 소켓이 사용하고 있다면, bind() API는 에러를 리턴한다.
  3. 클라이언트로부터 연결 요청이 들어오는지 listen

    • 이때, listen() 의 대기 상태에서 빠져나오는 경우는 2가지
    • 클라이언트 연결 요청이 수신되었는지(SUCCESS), 그렇지 않고 에러가 발생했는지(FAIL) 뿐이다.
    • 또 대신 클라이언트 연결 요청에 대한 정보는 시스템 내부적으로 관리되는 큐(Queue)에서 쌓이게 되는데, 이 시점에서 클라이언트와의 연결은 아직 완전히 연결되지 않은(not ESTABLISHED state) 대기 상태이다.
      대기 중인 연결 요청을 큐(Queue)로부터 꺼내와서, 연결을 완료하기 위해서는 accept() API를 호출해야 한다.
  4. 요청이 성공적으로 수신되면 accept 후 데이터 통신을 위한 소켓을 별도로 생성

    • client socket의 3번과 마찬가지로 별도로 생성
  5. 일단 새로운 소켓을 통해 연결이 수립(ESTABLISHED)되면, 클라이언트와 마찬가지로 데이터 송수신(send/recv) 가능

  6. 데이터 송수신이 끝나면 소켓 close

구현

1) 서버소켓 생성

ServerSocket serverSocket = new ServerSocket(8000);  // 포트번호

2) 클라이언트 접속 대기

Socket socket = serverSocket.accept( );

3) 데이터 송수신을 위한 input/output 스트림 생성 

InputStream in = socket.getInputStream( );
OutputStream out = socket.getOutputStream( );

4) input 스트림을 통한 데이터 수신 (클라이언트 → 서버)

byte[ ] inputData = new byte[100];
int length = in.read(inputData);
String inputMessage = new String(inputData, 0, length);

5) output 스트림을 통한 데이터 송신 (서버 → 클라이언트) 

String outputMessage = "보낼메시지";
out.write(outputMessage.getBytes( ));
out.flush( );

6) 통신 종료

socket.close( );
serverSocket.close( );

BufferedReader/BufferedWriter와 PrintWriter, PrintStream

데이터 전송의 기본단위는 바이트(byte)로 문자의 경우 데이터 전송시 문자를 바이트로, 수신시 바이트를 문자로 변환이 필요하다. 이를 위해서 앞에서는 String클래스의 생성자와 getBytes메소드를 이용했다. 

자바에는 이 변환을 편리하게 해주는 클래스로 InputStreamReader와 OutputStreamWriter가 있으며 charset을 지정하여 사용할 수 있다. 또한 데이터 입출력의 효율을 위해 바로 전달하지 않고 중간에 버퍼를 이용하기 위해서 BufferedReader와 BufferedWriter 클래스를 함께 사용할 수 있다. 

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream( )));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream( )));

데이터 출력의 경우에는 출력 포맷을 편리하게 해주는 기능이 있는 PrintWriter 또는 PrintStream 클래스를 사용할 수 있다. 

PrintWriter out = new PrintWriter(socket.getOutputStream( ));
PrintStream out = new PrintStream(socket.getOutputStream( ));

소켓통신 사용예제 (1:1 채팅 프로그램)

1) 서버

public class MultiServer {
	
	public static void main(String[] args) {
		MultiServer multiServer = new MultiServer();
		multiServer.start();
	}
	
	public void start() {
		ServerSocket serverSocket = null;
		Socket socket = null;
		try {
			serverSocket = new ServerSocket(8000);
			while (true) {
				System.out.println("[클라이언트 연결대기중]");
				socket = serverSocket.accept();
				
				// client가 접속할때마다 새로운 스레드 생성
				ReceiveThread receiveThread = new ReceiveThread(socket);	
				receiveThread.start();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (serverSocket!=null) {
				try {
					serverSocket.close();
					System.out.println("[서버종료]");
				} catch (IOException e) {
					e.printStackTrace();
					System.out.println("[서버소켓통신에러]");
				}
			}
		}
	}
}

class ReceiveThread extends Thread {
	
	static List<PrintWriter> list = 
			Collections.synchronizedList(new ArrayList<PrintWriter>());
	
	Socket socket = null;
	BufferedReader in = null;
	PrintWriter out = null;
			
	public ReceiveThread (Socket socket) {
		this.socket = socket;
		try {
			out = new PrintWriter(socket.getOutputStream());
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			list.add(out);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
		
	@Override
	public void run() {

		String name = "";
		try {
			// 최초1회는 client이름을 수신
			name = in.readLine();
			System.out.println("[" + name + " 새연결생성]");	
			sendAll("[" + name + "]님이 들어오셨습니다.");
			
			while (in != null) {
				String inputMsg = in.readLine();
				if("quit".equals(inputMsg)) break;
				sendAll(name + ">>" + inputMsg);
			}
		} catch (IOException e) {
			System.out.println("[" + name + " 접속끊김]");
		} finally {
			sendAll("[" + name + "]님이 나가셨습니다");
			list.remove(out);
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		System.out.println("[" + name + " 연결종료]");
	}
	
	private void sendAll (String s) {
		for (PrintWriter out: list) {
			out.println(s);
			out.flush();
		}
	}
}

2) 클라이언트

public class MultiClient {
	
	public static void main(String[] args) {
		MultiClient multiClient = new MultiClient();
		multiClient.start();
	}
	
	public void start() {
		Socket socket = null;
		BufferedReader in = null;
		try {
			socket = new Socket("localhost", 8000);
			System.out.println("[서버와 연결되었습니다]");

			String name = "user" + (int)(Math.random()*10);
			Thread sendThread = new SendThread(socket, name);
			sendThread.start();

			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			while (in != null) {
				String inputMsg = in.readLine();
				if(("[" + name + "]님이 나가셨습니다").equals(inputMsg)) break;
				System.out.println("From:" + inputMsg);
			}
		} catch (IOException e) {
			System.out.println("[서버 접속끊김]");
		} finally {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		System.out.println("[서버 연결종료]");
	}
}

class SendThread extends Thread {
	Socket socket = null;
	String name;

	Scanner scanner = new Scanner(System.in);
	
	public SendThread(Socket socket, String name) {
		this.socket = socket;
		this.name = name;
	}

	@Override
	public void run() {
 		try {
			// 최초1회는 client의 name을 서버에 전송
			PrintStream out = new PrintStream(socket.getOutputStream());
			out.println(name);
			out.flush();
			
			while (true) {
				String outputMsg = scanner.nextLine();
				out.println(outputMsg);
				out.flush();
				if("quit".equals(outputMsg)) break;
			}
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
}

reference

https://kadosholy.tistory.com/125

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글