[Java] 네트워킹(Networing) #2. 소켓 프로그래밍

bien·2024년 2월 12일
0

java

목록 보기
7/11

소켓 프로그래밍

  • 소켓 프로그래밍
    • 소켓을 이용한 통신 프로그래밍.
  • 소켓(socket)
    • 프로세스간의 통신에 사용되는 양쪽 끝단 (endpoint)
    • 서로 멀리 떨어진 두 사람이 통신하기 위해서 전화기가 필요한 것처럼, 프로세스간의 통신을 위해서는 소켓이 필요하다.
  • Java에서는 java.net패키지를 통해 소켓 프로그래밍을 지원한다.

1) TCP와 UDP

  • TCP/IP 프로토콜
    • 이기종 시스템간의 통신을 위한 표준 프로토콜. 프로토콜의 집합.
    • TCP와 UDP 모두 TCP/IP 프로토콜(TCP/IP protocol suties)에 포함되어 있다.
    • OSI 7계층의 전송계층(transport layer)에 해당하는 프로토콜이다.

TCP

  • 연결방식
    • 연결기반(connection-oriented)
      • 연결 후 통신(전화기)
      • 1:1 통신 방식
  • 특징
    • 데이터의 경계를 구분 안함 (byte-stream)
    • 신뢰성 있는 데이터 전송
      • 데이터의 전송 순서가 보장됨.
      • 데이터의 수신 여부를 확인함. (데이터가 손실되면 재전송됨)
      • 패킷을 관리할 필요가 없음
    • UDP보다 전송속도가 느림.
  • 관련 클래스
    • Socket, ServerSocket

데이터를 전송하기 전에 먼저 상대편과 연결을 한 후에 데이터를 전송하며 잘 전송되었는지 확인하고 전송에 실패했다면 해당 데이터를 재전송하기 때문에 신뢰 있는 데이터의 전송이 요구되는 통신에 적합하다. 예를 들어 파일을 주고받는데 적합하다.

UDP

  • 연결방식
    • 비연결기반(connectionless-oriented)
      • 연결없이 통신 (소포)
      • 1:1, 1:n, n:n 통신방식
  • 특징
    • 데이터의 경계를 구분함 (datagram)
    • 신뢰성 없는 데이터 전송
      • 데이터의 전송 순서가 바뀔 수 있음.
      • 데이터의 수신 여부를 확인안함. (데이터가 손실되어도 알 수 없음)
      • 패킷을 관리해주어야 함.
    • TCP보다 전송 속도가 빠름
  • 관련 클래스
    • DatagramSocket, DatagramPacket, MulticastSocket

상대편과 연결하지 않고 데이터를 전송하며, 데이터를 전송하지만 데이터가 바르게 수신되었는지 확인하지 않기 때문에 데이터가 전송되었는지 확인할 길이 없다. 또한 데이터를 보낸 순서대로 수신한다는 보장이 없다.
대신 이와같은 확인 과정이 필요하지 않아 TCP에 비해 전송 속도가 빠르며, 게임이나 동영상의 데이터 전송과 같이 일부 데이터가 손실되어 끊기더라도 빠른 전송이 필요할 때 적합하다. 이때 전송 순서가 바뀌어 늦게 도착한 데이터는 무시하면 된다.

2) TCP 소켓 프로그래밍

  • TCP 소켓 프로그래밍
    • 클라이언트와 서버간의 1:1 통신
    • 먼저 서버 프로그램이 실행되어 클라이언트 프로그램의 연결 요청을 기다리고 있어야 한다.

서버 프로그램과 클라이언트 프로그램간의 통신 과정

  1. 서버 프로그램에서는 서버소켓을 사용해서 서버 컴퓨터의 특정 포트에서 클라이언트의 연결요청을 처리할 준비를 한다.
  2. 클라이언트 프로그램은 접속할 서버의 IP주소와 포트 정보를 가지고 소켓을 생성해서 서버에 연결을 요청한다.
  3. 서버소켓은 클라이언트의 연결요청을 받으면 서버에 새로운 소켓을 생성해서 클라이언트의 소켓과 연결되도록 한다.
  4. 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 서버소켓과 관계없이 일대일 통신을 한다.
  • 서버 소켓 (ServerSocket)
    • 포트와 결합(bind)되어 포트를 통해 원격 사용자의 연결요청을 기다리다가 연결요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결한다.
    • 실질적인 데이터 통신은 서버소켓과 관계없이 소켓과 소켓 간에 이루어진다.
    • 서버 소켓은 전화교환기에, 소켓은 전화기에 비유할 수 있다.
      • 전화교환기(서버소켓)는 외부 전화기(원격 소켓)로 부터 걸려온 전화를 내부의 전화기(소켓)에 연결해주고 실제 통화는 전화기(소켓) 대 전화기(원격 소켓)로 이루어지게 한다.
  • 서버소켓은 하나의 포트를 독점한다. (소켓은 포트를 공유할 수 있다.)
    • 서버소켓이 둘 이상의 포트를 공유하면 클라이언트 프로그램이 어떤 서버소켓과 연결되어야 하는지 알 수 없기 때문.

