HTTP 통신과 WS

devAnderson·2022년 3월 28일
0

TIL

목록 보기
81/103

아마 중복되었을 수 있는데, 복습의 개념 및 설명할 수 있는 정도로의 개념으로 재학습한다는 느낌에서 정리해본다

🕹 1. HTTP 의 특징

  1. 무상태성 : 클라이언트를 서버가 기억하지 않는다.

    서버는 요청에 응답만 하는 구조이다. 즉, 어떤 서버 컴퓨터가 응답을 하더라도 동일한 결과를 가져오기 때문에 특정 응답에 특정 컴퓨터가 한정될 필요가 없으므로 확정이 아주 용이하다.
    단, 요청 자체가 로그인 서비스와 같이 클라이언트의 특정 정보를 서버가 알고있어야 한다면 클라이언트가 보유하고 있는 특정 정보를 서버에 전달하여 이것의 유효성을 검증하는 방식으로 무상태성을 보완한다.

  2. 비연결성 : 실제 요청을 주고받은 이후에는 클라이언트와 서버간의 연결이 끊어진다

    일반적으로 http 1.0에서는 TCP/IP 연결을 할 때에 3 way handshake를 통해 서버와 연결을 구축하는데, 이때 요청에 대한 응답이 완료된 이후에는 서로간의 연결을 끊어버린다.

이 비연결성의 특징은 불필요한 연결이 지속됨으로 인해 발생하는 서버의 자원소모를 억제할 수 있다는 장점이 있지만 이용자 수가 많고 요청이 빈번하게 발생하는 환경에서 비연결성은 오히려 감당할 수 없을만큼 반복되는 요청으로 인해 TCP/IP 연결을 시도하려고 하여 네트워크 혼잡을 발생시킬 수 있다.

예를들어, 단순하게 HTTP요청으로 html 파일을 받았다 하더라도 그 안에 존재하는 css, js파일에 대해서 또다시 추가로 요청이 갈 것이므로 매번 TCP/IP 연결을 해줘야 한다. 이것은 몹시 비효율적인 방식이다.

따라서 HTTP 2.0 이후부터는 특정 자원에 대해 예측되는 모든 요청이 완료된 이후 연결을 종료하는 방식으로 해당 한계점을 보완한다. 예를들어 html 파일을 전송했다면 그 안에 예측되는 css, js 파일들의 요청들이 전부 다 완료된 이후에 TCP/IP 연결을 해제하는 방식이다.

🕹 2. HTTP polling & Long polling

HTTP 는 기본적으로 비연결성, 즉 요청에 대해 응답을 보내면 연결을 해제하는 방식을 취하기 때문에 실시간 소통에는 어울리지 않는다.

더욱이 HTTP통신에서 서버는 클라이언트를 기억하지 않는 무상태성의 특징을 가지기 때문에 특정 클라이언트가 요청을 보내기 전까지는 응답을 보내줄 수가 없다. 역으로 이야기하면 특정 클라이언트에 대해 요청을 서버가 보내야 한다면 해당 클라이언트의 정보를 서버가 기억하고 있어야 한다는 논리가 성립된다.

HTTP polling은 실시간 업데이트를 위해 최대한 HTTP 통신의 기본 스펙을 이용하는 방식을 사용한다.

즉, 클라이언트는 서버에게 주기적으로 요청을 보내서 필요한 응답을 받아 이것을 반영하는 방식으로 실시간 업데이트를 진행하는 것이다. 이는 서버에 몹시 부담이 갈 수밖에 없다.

Long polling이라는 대체 방법이 등장하긴 했지만 이것 역시 완벽하지 않다.

이 방법은 리퀘스트를 브라우저에서 전송했을 때 만약 데이터가 변경된 점이 없다면 그냥 계속해서 연결을 유지한 상태를 갖게 되고, 동일정보에 대해서는 응답이 없이 서버와 연결되는 상태만 유지하기 때문에 자신이 보유한 서버의 스넵샷 데이터를 그대로 사용한다.

하지만 서버의 데이터가 실시간으로 mutation 되어 자신이 가진 데이터가 stale해진다면 그제서야 서버는 연결되어 있던 브라우저에게 새로운 데이터를 전송하고 연결이 끊어지는 형식을 가진다. 브라우저는 해당 응답을 받게 되면 자신도 역시 서버와의 연결을 끊고 다시 서버에 재연결을 요청한다.

위와 같은 long polling 방식은 서버 데이터가 이미 낡은 것이 되었다고 판단이 될 때에만 조건적으로 응답을 전송하는 형태를 가지기 때문에 기존의 주기적인 요청을 날리는 polling 방식보다는 개선되었다고 할 수 있지만 만약 앱 자체가 서버의 데이터가 많이 변동하는 특징을 가질 경우 일반 polling과 별반 다를바가 없어진다는 단점이 존재한다.

🕹 3. Web Socket

위와 같은 HTTP 통신의 한계를 극복하면서 실시간 통신을 구현하려면 결국 클라이언트와 서버가 서로간의 연결을 계속 유지하는 상태를 보유한 통신이 존재해야 한다는 결론이 날 수밖에 없다.

