Please note, this is a STATIC archive of website developer.mozilla.org from November 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

웹소켓 서버 작성하기

번역 작업 진행중입니다.

개요

웹 소켓 서버는 특정한 프로토콜을 따르는 서버의 임의 포트를 리스닝하고 있는 TCP 어플리케이션입니다. 사용자 서버를 만드는 작업은 두려운 일일수도 있습니다.  그러나 어떤 플랫폼에서 간단한 웹소켓 서버를 구현하는것은 쉬울것입니다.  

A WebSocket server can be written in any server-side programming language that is capable of Berkeley sockets, such as C(++) or Python or even PHP and server-side JavaScript. This is not a tutorial in any specific language, but serves as a guide to facilitate writing your own server.

You will need to already know how HTTP works and have medium programming experience. Depending on language support, knowledge of TCP sockets may be required. The scope of this guide is to present the minimum knowledge you need to write a WebSocket server.

Read the latest official WebSockets specification, RFC 6455. Sections 1 and 4-7 are especially interesting to server implementors. Section 10 discusses security and you should definitely peruse it before exposing your server.

A WebSocket server is explained on a very low level here. WebSocket servers are often separate and specialized servers (for load-balancing or other practical reasons), so you will often use a reverse proxy (such as a regular HTTP server) to detect WebSocket handshakes, pre-process them, and send those clients to a real WebSocket server. This means that you don't have to bloat your server code with cookie and authentication handlers (for example).

Step 1: The WebSocket Handshake

First of all, the server must listen for incoming socket connections using a standard TCP socket. Depending on your platform, this may be handled for you already. For an example, let's assume that your server is listening on example.com, port 8000, and your socket server responds to GET requests on /chat.

Warning: The server may listen on any port it chooses, but if it chooses any port other than 80 or 443, it may have problems with firewalls and/or proxies. Connections on port 443 tend to succeed more often but of course, that requires a secure connection (TLS/SSL). Also, note that most browsers (notably Firefox 8+) do not allow connections to insecure WebSocket servers from secure pages.

The handshake is the "Web" in WebSockets. It's the bridge from HTTP to WS. In the handshake, details of the connection are negotiated, and either party can back out before completion if the terms are unfavorable. The server must be careful to understand everything the client asks for, otherwise security issues will be introduced.

클라이언트가 보내는 핸드쉐이크 요청

웹소켓 서버라고 할지라도, 가장 처음에는 HTTP 프로토콜을 통해서 핸드쉐이킹을 수행합니다. 따라서 서버는 클라이언트가 보낸 HTTP 요청을 이해할 수 있어야 합니다. 클라이언트가 처음에 보내는 요청은 아래와 같습니다. (HTTP 버전은 1.1보다 같거나 높아야 합니다. Method는 항상 GET이어야 합니다.)

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

The client can solicit extensions and/or subprotocols here; see Miscellaneous for details. Also, common headers like User-Agent, RefererCookie, or authentication headers might be there as well. Do whatever you want with those; they don't directly pertain to the WebSocket. It's also safe to ignore them. In many common setups, a reverse proxy has already dealt with them.

If any header is not understood or has an incorrect value , the server should send a "400 Bad Request" and immediately close the socket. As usual, it may also give the reason why the handshake failed in the HTTP response body, but the message may never be displayed (browsers do not display it). If the server doesn't understand that version of WebSockets, it should send a Sec-WebSocket-Version header back that contains the version(s) it does understand. (This guide explains v13, the newest). Now, let's move on to the most curious header, Sec-WebSocket-Key.

Tip: All browsers will send an Origin header. You can use this header for security (checking for same origin, whitelisting/ blacklisting, etc.) and send a 403 Forbidden if you don't like what you see. However, be warned that non-browser agents can just send a faked Origin. Most applications will reject requests without this header.

Tip: The request-uri (/chat here) has no defined meaning in the spec. So many people cleverly use it to let one server handle multiple WebSocket applications. For example, example.com/chat could invoke a multiuser chat app, while /game on the same server might invoke a multiplayer game.