두 서버소켓이 서로 다른 프로토콜을 사용하는 경우에는 같은 포트를 사용할 수 있다. 포트는 같아도 클라이언트 프로그램이 사용하는 프로토콜로 어떤 서버소켓과 연결되어야 하는지 구별할 수 있기 때문이다. 그래돼 가능하면 하나의 포트는 하나의 서버소켓만 사용하도록 하는 것이 바람직하다.

  • 포트(port)
    • 호스트(컴퓨터)가 외부와 통신을 하기위한 통로.
    • 포트의 범위는 0~65535 사이로, 하나의 호스트가 65536개의 포트를 가지고 있다.
    • 보통 1023번 이하의 포트는 FTP나 Talnet과 같은 기존의 다른 통신 프로그램들에 의해서 사용되는 경우가 많기 때문에 1023번 이상의 번호 중 사용하지 않는 번호를 골라야 한다.
  • 소켓(socket)
    • 소켓들이 데이터를 주고받는 연결 통로는 입출력 스트림이다.
    • 소켓은 두 개의 스트림(입력스트림과 출력스트림)을 가지고 있으며, 이 스트림들은 연결된 상대편 소켓의 스트림들과 교차연결된다.
      • 한 소켓의 입력스트림은 상대편 소켓의 출력스트림과 연결되고, 출력스트림은 입력스트림과 연결된다.
      • 그래서 한 소켓에서 출력 스트림으로 데이터를 보내면 상대편 소켓에서 입력스트림으로 받게 된다.

TCP 관련 클래스

  • Socket
    • 프로세스간의 통신을 담당하며, InputStreamOutputStream을 가지고 있다.
      • 이 두 스트림을 통해 프로세스간의 통신(입출력)이 이루어진다.
  • ServerSocekt
    • 포트와 연결(bind)되어 외부의 연결 요청을 기다리다 연결요청이 들어오면 Socket을 생성해서 소켓과 소켓간의 통신이 이루어지도록 한다.
    • 한 포트에 하나의 ServerSocket만 연결할 수 있다. (프로토콜이 다르면 같은 포트 공유 가능)

2-1) 소켓 구현 예제1

서버 구현 예제

package _ch16;

import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.io.*;

public class TcpIpServer {

	public static void main(String[] args) {
		
		ServerSocket serverSocket = null;
		
		try {
			// 서버소켓을 생성하여 7777번 포트와 결합(bind) 시킨다.
			serverSocket = new ServerSocket(7777);
			System.out.println(getTime() + "서버가 준비되었습니다.");
		} catch(IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println(getTime() +"연결 요청을 기다립니다.");
				// 서버 소켓은 클라이언트의 연결요청이 올 때까지 실행을 멈추고 계속 기다린다.
				// 클라이언트의 연결 요청이 오면 클라이언트 소켓과 통신할 새로운 소켓을 생성한다.
				Socket socket = serverSocket.accept();
				System.out.println(getTime() + socket.getInetAddress() + 
						"로부터 연결요청이 들어왔습니다.");
				
				// 소켓의 출력 스트림을 얻는다.
				OutputStream out = socket.getOutputStream();
				DataOutputStream dos = new DataOutputStream(out);
				
				// 원격 소켓(remote socket)에 데이터를 보낸다.
				dos.writeUTF("[Notice] Test Message1 from Server");
				System.out.println(getTime() + "데이터를 전송했습니다.");
				
				// 스트림과 소켓을 닫아준다.
				dos.close();
				socket.close();
				
			} catch (IOException e) {
				e.printStackTrace();
			}
		} // while
		
	} // main
	
	// 현재 시간을 문자열로 반환하는 함수
	static String getTime() {
		SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
		return f.format(new Date());
	}
} // class
  • 예제 실행 시 서버소켓이 7777번 포트에서 클라이언트 프로그램의 연결 요청을 기다린다.
    • 클라이언트의 요청이 올 때까지 진행을 멈추고 계속 기다린다.
  • 클라이언트 프로그램이 연결을 요청하면, 서버소켓은 새로운 소켓을 생성하여 클라이언트의 프로그램의 소켓과 연결한다.
    • 새로 생성된 소켓은 "[Notice] Test Message1 from Server"라는 데이터를 원격소켓에 전송하로 연결을 종료한다.
  • 이후 서버소켓은 다시 클라이언트의 프로그램 요청을 기다린다.
