네트워킹(Networking)
네트워킹(Networking)이란 두 대 이상의 컴퓨터를 케이블로 연결하여 네트워크(Network)를 구성하는 것을 말한다.
자바에서 제공하는 java.net패키지를 사용하면 이러한 네트워크 어플리케이션의 데이터 통신 부분을 쉽게 작성할 수 있다.
클라이언트/서버(client/server)
클라이언트와 서버는 컴퓨터간의 관계를 역할(role)로 구분하는 개념이다.
- 서비스를 제공하는 쪽이 서버, 제공받는 쪽이 클라이언트가 된다.
서버(server) | 서비스를 제공하는 컴퓨터(service provider) |
클라이언트(client) | 서비스를 사용하는 컴퓨터(service user) |
서비스는 서버가 클라이언트로부터 요청받은 작업을 처리하여 그 결과를 제공하는 것을 뜻하며 서버가 제공하는 서버의 종류에 따라 메일서버(email server), 파일서버(file server), 웹서버(web server) 등이 있다.
네트워크를 구성할 때 전용서버를 두는 것을 서버기반 모델(server-based model), 전용서버없이 각 클라이언트가 서버역할까지 동시에 수행하는 것을 P2P 모델(peer-to-model)이라고 한다.
IP주소(IP address)
IP주소는 컴퓨터(host, 호스트)를 구별하는데 사용되는 고유한 값으로 인터넷에 연결된 모든 컴퓨터는 IP주소를 갖는다.
IP주소는 4 byte(32bit)의 정수로 구성되어 있으며 4개의 정수가 마침표를 구분자로 ‘a.b.c.d’와 같은 형식으로 표현된다.(a,b,c,d는부호없는 1byte의 값 0~255의 정수이다.)
IP주소는 네트워크주소와 호스트주소로 나눌 수 있는데, 32bit(4byte)의 IP주소중에서 네트워크 주소와 호스트주소가 각각 몇 bit를 차지하는 지는 네트워크를 어떻게 구성하였는지에 따라 달라진다.
- 네트워크주소가 같은 두 호스트는 같은 네트워크에 존재한다.
- IP주소와 서브넷마스크를 ‘&’연산하면 네트워크주소를 얻는다.
- 네트워크 주소가 차지하는 자리수가 많을 수록 호스트 주소의 범위가 줄어들기 때문에 네트워크의 규모가 작아진다.
- 위의 예시의 경우 호스트 주소가 8bit이므로 256개의 호스트만 네트워크에 포함될수 있다. (0, 255와 같은 호스트 주소는 특별한 의미로 사용되기 때문에 사용 불가)
InetAddress
자바에서는 IP주소를 다루기 위한 클래스로 InetAddress를 제공하며 다음과 같은 메서드 들이 정의되어 있다.
InetAddress ip = null;
InetAddress[] ipArr = null;
// getByName() : 도메인을 통해 Ip 주소를 얻는다.(InetAddress객체 반환)
ip = InetAddress.getByName("www.naver.com");
System.out.println("getHostName : "+ip.getHostName()); // 호스트의 이름을 반환한다.
System.out.println("getHostAddress : "+ip.getHostAddress()); // 호스트의 Ip주소를 반환한다.(String)
System.out.println("toString : " + ip.toString());
//getHostName : www.naver.com
//getHostAddress : 223.130.195.95
//toString : www.naver.com/223.130.195.95
// getAddress() : IP주소를 byte배열로 반환한다.(byte[])
byte[] ipAddr = ip.getAddress();
// 자바에서 기본형인 byte는 부호가 있기 때문에 표현 범위기 (-128 ~127)
System.out.println("getAddress() : " + Arrays.toString(ipAddr));
String result = "";
for(int i=0; i < ipAddr.length; i++) {
// 부호가 없는 1byte의 값으로 표현
result += (ipAddr[i] < 0) ? ipAddr[i] + 256 : ipAddr[i];
result +=".";
}
System.out.println("getAddress() + 256 :" + result);
//getAddress() : [-33, -126, -61, 95]
//getAddress() + 256 :223.130.195.95.
// getLocalHost() : 지역호스트의 IP주소를 반환한다.(InetAddress객체 반환)
ip = InetAddress.getLocalHost();
System.out.println("getHostName() : "+ ip.getHostName()); // getHostName() : 호스트의 이름을 반환한다.
System.out.println("getHostAddress() : "+ ip.getHostAddress()); // getHostAddress() : 호스트의 IP주소를 반환한다.
//getHostName() : DESKTOP-SJ9C79T
//getHostAddress() : 192.168.0.156
// getAllByName() : 도메인에 지정된 모든 호스트의 IP주소를 배열에 담아 반환한다.
ipArr = InetAddress.getAllByName("www.naver.com");
for(int i =0; i < ipArr.length;i++) {
System.out.println("IpArr["+i+"] :"+ ipArr[i]);
}
// IpArr[0] :www.naver.com/223.130.195.95
// IpArr[1] :www.naver.com/223.130.200.104
URL(Uniform Resource Location)
URL은 인터넷에 존재하는 여러 서버들이 제공하는 자원에 접근할 수 있는 주소를 표현하기 위한 것으로 '프로토콜://호스트명:포트번호/경로명/파일명?쿼리스트링#참조'의 형태로 이루어져 있다.
프로토콜 | 자원에 접근하기 위해 서버와 통신하는데 사용되는 통신규약 (http) |
호스트명 | 자원을 제공하는 서버의 이름(www.naver.com) |
포트번호 | 통신에 사용되는 서버의 포트번호(80) |
경로명 | 접근하려는 자원이 저장된 서버상의 위치(/sample/) |
파일명 | 접근하려는 자원의 이름(hello.html) |
쿼리 | URL에서 '?'이후의 부분(referer=javachobo) |
참조 | URL에서 '#'이후의 부분 (index1) |
포트번호, 쿼리, 참조는 생략할 수 있다. 각 프로토콜에 따라 사용하는 포트번호가 다르며 생략되면 각 프로토콜의 기본 포트가 사용된다.
- Http 프로토콜은 80번 포트를 사용하기 때문에 URL에서 포트번호를 생략하는 경우 80으로 간주한다.
자바에서는 URL을 다루기 위한 클로스로 URL클래스를 제공하며 다음과 같은 메서드가 정의되어 있다.
생성자 | 설명 |
URL(String spec) | 지정된 문자열 정보의 URL객체를 생성한다. |
URL(String protocol, String host, String file) | |
URL(String protocol, String host, int port, String file) |
메서드 | 설명 |
String getAuthority() | 호스트명과 포트를 문자열로 반환한다. |
Object getContent() | URL의 Content객체를 반환한다. |
Object getContent(Class[] classes) | URL의 Content객체를 반환한다. |
int getDefaultPort() | URL의 기본 포트를 반환한다(http = 80) |
String getFile() | 파일명을 반환한다. |
String getHost() | 호스트명을 반환한다. |
String getPath() | 경로명을 반환한다. |
int getPort() | 포트를 반환한다. |
String getProtocol() | 프로토콜을 반환한다. |
String getQuery() | 쿼리를 반환한다. |
메서드 | 설명 |
String getRef() | 참조(anchor)를 반환한다. |
String getUserInfo() | 사용자정보를 반환한다. |
URLConnection openConnection() | URL과 연결된 URLConnection을 얻는다. |
URLConnection openConnection(Proxy proxy) | URL과 연결된 URLConnection을 얻는다. |
InputStream openStream() | URL과 연결된 URLConnection의 InputStream을 얻는다. |
boolean sameFile(URL other) | 두 URL이 서로 같은 것인지 알려준다. |
void set(String protocol, String host, int port, String file, String ref) | URL객체의 속성을 지정된 값으로 설정한다. |
void set(String protocol, String host, int port, String authority, String userinfo, String path, String query, String ref) | URL객체의 속성을 지정된 값으로 설정한다. |
String toExternalForm() | URL을 문자열로 변환하여 반환한다. |
URL toURL() | URL을 URI로 변환하여 반환한다. |
URLConnection
URLConnection은 어플리케이션과 URL간의 통신연결을 나타내는 클래스의 최상위 클래스로 추상클래스이다.
- URLConnection을 상속받아 구현한 클래스로는 HttpURLConnection와 JarURLConnection이 있다.
- URL의 프로토콜이 http라면 openConnection()은 HttpURLConnection을 반환한다.
- URLConnection을 사용해서 연결하고자하는 자원에 접근하고 읽고 쓰기를 할 수 있다.
URLConnection을 생성하고 get메서드를 통해 정보를 얻어 출력한다.
URL url = null;
String address = "https://www.naver.com/";
url = new URL(address);
// openConnection() : URL과 연결된 URLConnection을 얻는다.
URLConnection conn = url.openConnection(); // URL의 메서드
System.out.println(conn.getContent());
URL에 연결하여 내용을 읽어오는 예제
URL url = null;
String address = "https://www.naver.com/";
BufferedReader input = null;
String line = "";
url = new URL(address);
// openStream() : URL과 연결된 URLConnection의 InpuStream을 얻는다.
input = new BufferedReader(new InputStreamReader(url.openStream()));// URL의 메서드
// openStream()은 openConnection()을 호출해서 URLConnection을 얻은 다음 getInputStream()을 호출한 것과 같다.
// url.openConnection().getInputStream()
while((line = input.readLine()) != null) {
System.out.println(line);
}
input.close();
소켓 프로그래밍
소켓 프로그래밍은 소켓(socket)을 이용한 통신 프로그래밍을 뜻한다. 소켓이란 프로세스 간의 통신에 사용되는 양쪽 끝단(endpoint)를 의미한다.
- 서로 떨어진 두 사람이 통신하기 위해서 전화기가 필요한 것처럼, 프로세스간의 통신을 위해서필요한 것이 소켓이다.
- 자바에서는 java.net패키지를 통해 소켓 프로그래밍을 지원하는데, 소켓통신에 사용되는 프롵토콜에 따라 다른 종류의 소켓을 구현하여 제공한다.
TCP와 UDP
TCP/IP 프로토콜은 이기종 시스템간의 통신을 위한 표준 프로토콜로 프로토콜의 집합니다.
- TCP와 UDP모두 TCP/IP프로토콜에 포함된 프로토콜. OSI 7계층의 전송계층에 해당하는 프로토콜이다.
- TCP와 UDP는 전송방식이 다르며, 각 방식에 따른 장단점이 있다.
TCP소켓 프로그래밍
TCP는 데이터를 전송하기 전에 먼저 상대편과 연결을 한 후에 데이터를 전송하며 잘 전송되었는지 확인하고 전송에 실패했다면 데이터를 재전송하여 신뢰있는 데이터 전송이 요구되는 통신에 적합하다.
- TCP는 클라이언트와 서버간의 일대일(1:1)이다.
- 서버가 먼저 실행되어 클라이언트의 연결요청을 기다리고 있어야 한다.
서버/클라이언트 프로그램 간의 단계별 통신과정
1. 서버는 서버소켓을 사용해서 서버의 특정포트에서 클라이언트의 연결요청을 처리할 준비를 한다.
2. 클라이언트는 접속할 서버의 IP주소와 포트정보로 소켓을 생성해서 서버에 연결을 요청한다.
3. 서버소켓은 클라이언트의 연결요청을 받으면 서버에 새로운 소켓을 생성해서 클라이언트의 소켓과
연결되도록 한다.
4. 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 서버소켓과 관계없이 일대일(1:1) 통신을 한다.
- 실제적인 데이터 통신은 서버소켓과 관계없이 소켓과 소켓간에 이루어진다.
서버소켓(serversocket)은 포트와 결합(bind)되어 포트를 통해 원격 사용자의 연결요청을 기다리다가 연결요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결한다.
이렇게 생성된 소켓을 통해 실제 데이터는 소켓들 끼리 주고 받는다. 소켓은 두개의 스트림 입력스트롬과 출력스트림을 가지고 있으며, 이 스트림들은 연결된 상대편 소켓의 스트림들과 교차 연결된다.
- 여러 소켓이 하나의 포트를 공유해서 사용할 수 있지만 서버소켓은 포트를 독점한다.
- 두 서버소켓이 서로 다른 프로토콜을 사용하는 경우에는 같은 포트를 사용할 수 있다.
- 소켓은 호스트(컴퓨터)가 외부와 통신을 하기 위한 통로로 하나의 호스트가 65536개의 포트를 가지고 있으며 포트는 번호로 구별된다.(0 ~ 65535의 범위)
자바에서는 TCP를 이용한 소켓 프로그래밍을 위해 Socket과 ServerSocket 클래스를 제공한다.
Socket | 프로세스산의 통신을 담당하며 InputStream과 OutputStream을 가지고 있다. 이 두 스트림을 통해 프로세스간의 통신(입출력)이 이루어진다. |
ServerSocket | 포트와 연결(bind)되어 외부의 연결요청을 기다리다 연결요청이 들어오면 Socket을 생성해서 소켓과 소켓간의 통신이 이루어지도록 한다. |
간단한 서버 구현 예제
public class TcpServer {
@SuppressWarnings("resource")
public static void main(String[] args) {
ServerSocket serversocket = null;
try {
// 1. 서버소켓을 생성한다.
// 서버 소켓을 생성하여 7777번 포트와 결합 한다.
serversocket = new ServerSocket(7777);
System.out.println("[Server]"+getTime()+" 서버 소켓이 준비되었습니다.");
} catch (IOException e) {}
while(true) {
try {
System.out.println("[Server]"+getTime()+ " 연결요청을 기다립니다.");
// 2. 서버소켓이 클라이언트 프로그램의 연결요청을 처리할 수 있도록 대기상태로 만든다.
// 4. 서버 소켓은 클라이언트 프로그램의 연결요청을 받아 새로운 소켓을 생성하여 클라이언트의 소켓과 연결한다.
Socket socket = serversocket.accept();
System.out.println("[Server]"+getTime()+" " + socket.getInetAddress()+ "로부터 연결요청이 들어왔습니다." );
// 생성된 소켓의 출력스트림을 얻는다.
OutputStream out = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(out);
// 원격(remote socket)에 데이터를 보낸다.
dos.writeUTF("[Test Message From server]");
System.out.println("[Server]"+getTime()+ " 데이터를 전송했습니다.");
// 스트림과 소켓을 닫아준다.
dos.close();
socket.close();
} catch (IOException e) {}
}
}
}
- 서버소켓을 생성한뒤 서버소켓이 클라이언트 프로그램의 연결요청을 처리할 수 있도록 대기상태로 만든다.(accept())
public class TcpClient {
public static void main(String[] args) throws UnknownHostException {
try {
String serverIp = InetAddress.getLocalHost().getHostAddress();
// 3. 클라이언트 프로그램에서 소켓을 생성하여 서버소켓에 연결을 요청한다.
System.out.println("[Client] 서버에 연결을 시도 합니다.");
Socket socket = new Socket(serverIp, 7777);
// 소켓의 입력 스트림을 얻는다.
InputStream in = socket.getInputStream();
DataInputStream dis = new DataInputStream(in);
// 소켓으로 부터 받은 데이터를 출력한다.
System.out.println("[To Server] " + dis.readUTF());
System.out.println("[Client] 연결을 종료합니다.");
dis.close();
socket.close();
System.out.println("[Client]연결이 종료되었습니다.");
} catch (IOException e) {}
}
}
- 클라이언트 프로그램에서 연결하고자 하는 서버의 IP번호와 포트번호를 가지고 소켓을 생성하면 자동적으로 서버에 연결요청을 하게 된다.
- 클라이언트 프로그램의 연결요청이 오면 새로운 소켓을 생성해서 클라이언트 프로그램의 소켓과 연결한다.
여러개의 쓰레드를 생성하여 클라이언트의 요청을 동시에 처리하는 예제
클라이언트의 수가 많을 때는 쓰레드를 이용해 병렬로 처리
// 쓰레드 구현
public class TcpServer implements Runnable{
ServerSocket serversocket;
Thread[] threadArr;
public static void main(String[] args) {
TcpServer server = new TcpServer(5);
server.start();
}
public TcpServer(int num) {
try {
serversocket = new ServerSocket(7777);
System.out.println(getTime()+"서버가 준비되었습니다.");
// 빈 쓰레드 배열 생성
threadArr = new Thread[num];
} catch (IOException e) {}
}
public void start() {
for(int i = 0; i < threadArr.length; i++) {
threadArr[i] = new Thread(this);
threadArr[i].start();
}
}
@Override
public void run() {
while(true) {
try {
System.out.println("[Server]"+getTime()+ " 연결요청을 기다립니다.");
Socket socket = serversocket.accept();
System.out.println("[Server]"+getTime()+" "
+ socket.getInetAddress()+ "로부터 연결요청이 들어왔습니다." );
// 생성된 소켓의 출력스트림을 얻는다.
OutputStream out = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(out);
// 원격(remote socket)에 데이터를 보낸다.
dos.writeUTF("[Test Message From server]");
System.out.println("[Server]"+getTime()+ " 데이터를 전송했습니다.");
// 스트림과 소켓을 닫아준다.
dos.close();
socket.close();
} catch (IOException e) {}
}
}
static String getTime() {
String name = Thread.currentThread().getName();
SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
return f.format(new Date()) + name;
}
}
- 프로그램이 실행되어 main 쓰레드가 생성(main() 메서드 실행)
- 생성자 TcpServer(int num)이 호출되며 TcpServer을 생성, TcpServer의 static 변수(serversocket, threadArr)초기화
- 생성된 TcpServer객체의 참조변수를 통해 start()를 호출, Runnable을 구현한 TcpServer의 객체로 쓰레드를 생성 하여 threadArr배열을 초기화후 쓰레드를 실행
[08:50:27]main서버가 준비되었습니다.
[Server][08:50:27]Thread-1 연결요청을 기다립니다.
[Server][08:50:27]Thread-0 연결요청을 기다립니다.
[Server][08:50:27]Thread-2 연결요청을 기다립니다.
[Server][08:50:27]Thread-3 연결요청을 기다립니다.
[Server][08:50:27]Thread-4 연결요청을 기다립니다.
[Server][08:50:29]Thread-2 /192.168.0.156로부터 연결요청이 들어왔습니다.
[Server][08:50:29]Thread-2 데이터를 전송했습니다.
[Server][08:50:29]Thread-2 연결요청을 기다립니다.
......
UDP소켓 프로그래밍
UDP는 상대편과 연결하지 않고 데이터를 전송하며, 데이터를 전송하지만 데이터가 바르게 수신되어 있는지 확인하지 않는다.
- 데이터를 보낸 순서대로 수신한다는 보장이 없다.
- 확인 과정이 필요하지 않기 때문에 TCP에 비해 빠른 전송이 가능하다.
TCP 소켓 프로그래밍 과정에서는 Socket과 ServerSocket을 사용하지만, UDP소켓 프로그래밍에서는 DatagramSocket과 DatagramPacket을 사용한다.
UDP 통신에서 사용하는 소켓은 DatagramSocket이며 데이터를 DatagramPacket에 담아 전송한다. DatagramPacket은 헤더와 데이터로 구성되어 있는데 헤더에는 DatagramPacket을 수신한 호스트의 정보(호스트의 주소와 포트)가 저장되어 있으며 DatagramPacket을 전송하면 DatagramPacket에 지정된 주소(호스트의 포트)의 DatagramSocket에 도착한다.
- 서버로부터 서버시간을 전송받아 출력하는 간단한 UDP소켓 클라이언트와 서버 프로그램이다.
public class UdpClient {
public void start() throws IOException {
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);
datagramSocket.receive(inPacket);
System.out.println("current server time :" + new String(inPacket.getData()));
datagramSocket.close();
} // start()
public static void main(String[] args) throws IOException {
try {
new UdpClient().start();
} catch (Exception e) {
}
} // main
}
클라이언트가 DatagramPacket을 생성해서 DatagramSocket으로 서버에 전송하면, 서버는 전송받은 DatagramPacket의 getAddress(), getPort()를 호출해서 클라이언트의 정보를 얻어서 서버시간을 DatagramPacket에 담아서 전송한다.
public class UdpServer {
public void start() throws IOException {
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(Exception e) {}
}
}
'Java' 카테고리의 다른 글
[JAVA] 부동 소수점 방식(Floating-Point Number Representation)과 정밀도 (0) | 2024.04.14 |
---|---|
[JAVA] 스트림(Stream) API(4, Collectors, 그룹화와 분할) (0) | 2024.03.14 |
[JAVA] 스트림(Stream) API(3, 최종 연산) (0) | 2024.03.09 |
[JAVA] 스트림(Stream) API(2, 중간 연산, Optinal<T>) (0) | 2024.02.29 |
[JAVA] 스트림(Stream) API(1) (0) | 2024.02.25 |