Note: Regular HTTP status codes can only be used before the handshake. After the handshake succeeds, you have to use a different set of codes (defined in section 7.4 of the spec).

서버가 보내는 핸드쉐이크 응답

위와 같은 요청을 받으면 서버 역시도 HTTP 구조의 응답을 보내주어야 합니다. 자세한 내용은 아래와 같습니다.(각각의 헤더 끝에는 \r\n을 그리고 가장 마지막에는 한번 더 \r\n을 넣는걸 잊지 마세요.)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Additionally, the server can decide on extension/subprotocol requests here; see Miscellaneous for details. The Sec-WebSocket-Accept part is interesting. The server must derive it from the Sec-WebSocket-Key that the client sent. To get it, concatenate the client's Sec-WebSocket-Key and "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" together (it's a "magic string"), take the SHA-1 hash of the result, and return the base64 encoding of the hash.

FYI: This seemingly overcomplicated process exists so that it's obvious to the client whether or not the server supports WebSockets. This is important because security issues might arise if the server accepts a WebSockets connection but interprets the data as a HTTP request.

So if the Key was "dGhlIHNhbXBsZSBub25jZQ==", the Accept will be "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=". Once the server sends these headers, the handshake is complete and you can start swapping data!

The server can send other headers like Set-Cookie, or ask for authentication or redirects via other status codes, before sending the reply handshake.

Keeping track of clients

This doesn't directly relate to the WebSocket protocol, but it's worth mentioning here: your server will have to keep track of clients' sockets so you don't keep handshaking again with clients who have already completed the handshake. The same client IP address can try to connect multiple times (but the server can deny them if they attempt too many connections in order to save itself from Denial-of-Service attacks).

Step 2: 데이터 프레임 교환

Either the client or the server can choose to send a message at any time — that's the magic of WebSockets. However, extracting information from these so-called "frames" of data is a not-so-magical experience. Although all frames follow the same specific format, data going from the client to the server is masked using XOR encryption (with a 32-bit key). Section 5 of the specification describes this in detail.

데이터 프레임 포멧

모든 데이터 프레임 (서버->클라이언트 / 클라이언트 -> 서버) 은 아래의 구조를 따릅니다:

 0               1               2               3              
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 4               5               6               7              
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
 8               9               10              11             
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
 12              13              14              15
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

RSV1-3 는 사용되지 않습니다. 이 필드들은 확장 프로토콜 또는 미래를 위해 예약되었습니다.

MASK 비트는 메세지가 인코딩되어있는지의 여부를 나타냅니다.클라이언트가 서버로 보내는 메세지는 항상 마스킹되어야합니다. 따라서 서버는 클라이언트로부터 받은 이 필드가 항상 1임을 기대할 수 있습니다. (만약 클라이언트가 마스킹되지 않은 메세지를 보낸다면 서버는 연결을 종료해야 합니다. 참고 : section 5.1 of the spec ).
서버가 클라이언트에게 보내는 메세지는 MASK 필드가 항상 0이고 데이터는 마스킹되지 않은 상태여야 합니다. 마스킹이 어떻게 이루어지는지 / 마스킹된 메세지를 어떻게 디코딩하는지는 나중에 설명합니다.
(주의: wss 연결이라고 하더라도 클라이언트가 보내는 패킷은 항상 마스킹되어야 합니다.)

opcode 필드는 뒤따라오는 payload 데이터가 어떠한 포멧인지를 나타냅니다. 0x0은 continuation, 0x1은 텍스트(항상 UTF-8), 0x2는 바이너리 데이터 / 나머지 값은 "컨트롤 코드"에 사용됩니다. (컨트롤 코드에 대해서는 나중에 다루게 됩니다.) 현재 버전의 웹소켓 프로토콜에서는 0x3 / 0x7 / 0xB~0xF는 아무런 의미도 지니고있지 않습니다.