while(true) {
	try {
    	...
        Socket socket = serverSocket.accept();
        ...
  • 클라이언트 프로그램의 요청을 지속적으로 처리하기 위해 무한 반복문을 사용했기 때문에 서버 프로그램을 종료시키려면 Ctrl + C를 눌러 강제종료시켜야 한다.

결과

[11:02:01]서버가 준비되었습니다.
[11:02:01]연결 요청을 기다립니다.
[11:02:04]/127.0.0.1로부터 연결요청이 들어왔습니다.
[11:02:04]데이터를 전송했습니다.
[11:02:04]연결 요청을 기다립니다.

클라이언트 소켓 예제

package _ch16;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.Socket;

public class TcpIpClient {

	public static void main(String[] args) {
		try {
			String serverIp = "127.0.0.1";
			
			System.out.println("서버에 연결중입니다. 서버IP :" + serverIp);
			// 소켓을 생성하여 연결을 요청한다.
			Socket socket = new Socket(serverIp, 7777);
			
			// 소켓의 입려스트림을 얻는다.
			InputStream in = socket.getInputStream();
			DataInputStream dis = new DataInputStream(in);
			
			// 소켓으로 부터 받은 데이터를 출력한다.
			System.out.println("서버로부터 받은 메시지 :" + dis.readUTF());
			System.out.println("연결을 종료합니다.");
			
			// 스트림과 소켓을 닫는다.
			dis.close();
			socket.close();
			System.out.println("연결이 종료되었습니다.");
		} catch (ConnectException ce) {
			ce.printStackTrace();
		} catch (IOException ie) {
			ie.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // main
}
  • 연결하고자 하는 서버의 ip번호와 포트번호를 가지고 소켓을 생성하면 자동으로 서버에 연결요청을 하게 된다.
String serverIp = "127.0.0.1";
Socket socket = new Socket(serverIp, 7777);
  • 서버 프로그램이 실행되고 있지 않거나 서버의 전원이 꺼져있어 서버와 연결을 실패하면 ConnectException이 발생한다.
    • 서버와 연결되면 소켓의 입력스트림을 얻어 서버가 전송한 데이터를 읽을 수 있다.
InputStream in = socket.getInputStream();
DataInputStream dis = new DataInputStream(in);

// 소켓으로부터 받은 데이터를 출력한다.
System.out.println("서버로부터 받은 메시지 :" + dis.readUTF());
  • 이후 서버와의 작업이 끝나면 소켓과 스트림을 닫아야 한다.
dis.close();
socket.close();

결과

서버에 연결중입니다. 서버IP :127.0.0.1
서버로부터 받은 메시지 :[Notice] Test Message1 from Server
연결을 종료합니다.
연결이 종료되었습니다.

소켓 연결 진행 과정

서버의 IP는 192.168.10.100, 클라이언트 IP는 192.168.10.101이라고 가정하자.

  1. 서버 프로그램(TcpIpServer.java)를 실행한다.
  2. 서버 소켓을 생성한다.
    • serverSocket = new ServerSocket(7777); // TcpIpServer.java
  3. 서버 소켓이 클라이언 프로그램의 연결요청을 처리할 수 있도록 대기상태로 만든다. 클라이언트 프로그램의 연결요청이 오면 새로운 소켓을 생성해서 클라이언트 프로그램의 소켓과 연결한다.
    • Socket socket = serverSocket.accept(); // TcpIpServer.java
  4. 클라이언트 프로그램(TcpIpClient.java)에서 소켓을 생성하여 서버소켓에 연결을 요청한다.
    • Socket socket = new Socket("192.168.10.100", 7777)
  5. 서버 소켓은 클라이언트 프로그램의 연결요청을 받아 새로운 소켓ㅇ르 생성하여 클라이언트 프로그램의 소켓과 연결한다.
    • Socket socket = serverSocket.accept();

2-2) 소켓구현 예제2

package _ch16;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TcpIpServer2 {
	
	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		
		try {
			// 서버소켓을 생성하여 7777번 포트와 결합(bind)시킨다.
			serverSocket = new ServerSocket(7777);
			System.out.println(getTime() + "서버가 준비되었습니다.");
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				// 서버소켓
				System.out.println(getTime() + "연결요청을 기다립니다.");
				Socket socket = serverSocket.accept();
				System.out.println(getTime() + socket.getInetAddress() 
												+ "로부터 연결요청이 들어왔습니다.");
				System.out.println("getPort() :" + socket.getPort());
				System.out.println("getLocalPort() :" + socket.getLocalPort());
				
				// 소켓의 출력 스트림을 얻는다.
				OutputStream out = socket.getOutputStream();
				DataOutputStream dos = new DataOutputStream(out);
				
				// 원격 소켓(remote socket)에 데이터를 보낸다.
				dos.writeUTF("[Notice] Test Message1 from Server.");
				System.out.println(getTime() + "데이터를 전송했습니다.");
				
				// 스트림과 소켓을 닫아준다.
				dos.close();
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} // while
	} // main

	// 현재 시간을 문자열로 반환하는 함수
	static String getTime() {
		SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
		return f.format(new Date());
	}
}
  • Socket 클래스에 정의된 getPort()getLocalPort()를 사용해서 TCP/IP 통신에서 소켓이 사용하고 있는 포트를 알아낼 수 있다.
    • getPort()가 반환하는 값은 상대편 소켓(원격 소켓)이 사용하는 포트이다.
    • getLocalPort()가 반환하는 값은 소켓 자신이 사용하는 포트이다.

결과

[12:07:17]서버가 준비되었습니다.
[12:07:17]연결요청을 기다립니다.
[12:07:21]/127.0.0.1로부터 연결요청이 들어왔습니다.
getPort() :2839
getLocalPort() :7777
[12:07:21]데이터를 전송했습니다.
[12:07:21]연결요청을 기다립니다.
  • 클라이언트 프로그램의 소켓이 사용한 포트는 2839번이고, 서버 프로그램의 소켓은 7777번이다.
    • 서버소켓이 7777번 포트를 사용하고 있어도, 서버소켓이 아닌 소켓은 7777번 포트를 사용할 수 있다.
    • 클라이언트 프로그램의 소켓이 사용하는 포트는 사용가능한 임의의 포트가 선택된다.

2-3) 소켓구현 예제3

package _ch16;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TcpIpServer3 {

	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		
		try {
			// 서버소켓을 생성하여 7777번 포트와 결합(bind)시킨다.
			serverSocket = new ServerSocket(7777);
			System.out.println(getTime() + "서버가 준비되었습니다.");
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		while(true) {
			try {
				System.out.println(getTime() + "연결요청을 기다립니다.");
				
				// 요청 대기 시간을 5초로 설정한다.
				// 5초동안 접속요청이 없으면 SocketTimeoutException이 발생한다.
				serverSocket.setSoTimeout(5 * 1000);
				Socket socket = serverSocket.accept();
				System.out.println(getTime() + socket.getInetAddress()
												+ "로부터 연결요청이 들어왔습니다.");
					
				// 소켓의 출력 스크림을 받는다.
				OutputStream out = socket.getOutputStream();
				DataOutputStream dos = new DataOutputStream(out);
				
				// 원격 소켓(remote socket)에 데이터를 보낸다.
				dos.writeUTF("[Notice] Test Message1 from Server.");
				System.out.println(getTime() + "데이터를 전송했습니다.");
				
				// 스트림과 소켓을 닫아준다.
				dos.close();
				socket.close();
			} catch (SocketTimeoutException e) {
				System.out.println("지정된 시간동안 접속요청이 없어서 서버를 종료합니다.");
				System.exit(0);
			} catch(IOException e) {
				e.printStackTrace();
			}
		}
	}

	static String getTime() { // 현재 시간을 문자열로 반환하는 함수
		SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
		return f.format(new Date());
	}
}
  • Socket 클래스의 setSoTimeout(int timeout)을 사용해 서버소켓의 대기시간을 지정할 수 있다.
    • timeout의 값은 천분의 일초단위이며 0을 입력하면 제한시간 없이 대기하게 된다.
    • 지정한 대기시간이 지나면 accept()에서 SocketTimeoutException이 발생하므로 catch문에서 적절한 처리를 할 수 있다.

결과

[12:19:31]서버가 준비되었습니다.
[12:19:31]연결요청을 기다립니다.
지정된 시간동안 접속요청이 없어서 서버를 종료합니다.

2-4) 소켓구현 예제4

