소켓 통신과 HTTP 통신을 비교해보려고 합니다.
프로젝트를 하며 챗팅 프로그램을 구현해야하는데
"소켓 통신"을 이용해야 한다는 말을 듣고 평상시에 명확하지 않았던 개념을 이번에 정리하겠습니다.
네트워크를 통해 서버로부터 데이터를 가져오기 위한 통신을 크게
"소켓" 통신과 "HTTP" 통신이 있습니다.
이 둘의 차이점과 각 특징을 정리하겠습니다.
HTML 파일을 전송하는 프로토콜이라는 의미를 가지지만 현재는 JSON, Image 파일 등 또한 전송합니다.
HTTP 통신은 몇 가지 특징을 가지고 있습니다.
start-line
header
empty line
message body
✅ HTTP start-line
➡️ HTTP 요청 start-line
GET /search?q=hello&hl=ko HTTP/1.1
method 공백 request-targer 공백 HTTP-version
⬅️ HTTP 응답 start-line
HTTP/1.1 200 OK
HTTP-version 공백 status-code 공백 response-phrase
✅ HTTP Header
위 그림과 같이 HTTP Header는 일반 헤더와 요청/응답 헤더 그리고 엔티티 헤더로 구성되어져 있습니다.
⏺️ 일반(General) 헤더
요청 및 응답 메세지 모두에서 공통적으로 사용 가능한 일반 목적의 헤더 항목입니다.
포함된 정보는 아래와 같습니다.
⏺️ 엔티티(Entity) 헤더
요청 및 응답 메시지에서도 모두 사용 가능, HTTP 바디에 내용이 존재할 때 이 내용에 대한 세부 정보를 나타내는데 사용합니다.
보통 응답 메세지에 Content 헤더들이 포함되어 있습니다.
➡️ HTTP 요청 헤더
요청한 URL, 메소드, 요청 생성에 사용된 브라우저 및 기타 정보와 같은 요청에 대한 정보가 포함된다.
⬅️ HTTP 응답 헤더
✅ HTTP 메세지 바디
실제 전송할 데이터를 담는 부분으로 html, 이미지, 영상, JSON 등 byte로 표현할 수 있는 모든 데이터 전송
소켓이란 프로세스간의 통신에 사용되는 양쪽 끝단을 의미합니다.
서로 멀리 떨어진 두 사람이 통신하기 위해 전화기가 필요한 것처럼
프로세스간의 통신을 위해서는 그 무언가가 필요하고 그것이 바로 소켓입니다.
즉 네트워크상에서 동작하는 프로그램 간 통신의 종착점이며 종착점은 IP와 Port번호의 조합으로 이루어진 최종 목적지를 나타냅니다.
HTTP 통신과 다르게 Server와 Client가 특정 Port를 통해 연결을 성립하고 있어 실시간으로 양방향 통신을 하는 방식입니다.
실시간 streaming 중계나 실시간 채팅과 같이 즉각적으로 정보를 주고 받는 경우에 사용합니다.
자바에서는 java.net 패키지를 통해 소켓 프로그래밍을 지원합니다.
TCP = 📞 = 전화
UDP = 📦 = 소포
TCP와 UDP 모두 TCP/IP 프로토콜에 포함되어 있으며, OSI 7계층의 전송계층에 해당하는 프로토콜입니다.
TCP와 UDP는 전송 방식이 다르며, 각 방식에 따른 장단점이 있습니다.
항목 | TCP | UDP |
---|---|---|
연결 방식 | 연결기반(connection-oriented) - 연결 후 통신(전화기) - 1:1 통신방식 | 비연결기반(connectionless-oriented) - 연결없이 통신(소포) 1:1, 1:n, n:n 통신방식 |
특징 | 데이터의 경계를 구분안함(byte-stream) 신뢰성 있는 데이터 전송 - 데이터의 전송 순서가 보장된다. -데이터의 수신여부를 확인한다. (데이터가 손실되면 재전송) - 패킷을 관리할 필요가 없다. UDP보다 전송 속도가 느리다. | 데이터의 경계를 구분한다(datagram) 신뢰성 없는 데이터 전송 - 데이터의 전송 순서가 바뀔 수 있다. - 데이터의 수신여부를 확인하지 않는다. (데이터가 손실되어도 알 수 없다) - 패킷을 관리해주어야 한다. TCP보다 전송속도가 빠르다. |
관련 클래스 | Socker ServerSocket | DatagramSocket DatagramPacket MulticastSocket |
- 서버 프로그램에서는 서버 소켓을 사용해서 서버 컴퓨터의 특정 포트에서 클라이언트의 연결요청을 처리할 준비를 한다.
ServerSocket serverSocket = new ServerSocket(7777);
- 클라이언트 프로그램은 접속한 서버의 IP주소와 포트 정보를 가지고 소켓을 생성해서 서버에 연결을 요청한다.
Socket socket = new Socket("192.168.45.184", 7777);
- 서버소켓은 클라이언트의 연결요청을 받으면 서버에 새로운 소켓을 생성해서 클라이언트 소켓과 연결되로록 한다.
Socket socket = serverSocket.accept();
- 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 서버소켓과 관계없이 일대일 통신을 한다.
즉 서버소켓의 역할은 포트와 결합되어 포트를 통해 원격 사용자의 연결요청을 기다리다가 연결요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결하는 것입니다.
실제 데이터 통신은 서버소케과 관계없이 소켓과 소켓 간에 이루어집니다.
소켓은 두 개의 스트림을 가지고 있습니다.
⏺️ Server 프로그램입니다.
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 TcpIpServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try{
//서버 소켓을 생성하여 7777번 포트와 결합
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());
}
}
⏺️ Client 프로그램입니다.
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.util.SortedMap;
public class TcpIpClient{
public static void main(String[] srgs){
try {
String serverIp = "192.168.45.184";
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();
}
}
}
위에서
String serverIp = "192.168.45.184";
은 서버프로그의 IP 주소입니다.!
중요한 것 몇가지를 살펴보겠습니다.
연결을 요청한 클라이언트 프로그램의 소켓은 사용한 임의의 포트가 선택됩니다. 예시는 서버소켓이 7777번의 포트를 사용합니다. 이 7777번 포트는 서버소켓이 아닌 다른 소켓이 사용할 수 있습니다!
클라이언트 프로그램의 소켓이 7777번일 수도 있습니다.
클라이언트 프로그램으로부터 접속 요청이 오면 서버 프로그램에서 아래와 같이 포트 번호를 출력하는 부분을 추가했습니다.
그 결과는 아래와 같습니다. 보면 클라이언트 프로그램의 포트 번호를 실행할 때마다 임의로 생성된다는 것을 알 수 있습니다.
ServerSocket 클래스의 setSoTimeout(int timeout)을 사용해서 서버소켓의 대기시간을 지정할 수 있습니다. 클라이언트 프로그램의 접속 요청이 올 때까지 무한정 기다리는 것이 아니라 제한시간을 두어 제한 시간 이내에 요청이 들어오지 않으면 accept()에서 SocketTimeoutException이 발생합니다.
아래의 결과를 보면 처음에는 5초 이내에 클라이언트 프로그램으로부터 연결 요청이 왔지만 이후 5초가 지나니 서버 프로그램이 종료되었습니다.
쓰레드를 생성하여 클라이언트의 요청을 동시에 처리하는 예시입니다.
서버에 접속하는 클라이언트가 많을 때 요청한 순서대로 처리하면 늦게 접속한 클라이언트는 오랜 시간 기다려야 합니다. 따라서 병렬적으로 동시에 처리하는 쓰레드를 적용한 예시를 살펴봅시다.
서버는 배열을 통해서 여러개를 만들었습니다.
TcpServer4의 생성자는 아래와 같습니다. 생성자에서 서버소켓을 생성하여 7777번 포트와 결합합니다.
run()과 start()메서드는 아래와 같습니다.
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);
//원격 소켓에 데이터를 보낸다.
dos.writeUTF("[Notice] Test Message1 from Server.");
System.out.println(getTime() + "데이터를 전송했습니다.");
//스트림과 소켓을 닫아준다.
dos.close();
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
아래의 결과를 보면 연결요청을 기다리는 쓰레드 5개가 한번에 같이 연결요청을 기다립니다.
서버와 클라이언트 프로그램에서는 두 종류의 스트림 InputStream과 OutputStream 모두 존재합니다.
보면 두개의 클래스를 선언하여 socket의 데이터를 쓰는 것과 데이터를 읽는 것을 분리하여 만들었습니다.
socket에 데이터를 쓰는 Sender
socket에 데이터를 읽는 Receiver
그리고 마지막에 클라이언트 프로그램입니다.
신기하게 1:1 채팅이 구현되었습니다.
TCP와 다르게 연결지향적인 프로토콜이 아니기 때문에 서버소켓이 필요하지 않습니다.
사용하는 소켓은 DatagramSocket이며 데이터는 DatagramPacket에 담아서 전송합니다.
DatagramPacket = 헤더(수신할 호스트의 정보) + 데이터
import javax.xml.crypto.Data;
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);
datagramSocket.receive(inPacket);
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.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 {
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);
//수신할 패킷으로부터 client의 IP주소와 Port를 얻는다,
InetAddress address = inPacket.getAddress();
int port = inPacket.getPort();
//서버의 현재 시간을 시분초 형태로 변환한다.
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();
}catch (IOException e){
e.printStackTrace();
}
}
}
자바의 정석 chapter 16 네트워킹
https://bentist.tistory.com/35
https://kotlinworld.com/75