FIN 비트는 이 메세지가 마지막임을 나타냅니다. 만약 FIN 비트가 0이라면 서버는 뒤에 더 따라오게 될 메세지들까지 수신해야 합니다. FIN 비트가 1일 경우에는 전체 메세지가 수신되었으므로 이를 처리합니다. 이 부분에 대해서는 뒤에 다시 설명합니다.

Payload 길이 알아내기

수신한 프레임으로부터 payload 데이터를 읽기 위해서는 payload length 필드를 읽어야 합니다. 불행히도 이는 약간 복잡한 작업을 거치는데 아래 순서대로 처리해 주세요.

  1. 9번째부터 15번재까지의 비트를 읽습니다. 이를 unsigned integer로 취급한 다음 값이 125거나 이보다 작을 경우 이 자체가 payload length 입니다. 이 경우에는 2, 3 과정은 필요 없습니다. 하지만 126이면 2번으로, 127일 경우 3번으로 가주세요
  2. 다음 16비트를 읽습니다. 이를 unsigned integer로 처리하고 payload length 값으로 사용합니다.
  3. 다음 64비트를 읽습니다. 이를 unsigned integer로 처리하고 payload length 값으로 사용합니다. (최상위 비트는 항상 0이어야 합니다.)

마스킹된 Payload 데이터 디코딩하기

MASK 비트가 설정되어 있디만 32비트 사이즈의 Masking-Key 필드 또한 존재하게 됩니다. 
아래 의사코드는 Payload 데이터를 ENCODED / Masking-Key 필드를 MASK 라고 가정하고 데이터를 디코딩하는 방법을 보여줍니다. ENCODED값을 0부터 순회하면서 MASK의 i % 4에 해당하는 1바이트와 XOR 연산을 수행합니다.

If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

이제 DECODED 데이터를 가지고 프로토콜에 맞게 활용하면 됩니다.

Message Fragmentation

The FIN and opcode fields work together to send a message split up into separate frames.  This is called message fragmentation. Fragmentation is only available on opcodes 0x0 to 0x2.

Recall that the opcode tells what a frame is meant to do. If it's 0x1, the payload is text. If it's 0x2, the payload is binary data. However, if it's 0x0, the frame is a continuation frame. This means the server should concatenate the frame's payload to the last frame it received from that client. Here is a rough sketch, in which a server reacts to a client sending text messages. The first message is sent in a single frame, while the second message is sent across three frames. FIN and opcode details are shown only for the client:

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

Notice the first frame contains an entire message (has FIN=1 and opcode!=0x0), so the server can process or respond as it sees fit. The second frame sent by the client has a text payload (opcode=0x1), but the entire message has not arrived yet (FIN=0). All remaining parts of that message are sent with continuation frames (opcode=0x0), and the final frame of the message is marked by FIN=1. Section 5.4 of the spec describes message fragmentation.

Pings and Pongs: The Heartbeat of WebSockets

핸드쉐이크가 끝난 시점부터 서버 혹은 클라이언트는 언제든지 ping 패킷을 보낼 수 있습니다. 만약 ping 패킷이 수신되면 수신자는 가능한 빨리 응답으로 pong 패킷을 보내야 합니다. (ping에서 전달된 payload와 동일한 payload를 붙여서 pong을 보냅니다. 이 경우 최대 payload length는 125입니다.) 서버는 주기적으로 ping을 보내 클라이언트가 아직 살아있는 상태인지 체크할 수 있습니다.

A ping or pong is just a regular frame, but it's a control frame. Pings have an opcode of 0x9, and pongs have an opcode of 0xA. When you get a ping, send back a pong with the exact same Payload Data as the ping (for pings and pongs, the max payload length is 125). You might also get a pong without ever sending a ping; ignore this if it happens.

If you have gotten more than one ping before you get the chance to send a pong, you only send one pong.

Step 4: Closing the connection