package _ch16;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TcpIpServer4 implements Runnable {
	ServerSocket serverSocket;
	Thread[] threadArr;

	public static void main(String[] args) {
		// 5개의 쓰레드를 생성하는 서버를 생성한다.
		TcpIpServer4 server = new TcpIpServer4(5);
		server.start();
	} // main

	public TcpIpServer4(int num) {
		try {
			// 서버소켓을 생성하여 7777번 포트와 결합(bind)시킨다.
			serverSocket = new ServerSocket(7777);
			System.out.println(getTime() + "서버가 준비되었습니다.");

			threadArr = new Thread[num];
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void start() {
		for (int i = 0; i < threadArr.length; i++) {
			threadArr[i] = new Thread(this);
			threadArr[i].start();
		}
	}

	public void run() {
		while(true) {
			try {
				System.out.println(getTime() + "가 연결요청을 기다립니다.");
				
				Socket socket = serverSocket.accept();
				System.out.println(getTime() + socket.getInetAddress() 
													+ "로부터 연결요청이 들어왔습니다.");
				
				// 소켓의 출력스트림을 얻는다.
				OutputStream out = socket.getOutputStream();
				DataOutputStream dos = new DataOutputStream(out);
				
				// 원격 소켓(remote socket)에 데이터를 보낸다.
				dos.writeUTF("[Notice] Test Message1 from Server.");
				System.out.println(getTime() + " 데이터를 전송했습니다.");
				
				// 스트림과 소켓을 닫아준다.
				dos.close();
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	static String getTime() { // 현재 시간을 문자열로 반환하는 함수
		String name = Thread.currentThread().getName();
		SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
		return f.format(new Date()) + name;
	}
}
  • 여러 개의 쓰레드를 생성해 클라이언트의 요청을 동시에 처리하도록 했다.
  • 서버에 접속하는 클라이언트의수가 많을 때는 쓰레드를 이용해 클라이언트의 요청을 병렬적으로 처리하는 것이 좋다.
    • 그렇지 않으면 서버가 접속을 요청한 순서대로 처리하기 때문에 늦게 접속을 요청한 클라이언트는 올내 시간을 기다릴 수 있다.

결과

[12:31:12]main서버가 준비되었습니다.
[12:31:12]Thread-4가 연결요청을 기다립니다.
[12:31:12]Thread-2가 연결요청을 기다립니다.
[12:31:12]Thread-1가 연결요청을 기다립니다.
[12:31:12]Thread-3가 연결요청을 기다립니다.
[12:31:12]Thread-0가 연결요청을 기다립니다.
[12:31:16]Thread-4/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:16]Thread-4 데이터를 전송했습니다.
[12:31:16]Thread-4가 연결요청을 기다립니다.
[12:31:18]Thread-2/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:18]Thread-2 데이터를 전송했습니다.
[12:31:18]Thread-2가 연결요청을 기다립니다.
[12:31:19]Thread-0/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:19]Thread-0 데이터를 전송했습니다.
[12:31:19]Thread-0가 연결요청을 기다립니다.
[12:31:20]Thread-1/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:20]Thread-1 데이터를 전송했습니다.
[12:31:20]Thread-1가 연결요청을 기다립니다.
[12:31:21]Thread-3/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:21]Thread-3 데이터를 전송했습니다.
[12:31:21]Thread-3가 연결요청을 기다립니다.
[12:31:22]Thread-4/127.0.0.1로부터 연결요청이 들어왔습니다.
[12:31:22]Thread-4 데이터를 전송했습니다.
[12:31:22]Thread-4가 연결요청을 기다립니다.

