CHAPTER 23. Socket Programming 알아가기
오늘은 자바의 네트워크 프로그래밍에서 소켓의 개념과 응용에 대해서 포스팅해보려 합니다!
이전 포스팅을 보면 I/O Stream과 같은 메소드로 타입을 바꾸고 그 바뀐 타입으로 여러 가지 기본적인 통신을 알아보았습니다.
자 그런데 그 통신은 default 값이라고 말할 수 있습니다. 즉, 윈도우에서 제공하는 아주 기본적이고 단순한 구조의 통신이라는 정리할 수 있습니다.
기본적인 통신구조는 잘 알려져있어 보안에도 취약합니다. 그래서 나만의 통신구조를 설계하는데 그것을 바로 소켓 프로그래밍이라고 합니다.
그럼 하나씩 알아봅시다.
■ 프로토콜(Protocol)
프로토콜(Protocol)이란 서로 다른 컴퓨터 간의 의사소통을 위한 통신 규약입니다. 즉, 무슨 행동을 수행할 것인지 약속을 해야 하며, 언제, 어떻게 호출할 것인지 순서를 정해야 합니다.
1 ) 프로토콜(Protocol)의 종류
TELNET : 텍스트 기반의 원격접속 서비스 (리눅스 서버 접속 등)
IP (Internet Protocol)
TCP (Transmission Control Protocol)
UDP (User Datagram Protocol)
FTP (File Transfer Protocol)
SMTP (Simple Mail Transfer Protocol)
HTTP (Hyper Text Transfer Protocol)
POP3 (Post Office Protocol)
DHCP (Dynamic Host Control Protocol)
ARP(Address Resolution Protocol) : IP 주소를 물리적 주소(MAC)로 변환
네트워크에서는 크게 두가지 프로토콜로 TCP와 UDP라고 말할 수 있습니다. 그럼 TCP와 UDP는 어떤 프로토콜일까??
가 ) TCP (Transmission Control Protocol)
TCP는 인터넷상에서 데이터를 메세지의 형태로 보내기 위해 IP와 함께 사용하는 프로토콜입니다. 즉, 연결형에 신뢰성을 바탕을 둔 전송 프로토콜입니다. (HTTP, Email, File transfer에서 사용)
나 ) UDP (User Datagram Protocol)
UDP는 데이터를 데이터그램 단위로 처리하는 프로토콜 입니다. TCP와 반대로 비연결성의 비신뢰성의 프로토콜이며, 패킷 유실 시에 다시 재전송해 주는 TCP와 달리 UDP는 그런 게 없이 재전송 따위는 없이 보냅니다.(DNS , 실시간 동영상 서비스에 사용)
TCP와 UDP를 좀 더 섬세하게 비교를 해보자면 다음과 같습니다.
프로토콜 종류 | TCP | UDP |
연결 방식 | 연결형 서비스 | 비연결형 서비스 |
패킷 교환 방식 | 가상 회선 방식 | 데이터그램 방식 |
전송 순서 | 전송 순서 보장 | 전송 순서가 바뀔 수 있음 |
수신 여부 확인 | 수신 여부를 확인함 | 수신 여부를 확인하지 않음 |
통신 방식 | 1:1 통신 | 1:1 OR 1:N or N:N 통신 |
신뢰성 | 높다 | 낮다 |
속도 | 느리다 | 빠르다 |
■ Socket(소켓)
Socket이란 네트워크 프로그래밍을 위한 인터페이스로서 프로그램이 네트워크에서 데이터를 송수신할 수 있도록 "네트워크 환경에 연결할 수 있게 만들어진 논리적 연결부"가 바로 "네트워크 소켓(socket)"입니다.
Socket 클래스는 데이터를 보내는 쪽(클라이언트)에서 객체를 생성하여 사용합니다. 데이터를 받는 쪽(서버)에서 클라이언트 요청을 받으면, 요청에 대한 Socket 객체를 생성하여 데이터를 처리합니다. 즉, Socket 클래스는 서버 쪽이든, 클라이언트 쪽이든 원격에 있는 장비와의 연결 상태를 보관하고 있다고 생각하면 될듯 합니다.
그렇다면 서버에서는 어떻게 데이터를 받을까?? 서버에서는 ServerSocket 클래스를 사용하여 데이터를 받습니다.
방금 서버에서 요청에 대한 Socket객체를 만든다고 했는데, 이 객체는 별도로 new 키워드를 사용하여 만들 필요는 없고 ServerSocket 클래스에서 제공하는 메서드에서 클라이언트 요청이 생기면 Socket 객체를 생성하여 전달해 줍니다.
자 그럼 코드를 통해서 알아봅시다.
■ Socket 구현
<예제 1>
import java.net.ServerSocket;
//서버소켓 : 서버에서 서비스를 위한 목적으로 만드는 소켓
//소켓 : 일반클라이언트에서 사용되는 소켓
//포트번호 : 0~65535 내에서 배정이 가능하고 중복되지 않아야 함.
//1port 1service
//well known port number(자주 사용되어지는 포트 번호)
//80 : 웹서비스 , 21 : FTP서비스 , 445 : 파일공유, 3389 : 원격접속...
public class SocketExam {
public static void main(String[] args) {
ServerSocket socket = null;
for (int i=0; i<=65535; i++){
try {
socket=new ServerSocket(i);//서버소켓생성
socket.close();//소켓 서비스 종료
}catch (Exception e){
System.out.println(i+"번 포트는 사용중입니다.");
}
}
System.out.println("포트 검사를 마쳤습니다.");
}
}
// 소켓을 사용할 땐 출력된 소켓은 피해서 작업해야함
위 예제 코드는 소켓을 응용하여 포트번호를 출력하는 코드입니다. 포트번호는 중복되지 않아야 합니다.(중복 시 에러 발생!)
제가 사용하고 있는 포트 번호 및 출력 결과는 다음과 같습니다.
<결과 1>
5000번 포트는 사용중입니다.
7000번 포트는 사용중입니다.
47317번 포트는 사용중입니다.
47337번 포트는 사용중입니다.
49152번 포트는 사용중입니다.
49153번 포트는 사용중입니다.
49154번 포트는 사용중입니다.
49155번 포트는 사용중입니다.
49156번 포트는 사용중입니다.
49157번 포트는 사용중입니다.
49158번 포트는 사용중입니다.
49159번 포트는 사용중입니다.
49160번 포트는 사용중입니다.
49161번 포트는 사용중입니다.
49162번 포트는 사용중입니다.
49167번 포트는 사용중입니다.
52542번 포트는 사용중입니다.
52610번 포트는 사용중입니다.
55555번 포트는 사용중입니다.
55704번 포트는 사용중입니다.
57965번 포트는 사용중입니다.
58343번 포트는 사용중입니다.
58371번 포트는 사용중입니다.
58378번 포트는 사용중입니다.
60527번 포트는 사용중입니다.
61326번 포트는 사용중입니다.
61647번 포트는 사용중입니다.
61648번 포트는 사용중입니다.
61649번 포트는 사용중입니다.
61650번 포트는 사용중입니다.
61651번 포트는 사용중입니다.
61652번 포트는 사용중입니다.
61673번 포트는 사용중입니다.
61692번 포트는 사용중입니다.
61693번 포트는 사용중입니다.
61694번 포트는 사용중입니다.
62456번 포트는 사용중입니다.
62482번 포트는 사용중입니다.
62508번 포트는 사용중입니다.
64357번 포트는 사용중입니다.
64358번 포트는 사용중입니다.
64457번 포트는 사용중입니다.
65104번 포트는 사용중입니다.
65116번 포트는 사용중입니다.
포트 검사를 마쳤습니다.
Process finished with exit code 0
포트 번호를 출력한 이유는 현재 사용하고 있는 포트 번호를 알아두고 코딩할 때 피해서 작업하기 위함입니다.
그럼 포트번호를 활용하여 TCP와 UDP 소켓 예제 코드도 알아봅시다.
출력할 땐 꼭 서버 쪽 먼저 출력하시고 클라이언트 출력을 하셔야 합니다.
1 ) TCP Server 구현
<예제 2>
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class ChatServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = null;
try {
//서비스를 위한 포트 개방
serverSocket = new ServerSocket(5555); // 포트번호
System.out.println("서비스가 시작되었습니다.");
}catch (Exception e){
e.printStackTrace();
System.out.println("서비스를 시작할 수 없습니다.");
System.exit(1);//비정상 종료
}
Socket clientSocket = null;
try {
//클라이언트의 접속을 기다리다가 접속하면 소켓연결
clientSocket = serverSocket.accept();
System.out.println("클라이언트의 ip : "+clientSocket.getInetAddress().getHostAddress());
}catch (Exception e){
e.printStackTrace();
}
//발신용 스트림
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
//수신용 스트림
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String receive = "";
String send= "Welcome!!!";
writer.println(send);// 서버가 클라이언트한테 메시지 보내기
Scanner sc = new Scanner(System.in);
while (true){
receive = reader.readLine(); //한 라인 읽기
if (receive == null || receive.equals("quit")){//종료 조건
break;
}
System.out.println("[client]"+ receive);
System.out.println("서버님 입력하세요(종료: quit) : ");
send = sc.nextLine();
writer.println(send);// 메시지 보내기
if (send.equals("quit"))break;
}
//리소스 정리
sc.close();
writer.close();
reader.close();
clientSocket.close();
serverSocket.close();
}
}
포트번호는 겹치면 안 되고, Try~Catch문으로 예외 처리를 하셔야 합니다.
그럼 이제 클라이언트 쪽 예제를 알아봅시다.
<예제 3>
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class ChatClient {
public static void main(String[] args) throws Exception{
Socket socket = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
socket = new Socket("127.0.0.1",5555);
writer = new PrintWriter(socket.getOutputStream(),true);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}catch (Exception e){
e.printStackTrace();
}
String receive= "";
String send;
Scanner sc = new Scanner(System.in);
while (true){
receive = reader.readLine(); //메시지 읽음
System.out.println("[server]" + receive);
if (receive.equals("quit"))break;
System.out.println("클라이언트님 입력하세요(종료:quit) : ");
send = sc.nextLine();//키보드 입력
if (send.equals("quit")){
System.out.println("종료되었습니다.");
break;
}
if (send != null){
writer.println(send);//메시지 보내기
}
}
//리소스 정리
sc.close();
writer.close();
reader.close();
socket.close();
}
}
위의 IP주소는 127.0.0.1인 이유는 저의 주소를 그대로 사용하게 만들도록 하기 위함입니다. 클라이언트는 포트번호 5555로 서버 소켓에 연결합니다. 아래는 서버와 클라이언트를 실행한 화면입니다.
앞서 말씀드린 것처럼 서버부터 실행하고 다음 클라이언트를 실행해야 합니다.
출력 결과를 확인해 보면 서버와 클라이언트의 소켓정보가 일치됨을 확인할 수 있고 클라이언트는 서버로부터 메시지를 잘 받아왔습니다.
그렇다면 이번에는 UDP Server 구현을 해봅시다.
2 ) UDP Server 구현
UDP 방식은 데이터를 바이트배열로 전송합니다. String을 바로 보낼 순 없습니다. 또한 UDP 통신은 데이터를 패킷(데이터 조각)으로 만들어서 전송해야 합니다.
그럼 서버 코드 먼저 알아봅시다.
<예제 4>
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPEchoServer {
public UDPEchoServer(int port){
try {
DatagramSocket ds = new DatagramSocket(port);
while (true){
//udp 방식은 데이터를 바이트배열로 전송함,String을 바로 보낼 순 없음
//(참고 : 바이트배열은 최대크기는 65508)
byte buffer[] = new byte[512];
//udp방식의 통신은 데이터를 패킷(데이터 조각)으로 만들어서 전송
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
System.out.println("ready");
//클라이언트가 보낸 메시지를 수신하기 위한 코드
// 소켓을 통해 수신된 클라이언트의 메시지를 DatagramPacket에 저장
ds.receive(dp);
// 클라이언트의 메시지(바이트배열)를 String으로 변환
String str = new String(dp.getData());
System.out.println("클라이언트에서 보낸 메시지 : "+ str);
InetAddress ia = dp.getAddress();//클리이언트측 ip주소를 get
port = dp.getPort();//클라이언트측 port번호 get
System.out.println("cilient ip"+ ia+", clinet port : "+ port);
//클라이언트에게 전송할 패킷객체 생성
//(바이트배열 , 바이트배열 크기, ip주소 , 포트번호)
dp = new DatagramPacket(dp.getData(),dp.getData().length,ia,port);
ds.send(dp);//클라이언트에게 자료 전송
ds.close();//소켓 닫기
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new UDPEchoServer(3000);// 서버측 포트번호 세팅
}
}
이번 코드는 매개변수로 포트번호를 받을 수 있게 세팅을 해보았습니다.
그럼 바로 클라이언트 코드 알아봅시다.
<예제 5>
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPEchoClient {
private String str;
private BufferedReader file;
private static final int SERVERPORT= 3000;//서버측 포트번호 셋팅
public UDPEchoClient(String ip, int port){
try {
InetAddress ia = InetAddress.getByName(ip);
DatagramSocket ds = new DatagramSocket(port);
System.out.println("서버측에서 보내는 messge : ");
file = new BufferedReader(new InputStreamReader(System.in));
str = file.readLine();// 한 라인씩 읽음
byte[] buffer = str.getBytes();//스트링을 바이트배열로 변환 (UDP통신에서는 바이트 배열로 송수신)
DatagramPacket dp = new DatagramPacket(buffer, buffer.length,ia,SERVERPORT);
ds.send(dp);
buffer = new byte[512]; // 서버에서 보낸 메시지를 저장하기 위한 바이트배열 처리
dp = new DatagramPacket(buffer, buffer.length);
ds.receive(dp);//서버에서 보낸 메시지를 수신
ds.close();//소켓 연결 닫기
System.out.println("server ip : "+ dp.getAddress() + ", server port : "+ dp.getPort());
System.out.println("서버에서 보낸 메시지 : "+ new String(dp.getData()).trim());
// trim(): 여백 처리
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new UDPEchoClient("localhost",2000);//(클라이언트 ip, 클라이언트 포트번호)
//UDP는 클라이언트 포트번호와 서버측 포트번호가 달라야 한다.
}
}
위 코드를 보시면 출력 결과를 Server ip, port 번호까지 출력값으로 나오게 코드를 구현해 보았습니다.
참고로 UDP는 TCP와 다르게 클라이언트 포트번호와 서버 측 포트번호가 달라야 합니다.
결과는 아래와 같습니다.
마치며
여기까지 Socket을 생성하여 연결하는 방법과 네트워크 전송에 사용하는 각 메서드들을 짤막한 사용법도 알아보았습니다.
네트워크 프로그래밍 개념을 바탕으로 응용하기 나름이겠지만 채팅 애플리케이션도 만들 수 있습니다. 컴퓨터 프로그램에서 네트워크는 절대 빠질 수 없는 개념이니 자바로 꼭 익혀두시는 게 좋습니다!
'[ JAVA ] > JAVA' 카테고리의 다른 글
[ Java ] Lambda (0) | 2023.01.17 |
---|---|
[ Java ] File 클래스 (0) | 2023.01.16 |
[ Java ] 객체 직렬화(Object serialization) (1) | 2023.01.11 |
[ Java ] I/O Stream (2) | 2023.01.10 |
[ Java ] GUI 프로그래밍 응용 (2) | 2023.01.09 |