네트워킹(Networking)이란 두 대 이상의 컴퓨터를 케이블로 연결하여 네트워크(network)를 구성하는 것을 말한다. 네트워킹의 개념은 컴퓨터들을 서로 연결하여 데이터를 손쉽게 주고받거나 또는 자원프린터와 같은 주변기기을 함께 공유하고자 하는 노력에서 시작되었다.
java.net 패키지를 이용하면 네트워크 어플리케이션의 데이터 통신 부분 쉽게 작성 가능하다.
클라이언트 / 서버
는 컴퓨터간의 관계를 역할로 구분하는 개념이다.
서버
는 서비스를 제공하는 컴퓨터이고, 클라이언트
는 서비스를 사용하는 컴퓨터가 된다.
일반적으로 서버는 다수의 클라이언트에게 서비스를 제공하기 때문에 고사양의 하드웨어를 갖춘 컴퓨터이지만, 하드웨어의 사양으로 서버와 클라이언트를 구분하는 것이 아니기에 서비스를 제공하는 스프트웨어가 실행되는 컴퓨터를 서버라 한다.
서비스
는 서버가 클라이언트로부터 요청받은 작업을 처리하여 그 결과를 제공하는 것을 뜻하며 서버가 제공하는 서비스의 종류에 따라 파일 서버, 메일 서버 등등이 있다.
서버가 서비스를 제공하기 위해서는 서버 프로그램이 있어야 하고 클라이언트가 서비스를 제공받기 위해서는 서버 프로그램과 연결할 수 있는 클라이언트 프로그램이 있어야 한다. 예를 들어 웹서버에 접속하여 정보를 얻기 위해서는 웹브라우저(클라이언트 프로그램)가 있어야 하고, FTP 서버에 접속해서 파일을 전송받기 위해서는 FTP 클라이언트 프로그램이 필요하다.
네트워크를 구성할 때 전용 서버를 두는 것을 서버기반 모델이라 하고 별도의 전용 서버 없이 각 클라이언트가 서버역할을 동시에 수행하는 것을 P2P모델이라고 한다.
IP주소는 컴퓨터(호스트, host)를 구별하는데 사용되는 고유한 값으로 인터넷에 연결된 모든 컴퓨터는 IP주소를 갖는다. IP주소는 4 byte(32bit)의 정수로 구성되어 있으며, 4개의 정수가 마침표를 구분자로 a.b.c.d
와 같은 형식으로 표현된다. 여기서 a, b, c, d는 부호없는 1byte값, 즉 0 ~ 255 사이의 정수이다.
IP주소는 다시 네트워크 주소와 호스트 주소로 나눌 수 있는데, 32 bit(4 byte)의 IP주소 중에서 네트워크 주소와 호스트 주소가 각각 몇 bit를 차지하는 지는 네트워크를 어떻게 구성하였는지에 따라 달라진다. 그리고 서로 다른 두 호스트의 IP주소의 네트워크 주소가 같다는 것은 두 호스트가 같은 네트워크에 포함되어 있다는 것을 의미한다.
자바에서 IP주소를 다루기 위한 클래스로 InetAddress를 제공하며 다음과 같은 메서드가 정의되어 있다.
URL은 인터넷에 존재하는 여러 서버들이 제공하는 자원에 접근할 수 있는 주소를 표현하기 위한 것으로 프로토콜://호스트명:포트번호/경로명/파일명?쿼리스트링#참조
의 형태로 이루어져 있다.
http://www.codechobo.com:80/sample/hello.html?referer=codechobo#index1
프로토콜 : 자원에 접근하기 위해 서버와 통신하는데 사용하는 통신규약(http)
호스트명 : 자원을 제공하는 서버의 이름(www.codechobo.com)
포트번호 : 통신에 사용되는 서버의 포트번호(80)
경로명 : 접근하려는 자원이 저장된 서버상의 위치(/sample/)
파일명 : 접근하려는 자원의 이름(hello.html)
쿼리 : URL에서 ?이후의 부분(referer=codechobo)
참조 : URL에서 # 이후의 부분(index1)
포트 번호 생략시 http프로토콜에서 사용하는 80번 포트라고 간주한다.
URLConnection은 어플리케이션과 URL간의 통신연결을 나타내는 클래스의 최상위 클래스로 추상클래스이다. URLConnection을 상속받아 구현한 클래스로는 HttpURLConnection과 JarURLConnection이 있으며 URL의 프로토콜이 http 프로토콜이라면 openConnection()은 Http URLConnection을 반환한다. URLConnection을 사용해서 연결하고자하는 자원에 접근하고 읽고 쓰기를 할 수 있다.
소켓 프로그래밍은 소켓을 이용한 통신 프로그래밍을 뜻하는데, 소켓(socket)이란 프로세스간의 통신에 사용되는 양쪽 끝단(endpoint)을 의미한다.
java.net 패키지를 통해 소켓 프로그래밍을 지원하며 통신에 사용하는 프로토콜에 따라 다른 종류의 소켓을 구현하여 제공한다.
TCP/IP 프로토콜은 이기종 시스템간의 통신을 위한 표준 프로토콜로 프로토콜의 집합이다. TCP와 UDP 모두 TCP/IP 프로토콜에 포함되어 있으며, OSI 7계층의 전송계층에 해당하는 프로토콜이다.
TCP를 이용한 통신은 전화에, UDP를 이용한 통신은 소포에 비유된다.
TCP는 데이터를 전송하기 전에 먼저 상대편과 연결을 한 후에 데이터를 전송하며 잘 전송되었는지 확인하고 전송에 실패했다면 해당 데이터를 재전송하기 때문에 신뢰 있는 데이터의 전송이 요구되는 통신에 적합하다. 예를 들면 파일을 주고받는데 적합하다.
UDP는 상대편과 연결하지 않고 데이터를 전송하며, 데이터를 전송하지만 데이터가 바르게 수신되었는지 확인하지 않기 때문에 데이터가 전송되었는지 확인할 길이 없다. 또한 데이터를 보낸 순서대로 수신한다는 보장이 없다.
대신 이러한 확인과정이 필요없기 때문에 TCP에 비해 빠른 전송이 가능하다. 게임이나 동영상의 데이터를 전송하는 경우와 같이 중간에 손실되어 좀 끊기더라도 빠른 전송이 필요할 때 적합하다. 이 때 전송 순서가 바뀌어 늦게 도착한 데이터는 무시하면 된다.
TCP 소켓 프로그래밍은 클라이언트와 서버간 1:1 통신이다.
서버 프로그램과 클라이언트 프로그램간의 통신과정을 단계별로 보면 다음과 같다.
- 서버 프로그램에서는 서버 소켓을 사용해서 서버 컴퓨터의 특정 포트에서 클라이언트의 연결요청을 처리할 준비한다.
- 클라이언트 프로그램은 접속할 서버의 IP주소와 포트 정보를 가지고 소켓을 생성해서 서버에 연결을 요청한다.
- 서버소켓은 클라이언트의 연결요청을 받으면 서버에 새로운 소켓을 생성해서 클라이언트의 소켓과 연결되도록 한다.
- 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 서버소켓과 관계없이 일대일 통신을 한다.
서버소켓(ServerSocket)은 포트와 결합(bind)되어 포트를 통해 원격 사용자의 연결요청을 기다리다가 연결요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결한다. 여기까지가 서버소켓의 역할이고,
실제적인 데이터 통신은 서버소켓과 관계없이 소켓과 소켓 간에 이루어진다.
즉 서버 소켓은 소켓간의 연결만 처리하고 실제 데이터는 소켓 끼리 주고 받는다.
소켓들이 데이터를 주고 받는 연결통로는 바로 입출력 스트림이다.
이렇게 연결되어 있기 때문에 한 소켓에서 출력스트림으로 데이터를 보내면 상대편 소켓에서는 입력스트림으로 받게 된다.
import java.net.*;
import java.io.*;
import java.util.Date;
import java.text.SimpleDateFormat;
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);
// 원격 소켓(클라이언트의 소켓) 에 데이터를 보낸다.
dos.writeUTF("[Notice] Test Message1 from Server.");
System.out.println(getTime() + "데이터를 전송하였습니다. ");
//스트림과 소켓을 닫아준다.
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 현재 시간을 문자열로 변환하는 정수
static String getTime() {
SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
return f.format(new Date());
}
}
위 예제 실행 시 서버소켓이 7777번 포트에서 클라이언트에서 요청 기다린다. 클라이언트 프로그램이 서버에 연결 요청 시 서버 소켓이 새로운 소켓 생성하여 원격 소켓과 연결하고 새로 생성된 소켓은 데이터를 원격 소켓에 전송하고 연결 종료된다.
import java.net.*;
import java.io.*;
public class TcpIpCilent {
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();
}
}
}
이 예제는 이전 예제인 TCP/IP서버와 통신하기 위한 클라이언트 프로그램이다. 연결하고자 하는 서버의 IP와 포트번호를 가지고 소켓을 생성하면 자동적으로 서버에 연결 요청을 하게 된다.
이제 서버와 클라이언트의 연결 과정을 단계별로 그림과 함께 자세히 살펴볼건데 서버의 IP는 192.168.10.100, 클라이언트 IP는 192.168.10.101로 가정하고 시작하겠다.
import java.net.*;
import java.io.*;
import java.util.Date;
import java.text.SimpleDateFormat;
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());
}
} // class
import java.net.*;
import java.io.*;
import java.util.Date;
import java.text.SimpleDateFormat;
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();
}
} // while
} // main
// 현재시간을 문자열로 반환하는 함수
static String getTime() {
SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
return f.format(new Date());
}
} // class
import java.net.*;
import java.io.*;
import java.util.Date;
import java.text.SimpleDateFormat;
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();
}
} // while
} // run
// 현재시간을 문자열로 반환하는 함수
static String getTime() {
String name = Thread.currentThread().getName();
SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
return f.format(new Date()) + name ;
}
} // class
import java.net.*;
import java.io.*;
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
}
import java.net.*;
import java.io.*;
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
import java.net.*;
import java.io.*;
import java.util.*;
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();
}
} // start()
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
import java.net.*;
import java.io.*;
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) {}
} // 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
UDP 소켓 프로그래밍에서는 DatagramSocket과 DatagramPacket을 사용한다. UDP는 연결 지향적인 프로토콜이 아니라서 ServerSocket 필요하지 않다.
UDP 통신에서 사용하는 소켓은 DatagramPacket이며, 데이터를 DatagramPacket에 담아서 전송한다.
DatagramPacket은 헤더(패킷을 수신할 호스트의 정보)와 데이터로 구성되어 있으며, 헤더에는 DatagramPacket을 수신할 호스트의 정보(호스트의 주소와 포트)가 저장되어 있다.
import java.net.*;
import java.io.*;
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); // packet 전송
datagramSocket.receive(inPacket); // packet 수신
System.out.println("current server time:"
+ new String(inPacket.getData()));
datagramSocket.close();
}
public static void main(String[] args) {
try {
new UdpClient().start();
} catch(Exception e) {
e.printStackTrace();
}
}
}
import java.net.*;
import java.io.*;
import java.util.Date;
import java.text.SimpleDateFormat;
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);
socket.receive(inPacket); // 패킷을 통해 데이터 수신
// 수신할 패킷으로 부터 cilent의 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 에게 전송
outPacket = new DatagramPacket(outMsg, outMsg.length, address, port);
socket.send(outPacket);
}
}
public static void main(String[] args) {
try {
new UdpServer().start(); // UDP 서버 실
} catch(IOException e) {
e.printStackTrace();
}
}
}