2-5) 소켓구현 예제5

TcpIpServer5

package _ch16;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpIpServer5 {
	
	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		Socket socket = null;
		
		try {
			// 서버소켓을 생성하여 7777번 포트와 결합(bind)시킨다.
			serverSocket = new ServerSocket(7777);
			System.out.println("서버가 준비되었습니다.");
			
			socket = serverSocket.accept();
			
			Sender sender = new Sender(socket);
			Receiver receiver = new Receiver(socket);
			
			sender.start();
			receiver.start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // main()
} // class

class Sender extends Thread {
	Socket socket;
	DataOutputStream out;
	String name;
	
	Sender(Socket socket) {
		this.socket = socket;
		try {
			out = new DataOutputStream(socket.getOutputStream());
			name = "[" + socket.getInetAddress() + " : " + socket.getPort() + "]";
		} catch (Exception e) {}
	}
	
	public void run() {
		Scanner scanner = new Scanner(System.in);
		while(out != null) {
			try {
				out.writeUTF(name + scanner.nextLine());
			} catch (IOException e) {}
		}
	} // run()
}

class Receiver extends Thread {
	Socket socket;
	DataInputStream in;
	
	Receiver(Socket socket) {
		this.socket = socket;
		try {
			in = new DataInputStream(socket.getInputStream());
		} catch (IOException e) {}
	}
	
	public void run() {
		while(in != null) {
			try {
				System.out.println(in.readUTF());
			} catch(IOException e) {}
		}
	} // run()
}
  • 소켓으로 데이터를 송신하는 작업과 수신하는 작업을 별도의 쓰레드 Sender와 Receiver가 처리하도록 하여 송신과 수신이 동시에 이루어지도록 했다.
    • 서버 프로그램(TcpIpServer5)과 클라이언트 프로그램(TcpIpClient5)의 화면에 입력한 데이터가 상대방의 화면에 출력되므로 1:1 채팅이 가능하다.

TcpIpClient5

package _ch16;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;

public class TcpIpClient5 {

	public static void main(String[] args) {
		try {
			String serverIp = "127.0.0.1";
			// 소켓을 생성하여 연결을 요청한다.
			Socket socket = new Socket(serverIp,7777);
			
			System.out.println("서버에 연결되었습니다.");
			Sender sender = new Sender(socket);
			Receiver receiver = new Receiver(socket);
			
			sender.start();
			receiver.start();
		} catch(ConnectException ce) {
			ce.printStackTrace();
		} catch(IOException ie) {
			ie.printStackTrace();
		} catch(Exception e) {
			e.printStackTrace();
		}
	} // main
    
} // class

2-6) 멀티 채팅 프로그램 예제

