[Python] Websocket을 사용하는 방법


Development note/Python  2020. 1. 19. 09:00

안녕하세요. 명월입니다.


이 글은 Python에서 Websocket을 사용하는 방법에 대한 글입니다.


Websocket은 웹 브라우져에서 javascript로 TCP 동기 통신을 위한 통신 프로토콜입니다. 제가 이전에 Java로 websocket을 구현한 적이 있는데 참고하시면 이해하기 편하실 것입니다.

링크 - [Java] 웹 소켓 (WebSocket)


이런 웹 소켓을 C#에서는 signalR을 사용하고 Java에서는 표준 Websocket을 사용합니다.(SignalR이라고 웹소켓을 사용하지 않는 것은 아니고 MVC Framework에 맞게 표준 websocket에서 여러 기능을 추가한 것입니다.)

그런데 저의 경우는 Python에서 왜 웹 소켓을 사용할까 하는 것에 PHP에서는 웹소켓을 구현하기가 힘듭니다. 그 이유는 여러가지가 있는데 제가 생각하기에는 PHP에서 직접적으로 Thread를 관리하기가 어렵기 때문이지 않을까 싶습니다.


웹 소켓이라고 하지만 실제는 http 프로토콜로 tcp socket접속을 해서 해더의 handshake를 하여 http 접속을 비동기식(request & response)처리를 하고 접속을 끊는 행위가 아니라 동기식으로 접속을 유지해야 합니다.

그렇다면 접속을 관리해야 하는 멀티 쓰레드를 만들어야하고 그렇다면 쓰레드를 관리하는 풀과 제어가 만들어져야 하는데 그 점이 PHP에서는 관리하기가 어렵지 않을까 싶습니다.

PHP는 단순히 한번의 request와 response에 대한 스크립트로 표현되어 있기 때문에 한계가 있습니다.


그래서 할 수 있는 대안으로 node.js의 socket.io와 python의 websocket이 있습니다.

구글링을 하면 PHP의 웹 소켓 대안으로 socket.io가 많이 설명이 되어있네요. 언젠가 기회가 되면 node.js도 포스팅을 하고 싶습니다만, 개인적으로 node.js를 좋아하지 않아요.

node.js가 언어적으로 한계가 있다라기 보다는 python이라는 훌륭한 로컬 스크립트가 있는데.. 굳이 왜?라는 인식이 있기 때문입니다.

그리고 socket.io는 정확히 websocket 기술이 아닙니다. AJAX Long Polling기술이라고 이게 http 프로토콜에서 request & response 후에 접속을 바로 팍 끊는 게 아니고 약간 갭을 주는 것입니다.

그래서 데이터가 바뀌면 브라우져로 하여금 재 요청을 하게 하는 것인데.. 유저가 느끼기에는 http의 풀링으로 마치 접속이 연결을 유지하는 듯한 효과를 내는 방법입니다.

그러나 이게 결국은 tcp로 동기적으로 묶여 있는 것이 아니기 때문에 여러가지 버그가 발생합니다. 대표적인게 반응이 느린 점이 있겠네요.


그래서 저의 경우는 이 python으로 websocket을 구현하는 게 가장 좋지 않을까 생각됩니다.

python에서 웹 소켓을 사용하려면 라이브러리를 다운로드 받아야 합니다.

pip install websockets

설치가 완료가 되었으면 websocket 스크립트를 작성하겠습니다.

import asyncio;
# 웹 소켓 모듈을 선언한다.
import websockets;

# 클라이언트 접속이 되면 호출된다.
async def accept(websocket, path):
  while True:
    # 클라이언트로부터 메시지를 대기한다.
    data = await websocket.recv();
    print("receive : " + data);
    # 클라인언트로 echo를 붙여서 재 전송한다.
    await websocket.send("echo : " + data);

# 웹 소켓 서버 생성.호스트는 localhost에 port는 9998로 생성한다. 
start_server = websockets.serve(accept, "localhost", 9998);
# 비동기로 서버를 대기한다.
asyncio.get_event_loop().run_until_complete(start_server);
asyncio.get_event_loop().run_forever();

그리고 클라이언트는 로컬의 적당한 장소에 html파일로 작성합니다.(python으로 웹서버를 만든 것이 아니기 때문에 웹 브라우져를 실행해야할 html파일이 필요합니다.)

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Insert title here</title>
  </head>