이를 위해 만들어 진 것이 바로 웹소켓이다.
웹소켓은 OSI 7계층에서 마지막 계층인 7계층 application layer에 속한다. HTTP 역시 동일한 계층에 존재하는데, WS 연결이 이루어지면 HTTP가 아닌 WS로 프로토콜이 변경된다.

해당 프로토콜은 서버가 클라이언트에게 요청이 없더라도 응답을 보낼 수 있도록 표준화된 스펙을 제공한다.

연결 방식은 기존 http 1.1 버전을 이용하여 헤더에 "Upgrade: WebSocket" 와 랜덤키를 서버에 전송하는 방식으로 시작된다.

**GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13**

연결이 성공하면 서버는 101 상태코드와 함께 WS 연결이 완성되었음을 알린다.

🕹 4. Socket.io

Socket.io는 WS를 기반으로 만들어진 실시간 소통 라이브러리이다.

WS는 이미 그 자체로 훌륭한 실시간 소통이 가능하지만, connection이 불가능하여진 상황에서는 일정 시간동안 buffer을 통해 전송할 데이터를 보유했다가 re-connect가 이루어졌을때 다시 데이터를 전달해주거나, 아예 WS가 사용이 불가능한 브라우저에게는 대체 방법(ex, long polling)을 사용해주는 등 여러가지 실시간 소통에 있어서 발생할 수 있는 예외 사항들을 대신 처리해주는 편리한 툴이다.

이때, Socket.io는 위와 같은 예외처리를 위해서 특수한 헤더들을 여러개 더 추가하는 방식으로 이루어져있기 때문에 Socket.io를 사용하고자 한다면 필수적으로 클라이언트와 서버 둘 다 Socket.io가 구축되어 있어야 한다.

Socket.io는 Room이라는 특수한 공간개념을 가진다.
모든 서버와의 연결이 이루어진 클라이언트의 socket은 기본적으로 자신의 해시 아이디를 기반으로 한 room에 접속하도록 되어있다. (private message의 구현이 쉽게 할 수 있도록 설계되어있다고 설명한다)

const app = require(‘express’)();
const server = require(‘http’).Server(app);
const io = require(‘socket.io’)(server);

io.on("connection", (socket) => {
  socket.on("private message", (anotherSocketId, msg) => {
    socket.to(anotherSocketId).emit("private message", socket.id, msg);
  });
});

위에서 io는 초기화가 완료된 서버측 실시간 통신객체이다. 이 객체의 메서드 "on"을 이용하면 클라이언트 측에서 보내는 요청에 대해 감지할 수 있다.

즉 위의 예시에 따르면 클라이언트와 connection이 완료된 이후 콜백함수의 인자로 전달되는 socket 은 타겟 클라이언트의 소켓을 의미한다.

위에 따르면 private message라고 하는 이벤트를 가지고 전송된 요청에 대해서 on이 감지를 완료하였고, 이때 콜백으로 전달된 타 유저의 socekt Id를 통해 "to" 라는 메서드를 이용하여 타겟팅하고, "emit"이라는 메서드를 통해
필요한 데이터들을 인자로 전송하면서 통신하는 것을 확인할 수 있다.

위와 같은 Default room이 아니더라도, 커스텀제작된 room에도 참여할 수 있다.

io.on("connection", socket => {
  socket.join("some room");
  io.to("some room").emit("someone joined")
});

서버의 io 객체는 connection이 감지되면 해당 클라이언트 소켓을 "some room" 이라는 방에 전달하는 것을 알 수 있다. 그 이후 some room 에 있는 모든 소켓들에게 emit을 통해 메세지를 전달할 수 있다.
(to는 마치 then처럼 연결되어 여러 room을 연결하여 동작하는 것이 가능하다)

여담으로 namespace라는 개념도 존재한다

namespace는 비유를 하자면 routing의 분기점과 같은 역할을 한다.
socket.io가 초기화되면 여기에는 마치 초기 폴더처럼 "/"에 대한 공간이 존재한다.
(공식문서에 따르면 사실상 io.on ... 이라는 내용 자체가 io.of("/").on ... 과 동일한 것이라고 한다)

io.on("connection", (socket) => {});
io.use((socket, next) => { next() });
io.emit("hello");
// are actually equivalent to
io.of("/").on("connection", (socket) => {});
io.of("/").use((socket, next) => { next() });
io.of("/").emit("hello");

만약 다른 io에 대한 namespace로 분기점을 만들고 싶다면 아래와 같이 하면 된다

//from client
const socket = io("http://localhost:8000").connect(); // the main namespace
const roomNamespace = io("http://localhost:8000/room").connect(); 
const chatNamespace = io("http://localhost:8000/chat").connect(); 

roomNamespace.on("some event", (dataString)=>{
	console.log(dataString
})


///from server 
//http://localhost:8000
const room = io.of('/room'); 
const chat = io.of('/chat'); 
// room 네임스페이스 전용 이벤트 
room.on('connection', (socket) => { 
  console.log("one socket connected on room namespace")
  
  socket.emit("some event","data string")
}
        
chat.on('connection", (socket)=>{
	console.log("one socket connected on chat namespace"        
})
profile
자라나라 프론트엔드 개발새싹!

0개의 댓글