Websocket 프로토콜의 원리와 실현(一)

7774 단어
최근 시간이 한가해서 채팅 시스템 구축을 조금 연구하고 그 실현 원리를 깊이 이해한 김에 문장으로 정리해 봤습니다.저는 주로 안드로이드를 쓰기 때문에 구체적인 분석은 모바일 채팅 시스템 구축을 위주로 합니다.
하나의 채팅 시스템은 복잡하지도 복잡하지도 않지만 안정적인 시스템을 실현하려면 고려해야 할 일이 매우 많다.가장 기본적인 것은 채팅 협의의 처리다.흔히 볼 수 있는 실시간 통신 프로토콜은 XMPP,Websocket이다. 대기업들은 일반적으로 스스로 프로토콜을 정의한다. 예를 들어 텐센트, 왕이 같은 것들은 모두 자신의 프로토콜을 사용한다.내가 본 원본 코드는 Leancloud의 실시간 통신 구성 요소이다. 그들의 채팅은 Websocket에 기반을 두고 있기 때문에 이 블로그의 주제는 Websocket이다.Leancloud의 안드로이드 실시간 통신 구성 요소에서 Websocket의 봉인은 Github의 소스 프로젝트인 Nathan Rajlich의 Java-Websocket입니다. 이것은'100% 자바가 쓴 간단한 Websocket 클라이언트와 서버 구현'입니다.
문장의 대략적인 틀
  • Websocket 프로토콜에 대한 간단한 소개
  • 프로토콜의 봉인 및 전송
  • Websocket 클라이언트의 실현
  • Websocket 프로토콜에 대한 간단한 설명
    Websocket은 하나의 TCP 연결에서 듀플렉스 통신을 하는 프로토콜로 듀플렉스(duplex)는 두 통신 설비 간에 양방향의 자료 전송을 허용하는 것을 말한다.전이중은 두 설비 간에 양방향 자료 전송을 동시에 허용하는 것을 말한다.이것은 반이중에 비해 반이중은 양방향 전송을 동시에 할 수 없다. 이 기간의 차이는 휴대전화와 무전기의 차이에 해당한다. 휴대전화는 말을 하는 동시에 상대방의 말을 들을 수 있고 무전기는 하나만 말하고 다른 하나만 말할 수 있다.
    긴 말은 짧게 말하자면 Websocket 프로토콜에서 클라이언트와 서비스 측은 악수하는 동작만 하면 하나의 통로를 형성하고 양자간에 데이터를 서로 전송할 수 있다.
    WebSocket 프로토콜은
  • 악수
  • 데이터 전송
  • 활용단어참조
    클라이언트에서 요청 보내기
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    

    서버 응답
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/
    

    악수를 할 때 클라이언트는 무작위로 Sec-WebSocket-Key를 보내는데 서비스 측은 이 키에 따라 처리를 하고 Sec-WebSocket-Accept의 값을 클라이언트에게 되돌려준다. 구체적인 원리는 뒷글에서 구체적으로 설명한다.
    데이터 전송
    이것은 Websocket의 데이터 전송 프로토콜이다. 채팅 정보는 일반적으로 이 프로토콜의 규칙에 따라 전송된다. 아래 그림의 모든 것을 하나의 데이터 프레임이라고 하는데 데이터 프레임의 프레임과 해석은 이 프로토콜을 처리할 때 가장 번거로운 부분이다.구체적으로 이 시계는 어떻게 보면 참조할 수 있다
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-------+-+-------------+-------------------------------+
    |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
    |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
    |N|V|V|V|       |S|             |   (if payload len==126/127)   |
    | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
    |     Extended payload length continued, if payload len == 127  |
    + - - - - - - - - - - - - - - - +-------------------------------+
    |                               |Masking-key, if MASK set to 1  |
    +-------------------------------+-------------------------------+
    | Masking-key (continued)       |          Payload Data         |
    +-------------------------------- - - - - - - - - - - - - - - - +
    :                     Payload Data continued ...                :
    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    |                     Payload Data continued ...                |
    +---------------------------------------------------------------+
    
        bit   
    FIN      1bit          
    RSV 1-3  1bit each            0
    Opcode   4bit    ,    
    Mask     1bit   ,      ,      1 (     )
    Payload  7bit      
    Masking-key      1 or 4 bit   
    Payload data     (x + y) bytes   
    Extension data   x bytes      
    Application data y bytes      
    

    프로토콜의 봉인과 전송
    1. 악수 프로토콜의 봉인과 전송
    Handshake 클래스는 요청 헤드에 따라
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    

    이 요청 헤더의 필드 순서가 임의이기 때문에, 우리는 하나의 맵으로 메시지를 저장할 수 있으며, 메시지를 보낼 때 Socket의 출력 흐름을 쓸 수 있다
    다음은 Handshakedata 클래스입니다. 글을 쉽게 읽을 수 있도록 코드를 간소화했습니다.
    public class Handshakedata
    {
        private byte[] content;                 //   ,          
        private TreeMap map;  //       
    
    }
    

    악수 요청 헤더를 초기화하여 코드를 쉽게 이해하기 위해 자바-Websocket의 코드를 약간 수정했습니다.
    public Handshakedata postProcessHandshakeRequestAsClient(Handshakedata request)
    {
        request.put("Upgrade", "websocket");
        request.put("Connection", "Upgrade");
        request.put("Sec-WebSocket-Version", "8");    
    
        byte[] random = new byte[16];
        this.reuseableRandom.nextBytes(random);     //       Sec-WebSocket-Key
        request.put("Sec-WebSocket-Key", Base64.encodeBytes(random));
    
        return request;
    }
    

    데이터 프레임을 생성합니다. Socket을 통해 메시지를 전송하기 때문에 최종적으로 전송된 내용은 Socket의 OutputStream에 기록해야 합니다. 악수 메시지를bytebuffer로 변환하는 방법이 필요합니다. 이bytebuffer를 통해 흐름에 기록해야 합니다.
    public ByteBuffer createHandshake(Handshakedata handshakedata) {
        StringBuilder bui = new StringBuilder(100);
        bui.append("GET ");
        bui.append(handshakedata.getResourceDescriptor());
        bui.append(" HTTP/1.1");
        bui.append("\r
    "); Iterator it = handshakedata.iterateHttpFields(); while (it.hasNext()) { String fieldname = (String)it.next(); String fieldvalue = handshakedata.getFieldValue(fieldname); bui.append(fieldname); bui.append(": "); bui.append(fieldvalue); bui.append("\r
    "); } bui.append("\r
    "); byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString()); byte[] content = withcontent ? handshakedata.getContent() : null; ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length); bytebuffer.put(httpheader); bytebuffer.flip(); return bytebuffer; }

    마지막으로 Socket의 흐름에 쓰기
    ByteBuffer buffer = (ByteBuffer)WebSocketClient.this.engine.outQueue.take();   //              bytebuffer
    WebSocketClient.this.ostream.write(buffer.array(), 0, buffer.limit());    //this.ostream = this.socket.getOutputStream() Socket    
    WebSocketClient.this.ostream.flush();   //  ,    
    

    이상은 클라이언트가 악수 프로토콜을 보내는 과정입니다.
    클라이언트 수신 서버 응답
    서버에서 클라이언트의 악수 요청을 받은 후 응답을 되돌려야 합니다
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/
    

    이 응답을 받은 후 클라이언트는 Sec-WebSocket-Accept 값을 비교해야 한다. 이 값은 서버가 악수를 하고 연결을 맺는 것에 동의하는 것을 의미한다. 클라이언트가 전송한 Sec-WebSocket-Key와'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'을 연결한 후 SHA-1로 암호화하고 BASE-64로 인코딩한 것이다.
    클라이언트가 Sec-WebSocket-Accept를 받은 후 로컬 Sec-WebSocket-Key를 같은 인코딩하여 비교합니다.
    public Draft.HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response)
    throws InvalidHandshakeException
      {
        if ((!request.hasFieldValue("Sec-WebSocket-Key")) || (!response.hasFieldValue("Sec-WebSocket-Accept"))) {
          return Draft.HandshakeState.NOT_MATCHED;
        }
    
    //Sec-WebSocket-Key Sec-WebSocket-Accept    
        String seckey_answere = response.getFieldValue("Sec-WebSocket-Accept");
        String seckey_challenge = request.getFieldValue("Sec-WebSocket-Key");
        seckey_challenge = generateFinalKey(seckey_challenge);
    
        if (seckey_challenge.equals(seckey_answere))
          return Draft.HandshakeState.MATCHED;
        return Draft.HandshakeState.NOT_MATCHED;
      }
    
     //  Sec-WebSocket-Accept   
    private String generateFinalKey(String in) { 
        String seckey = in.trim();
        String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        MessageDigest sh1;
        try {
          sh1 = MessageDigest.getInstance("SHA1");
        } catch (NoSuchAlgorithmException e) {
          throw new RuntimeException(e);
        }
        return Base64.encodeBytes(sh1.digest(acc.getBytes()));
      }
    

    2. 데이터의 봉인 및 전송
    ....계속

    좋은 웹페이지 즐겨찾기