<body>
  <form>
    <!-- 서버로 메시지를 보낼 텍스트 박스 -->
    <input id="textMessage" type="text">
    <!-- 전송 버튼 -->
    <input onclick="sendMessage()" value="Send" type="button">
    <!-- 접속 종료 버튼 -->
    <input onclick="disconnect()" value="Disconnect" type="button">
  </form>
  <br />
  <!-- 출력 area -->
  <textarea id="messageTextArea" rows="10" cols="50"></textarea>
  <script type="text/javascript">
    // 웹 서버를 접속한다.
    var webSocket = new WebSocket("ws://localhost:9998");
    // 웹 서버와의 통신을 주고 받은 결과를 출력할 오브젝트를 가져옵니다.
    var messageTextArea = document.getElementById("messageTextArea");
    // 소켓 접속이 되면 호출되는 함수
    webSocket.onopen = function(message){
      messageTextArea.value += "Server connect...\n";
    };
    // 소켓 접속이 끝나면 호출되는 함수
    webSocket.onclose = function(message){
      messageTextArea.value += "Server Disconnect...\n";
    };
    // 소켓 통신 중에 에러가 발생되면 호출되는 함수
    webSocket.onerror = function(message){
      messageTextArea.value += "error...\n";
    };
    // 소켓 서버로 부터 메시지가 오면 호출되는 함수.
    webSocket.onmessage = function(message){
      // 출력 area에 메시지를 표시한다.
      messageTextArea.value += "Recieve From Server => "+message.data+"\n";
    };
    // 서버로 메시지를 전송하는 함수
    function sendMessage(){
      var message = document.getElementById("textMessage");
      messageTextArea.value += "Send to Server => "+message.value+"\n";
      //웹소켓으로 textMessage객체의 값을 보낸다.
      webSocket.send(message.value);
      //textMessage객체의 값 초기화
      message.value = "";
    }
    function disconnect(){
      webSocket.close();
    }
  </script>	
</body>	
</html>

웹 브라우져에서 위에 작성한 html를 실행시키면 javascript에서 python websocket서버로 접속을 합니다. 그리고 hello와 test의 메시지를 작성해서 보냈는데 server측에서는 hello와 test의 메시지를 받아서 콘솔에 출력을 했고 브라우저에서는 echo : 가 붙은 메시지가 표시가 되었습니다.

사실 여기까지만 해도 python에서 웹 소켓을 사용하는 데 문제없습니다.


그런데 python에서는 websocket의 서버 뿐아니라 client의 입장에서도 서버측으로 메시지를 보낼 수가 있네요.

제가 예전에 어플리케이션에서 Websocket을 사용할 수 있으면 Tomcat으로도 어플리케이션을 구현할 수 있겠다고 글을 쓴 적이 있습니다.

링크 - [Java] Tomcat의 websocket을 이용해서 socket서버 만들기


이게 Python에서는 구현이 되어있네요..

import asyncio
# 웹 소켓 모듈을 선언한다.
import websockets

async def connect():
  # 웹 소켓에 접속을 합니다.
  async with websockets.connect("ws://localhost:8080/WebSocketExample/websocket") as websocket:
    # 10번을 반복하면서 웹 소켓 서버로 메시지를 전송합니다.
    for i in range(1,10,1):
      await websocket.send("hello socket!!");
      # 웹 소켓 서버로 부터 메시지가 오면 콘솔에 출력합니다.
      data = await websocket.recv();
      print(data);
# 비동기로 서버에 접속한다.
asyncio.get_event_loop().run_until_complete(connect())
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.server.ServerEndpoint;
 
@ServerEndpoint("/websocket")
public class websocket {
  /***
   * 웹 소켓이 연결되면 호출되는 이벤트
   */
  @OnOpen
  public void handleOpen(){
    System.out.println("client is now connected...");
  }
  /**
   * 웹 소켓으로부터 메시지가 오면 호출되는 이벤트
   * @param message
   * @return
   */
  @OnMessage
  public String handleMessage(String message){
    System.out.println("receive from client : "+message);
    String replymessage = "echo "+message;
    System.out.println("send to client : "+replymessage);
    return replymessage;
  }
  /**
   * 웹 소켓이 닫히면 호출되는 이벤트
   */
  @OnClose
  public void handleClose(){
    System.out.println("client is now disconnected...");
  }
  /**
   * 웹 소켓이 에러가 나면 호출되는 이벤트
   * @param t
   */
  @OnError
  public void handleError(Throwable t){
    t.printStackTrace();
  }
}

클라이언트(python)에서 웹 소켓서버(java)로 접속을 하니 잘 됩니다. 이렇게 되면 자이썬(java에서 사용되는 python) IronPython등을 이용하면 예전에 제가 만든 C# 프로그램처럼 websocket 프로토콜을 1부터 만들 필요가 없을 듯 싶네요.


여기까지 Python에서 Websocket을 사용하는 방법에 관한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.