To close a connection either the client or server can send a control frame with data containing a specified control sequence to begin the closing handshake (detailed in Section 5.5.1). Upon receiving such a frame, the other peer sends a Close frame in response. The first peer then closes the connection. Any further data received after closing of connection is then discarded. 

Miscellaneous

WebSocket codes, extensions, subprotocols, etc. are registered at the IANA WebSocket Protocol Registry.

WebSocket extensions and subprotocols are negotiated via headers during the handshake. Sometimes extensions and subprotocols seem too similar to be different things, but there is a clear distinction. Extensions control the WebSocket frame and modify the payload, while subprotocols structure the WebSocket payload and never modify anything. Extensions are optional and generalized (like compression); subprotocols are mandatory and localized (like ones for chat and for MMORPG games).

Extensions

This section needs expansion. Please edit if you are equipped to do so.

Think of an extension as compressing a file before e-mailing it to someone. Whatever you do, you're sending the same data in different forms. The recipient will eventually be able to get the same data as your local copy, but it is sent differently. That's what an extension does. WebSockets defines a protocol and a simple way to send data, but an extension such as compression could allow sending the same data but in a shorter format.

Extensions are explained in sections 5.8, 9, 11.3.2, and 11.4 of the spec.

TODO

서브프로토콜

Think of a subprotocol as a custom XML schema or doctype declaration. You're still using XML and its syntax, but you're additionally restricted by a structure you agreed on. WebSocket subprotocols are just like that. They do not introduce anything fancy, they just establish structure. Like a doctype or schema, both parties must agree on the subprotocol; unlike a doctype or schema, the subprotocol is implemented on the server and cannot be externally refered to by the client.

Subprotocols are explained in sections 1.9, 4.2, 11.3.4, and 11.5 of the spec.

클라이언트는 핸드쉐이크 요청 시에 특정한 서브프로콜의 목록을 같이 보낼 수 있습니다. Sec-WebSocket-Protocol 헤더에 사용하기를 원하는 서브프로토콜의 목록을 같이 보냅니다.

GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

또는 아래와 같이 보낼 수도 있습니다.:

...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp

클라이언트로부터 서브프로토콜 요청을 받으면, 서버는 그 중에서 자신이 지원할 수 있는 서브프로토콜을 하나 골라야 합니다. 만약 클라이언트가 보낸 목록 중, 여러개를 지원할 수 있다면 지원하는 목록 중 가장 첫번째 서브프로토콜을 보내주세요. 

Imagine our server can use both soap and wamp. Then, in the response handshake, it'll send:

Sec-WebSocket-Protocol: soap

서버는 반드시 하나의 Sec-Websocket-Protocol 헤더만을 송신해야 합니다.
만약 서버가 어떠한 서브프로토콜도 지원하고 싶지 않다면 Sec-Websocket-Protocol 헤더를 빼고 보내주세요. 빈 값을 넣어서 보내도 안됩니다.
서버가 아무 서브프로토콜을 지원하지 않겠다고 한다면 클라이언트는 연결을 닫아버릴수도 있습니다.

If you want your server to obey certain subprotocols, then naturally you'll need extra code on the server. Let's imagine we're using a subprotocol json. In this subprotocol, all data is passed as JSON. If the client solicits this protocol and the server wants to use it, the server will need to have a JSON parser. Practically speaking, this will be part of a library, but the server will need to pass the data around.

Tip: To avoid name conflict, it's recommended to make your subprotocol name part of a domain string. If you are building a custom chat app that uses a proprietary format exclusive to Example Inc., then you might use this: Sec-WebSocket-Protocol: chat.example.com. For different versions, a widely-used method is to add a / followed by the version number, like chat.example.com/2.0. Note that this isn't required, it's just an optional convention, and you can use any string you wish.

같이 보기

문서 태그 및 공헌자

 이 페이지의 공헌자: SQLGate, green-autumn, pjc0247
 최종 변경: SQLGate,