TcpIpMultichatServer.java

package _ch16;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;

public class TcpIpMultichatServer {

	HashMap clients;
	
	TcpIpMultichatServer() {
		clients = new HashMap();
		Collections.synchronizedMap(clients);
	}
	
	public void start() {
		ServerSocket serverSocket = null;
		Socket socket = null;
		
		try {
			serverSocket = new ServerSocket(7777);
			System.out.println("서버가 시작되었습니다.");
			
			while(true) {
				socket = serverSocket.accept();
				System.out.println("[" + socket.getInetAddress()
								+" : " + socket.getPort() + "] 에서 접속하셨습니다.");
				ServerReceiver thread = new ServerReceiver(socket);
				thread.start();
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	void sendToAll(String msg) {
		Iterator it = clients.keySet().iterator();
		
		while(it.hasNext()) {
			try {
				DataOutputStream out = (DataOutputStream) clients.get(it.next());
				out.writeUTF(msg);
			} catch(IOException e) {}
		} //while
	}// sendToAll
	
	public static void main(String args[]) {
		new TcpIpMultichatServer().start();
	}
	
	class ServerReceiver extends Thread {
		Socket socket;
		DataInputStream in;
		DataOutputStream out;
		
		ServerReceiver(Socket socket) {
			this.socket = socket;
			try {
				in = new DataInputStream(socket.getInputStream());
				out = new DataOutputStream(socket.getOutputStream());
			} catch(IOException e) {}
		}
		
		public void run() {
			String name = "";
			
			try {
				name = in.readUTF();
				sendToAll("#" + name + "님이 들어오셨습니다.");
				
				clients.put(name, out);
				System.out.println("현재 서버접속자 수는 " + clients.size() + "입니다.");
				
				while(in != null) {
					sendToAll(in.readUTF());
				}
			} catch(IOException e) {
				// ignore
			} finally {
				sendToAll("#" + name + "님이 나가셨습니다.");
				clients.remove(name);
				System.out.println("[" + socket.getInetAddress()
							+ " : " + socket.getPort() + "] 에서 접속을 종료하셨습니다.");
				System.out.println("현재 서버 접속자수는 " + clients.size() + "입니다.");
			} // try
		} // run
		
	} // ReceiverThread
} // class

결과

서버가 시작되었습니다.
[/127.0.0.1 : 53967] 에서 접속하셨습니다.
현재 서버접속자 수는 1입니다.
[/127.0.0.1 : 53976] 에서 접속하셨습니다.
현재 서버접속자 수는 2입니다.
[/127.0.0.1 : 53988] 에서 접속하셨습니다.
현재 서버접속자 수는 3입니다.
[/127.0.0.1 : 53967] 에서 접속을 종료하셨습니다.
현재 서버 접속자수는 2입니다.
[/127.0.0.1 : 53988] 에서 접속을 종료하셨습니다.
현재 서버 접속자수는 1입니다.
[/127.0.0.1 : 53976] 에서 접속을 종료하셨습니다.
현재 서버 접속자수는 0입니다.

TcpIpMultichatClient.java

package _ch16;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.util.Scanner;

public class TcpIpMultichatClient {

	public static void main(String[] args) {
		if(args.length != 1) {
			System.out.println("USAGE: java TcpIpMultichatClient 대화명");
			System.exit(0);
		}
		
		try {
			String serverIp = "127.0.0.1";
			// 소켓을 생성하여 연결을 요청한다.
			Socket socket = new Socket(serverIp, 7777);
			System.out.println("서버에 연결되었습니다.");
			
			Thread sender = new Thread(new ClientSender(socket, args[0]));
			Thread receiver = new Thread(new ClientReceiver(socket));
			
			sender.start();
			receiver.start();
		} catch(ConnectException ce) {
			ce.printStackTrace();
		} catch(Exception e) {
			e.printStackTrace();
		}
	} // main

	static class ClientSender extends Thread {
		Socket socket;
		DataOutputStream out;
		String name;

		ClientSender(Socket socket, String name) {
			this.socket = socket;
			try {
				out = new DataOutputStream(socket.getOutputStream());
				this.name = name;
			} catch (Exception e) {
			}
		}

		public void run() {
			Scanner scanner = new Scanner(System.in);
			try {
				if (out != null) {
					out.writeUTF(name);
				}

				while (out != null) {
					out.writeUTF("[" + name + "]" + scanner.nextLine());
				}
			} catch (IOException e) {
			}
		} // run()
	} // ClientSender

	static class ClientReceiver extends Thread {
		Socket socket;
		DataInputStream in;

		ClientReceiver(Socket socket) {
			this.socket = socket;
			try {
				in = new DataInputStream(socket.getInputStream());
			} catch (IOException e) {
			}
		}

		public void run() {
			while (in != null) {
				try {
					System.out.println(in.readUTF());
				} catch (IOException e) {
				}
			}
		} // run
	} // ClientReceiver
} // class

결과

C:\jdk1.8\work>java TcpIpMultichatClient aaa
서버에 연결되었습니다.
#bbb님이 들어오셨습니다.
#ccc님이 들어오셨습니다.
안녕하세요!
[aaa]안녕하세요!
[ccc]반갑습니다~
[bbb]만나서 반가워요~~
저는 가보겠습니다!
[aaa]저는 가보겠습니다!
C:\jdk1.8\work>java TcpIpMultichatClient bbb
서버에 연결되었습니다.
#ccc님이 들어오셨습니다.
[aaa]안녕하세요!
[ccc]반갑습니다~
만나서 반가워요~~
[bbb]만나서 반가워요~~
[aaa]저는 가보겠습니다!
#aaa님이 나가셨습니다.
#ccc님이 나가셨습니다.
C:\jdk1.8\work>java TcpIpMultichatClient ccc
서버에 연결되었습니다.
[aaa]안녕하세요!
반갑습니다~
[ccc]반갑습니다~
[bbb]만나서 반가워요~~
[aaa]저는 가보겠습니다!
#aaa님이 나가셨습니다.

코드 분석

서버 프로그램(TcpIpMultichatServer.java)을 보면 서버에 접속한 클라이언트 (TcpIpMultichatClient.java)를 HashMap에 저장해서 관리하고 있다.

  • 클라이언트가 멀티채팅서버에 접속하면 HashMap에 저장되고 접속을 해제하면 제거된다.
  • 클라이언트가 데이터를 입력하면, 멀티채팅서버는 HashMap에 저장된 모든 클라이언트에게 데이터를 전송한다.
TcpIpMultichatServer() {
	clients = new HashMap();
    Collections.synchronizedMap(clients); // 동기화 처리

멀티채팅서버의 ServerReceiver쓰레드는 클라이언트가 추가될 때마다 생성되며 클라이언트의 입력을 서버에 접속된 모든 클라이언트에게 전송하는 일을 한다.

void sendToAll(String msg) {
	Iterator it = clients.keySet().iterator(); //**
    
    while(it.hasNext()) {
    	try {
        	DataOutputStream out = (DataOutputStream) clients.get(it.next));
            out.writeUTF(msg);
		} catch(IOException e) {}
    } // while
} // sendToAll
  • run()
    • 클라이언트가 새로 추가되었을 때 클라이언트의 일믕르 key로 클라이언트의 출력 스트림을 HashMap인 clients에 저장해 다른 클라이언트가입력한 데이터를 전송하는데 사용한다.
    • 만일 클라이언트가 종료되어 클라이언트의 입력스트림(in)이 null이되면 while문을 빠져나가서 clients의 목록에서 해당 클라이언트를 제거한다.
public void run() {
	...
    try {
    	...
        clients.put(name, out);
        System.out.println("현재 서버 접속자 수는" + clients.size() + "입니다.);
        while(in != null) {
        	sendToAll(in.readUTf));
        }
    } catch(IOException e ) {
    	// ignore
    } finally {
    	sendToAll("#" + name + "님이 나가셨습니다.");
        clients.remove(name);
        ...
    } // try
} // run    

3) UDP 소켓 프로그래밍

  • UDP 소켓 프로그래밍에서는 DatagramSocketDatagramPacket을 사용한다.
  • UDP는 연결지향적인 프로토콜이 아니기 때문에 ServerSocket이 필요하지 않다.
    • UDP 통신에서 사용하는 소켓은 DatagramSocket이며 데이터를 DatagramPacket에 담아서 전송한다.
  • DatagramPacket
    • 헤더와 데이터로 구성.
      • 헤더: DatagramPacket을 수신할 호스트의 정보(호스트의 주소와 포트)가 저장되어 있다. 소포(packet)에 수신할 상대편의 주소를 적어서 보내는 것과 같다고이해하면 된다.
    • DatagramPacket을 전송하면 DatagramPacket에 지정된 주소(호스트의 포트)의 DatagramSocket에 도착한다.

UdpClient.java

package _ch16;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class UdpClient {

	public void start() throws IOException, UnknownHostException {
		DatagramSocket datagramSocket = new DatagramSocket();
		InetAddress serverAddress = InetAddress.getByName("127.0.0.1");
		
		// 데이터가 저장될 공간으로 byte배열을 생성한다.
		byte[] msg = new byte[100];
		
		DatagramPacket outPacket = new DatagramPacket(msg, 1, serverAddress, 7777);
		DatagramPacket inPacket = new DatagramPacket(msg, msg.length);
		
		datagramSocket.send(outPacket); // DatagramPacket을 전송한다.
		datagramSocket.receive(inPacket); // DatagramPacket을 수신한다.
		
		System.out.println("current server time :" + new String(inPacket.getData()));

		datagramSocket.close();
	} // start
	
	public static void main(String args[]) {
		try {
			new UdpClient().start();
		} catch(Exception e) {
			e.printStackTrace();
		}
	} // main
}

결과

current server time :[02:02:01]

UdpServer.java

package _ch16;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;

public class UdpServer {

	public void start() throws IOException {
		
		// 포트 7777번을 사용하는 소켓을 생성한다.
		DatagramSocket socket = new DatagramSocket(7777);
		DatagramPacket inPacket, outPacket;
		
		byte[] inMsg = new byte[10];
		byte[] outMsg;
		
		while(true) {
			// 데이터를 수신하기 위한 패킷을 생성한다.
			inPacket = new DatagramPacket(inMsg, inMsg.length);
			
			// 패킷을 통해 데잍를 수신(receive)한다.
			socket.receive(inPacket);
			
			// 수신한 패킷으로부터 client의 IP주소와 Port를 얻는다.
			InetAddress address = inPacket.getAddress();
			int port = inPacket.getPort();
			
			// 서버의 현재 시간을 시분초 형태([hh:mm:ss])로 반환한다.
			SimpleDateFormat sdf = new SimpleDateFormat("[hh:mm:ss]");
			String time = sdf.format(new Date());
			outMsg = time.getBytes(); // time읠 byte배열로 변환한다.
			
			// 패킷을 생성해서 client에게 전송(send)한다.
			outPacket = new DatagramPacket(outMsg, outMsg.length, address, port);
			socket.send(outPacket);
		}
	} // start()

	public static void main(String[] args) {
		try {
			// UDP서버를 실행시킨다.
			new UdpServer().start();
		} catch(IOException e) {
			e.printStackTrace();
		}
	} // main
}
  • 서버로부터 서버시간을 전송받아 출력하는 간단한 UDP 소켓 클라이언트와 서버 프로그램.
  • 클라이언트가 DatagramPacket을 생성해 DatagramSocket으로 서버에 전송하면 서버는 전송받은 DatagramPacket의 getAddress(), getPort()를 호출해 클라이언트의 정보를 얻어서 서버시간을 DatagramPacket에 담아서 전송한다.

UdpClient 실행 전 UdpServer를 먼저 실행해야 한다.


Reference

profile
Good Luck!

0개의 댓글