I have implemented a simple Java server side example that we can take a look at. I start by creating a ServerSocket that listens for connection to port 2005
public class WebsocketServer { public static final int MASK_SIZE = 4; public static final int SINGLE_FRAME_UNMASKED = 0x81; private ServerSocket serverSocket; private Socket socket; public WebsocketServer() throws IOException { serverSocket = new ServerSocket(2005); connect(); } private void connect() throws IOException { System.out.println("Listening"); socket = serverSocket.accept(); System.out.println("Got connection"); if(handshake()) { listenerThread(); } }
As defined in the RFC standard for the websocket protocol , when a client connects through a website, a handshake must be performed. So let's take a look at the handshake () method, it's pretty ugly, so it will step through it: The first part reads a client handshake.
private boolean handshake() throws IOException { PrintWriter out = new PrintWriter(socket.getOutputStream()); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); //This hashmap will be used to store the information given to the server in the handshake HashMap<String, String> keys = new HashMap<>(); String str; //Reading client handshake, handshake ends with CRLF which is again specified in the RFC, so we keep on reading until we hit ""... while (!(str = in.readLine()).equals("")) { //Split the string and store it in our hashmap String[] s = str.split(": "); System.out.println(str); if (s.length == 2) { keys.put(s[0], s[1]); } }
The client handshake looks something like this (this is what chrome gave me, version 22.0.1229.94 m), in accordance with RFC 1.2!
GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: localhost:2005 Origin: null Sec-WebSocket-Key: PyvrecP0EoFwVnHwC72ecA== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: x-webkit-deflate-frame
Now we can use the key card to create the appropriate response during the handshake. Quote from the RFC:
To confirm that a handshake has been received, the server must accept two pieces of information and combine them to form a response. The first piece of information comes from | Sec-WebSocket-Key | header field in client handshake. For this header field, the server must take a value and combine this with a globally unique identifier, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" as a string that is unlikely to be used by network endpoints that do not understand the WebSocket Protocol. The SHA-1 hash (160 bits), base64 encoded, this concatenation is then returned in the server acknowledgment.
So what should we do! Sec-WebSocket-Key concatenation with magic string, hash using the SHA-1 hash function and Base64-encode. This is what the next ugly single-liner does.
String hash; try { hash = new BASE64Encoder().encode(MessageDigest.getInstance("SHA-1").digest((keys.get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes())); } catch (NoSuchAlgorithmException ex) { ex.printStackTrace(); return false; }
Then we simply return the expected response with a new hash created in the "Sec-WebSocket-Accept" field.
//Write handshake response out.write("HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + hash + "\r\n" + "\r\n"); out.flush(); return true;
}
Now we have established a successful connection to the server between the client and server. And now what? How do we make them talk to each other? We can start by sending messages from the server to the client. NB! We are doing from now on, and not talking to the client with HTTP anymore. Now we have to report sending pure bytes and interpret incoming bytes. So how do we do this?
The message from the server must be in a specific format called "frames", as described in RFC 5.6. When sending a message from the server, the RFC states that the first byte should indicate what it is. A byte with a value of 0x81 tells the client that we are sending a "single-frame unmasked text message", which is basically a text message. The subsequent byte should represent the length of the message. After that, data or payload. Well, okay ... let this be realized!
public void sendMessage(byte[] msg) throws IOException { System.out.println("Sending to client"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedOutputStream os = new BufferedOutputStream(socket.getOutputStream());
So, to send a message to the client, we simply call sendMessage ("Hello, client!". GetBytes ()).
Wasn't that too complicated? How to receive messages from a client? Well, it's a little trickier, but hang in there!
The sendt frame from the client is almost structured like the frame sendt from the server. The first byte is the type of message, and the second byte is the length of the payload. Then there is a difference: the next four bytes are a mask. What mask, and why messages from the client are masked, but there are no messages with the servers? Section 5.1 of the RFC shows that:
... the client SHOULD mask all the frames that it sends to the server ... The server SHOULD not mask any frames that it sends to the client.
So the easy answer is: we just HAVE. Well, why should we, you may ask? Didn't I tell you to read the RFC?
Moving, after a four-byte mask in the frame, the oil payload is displayed. And one more thing: the client must set the 9th leftmost bit in the frame to 1 to inform the server that the message is masked (check the clean ASCII-art frame in the RFC section - section 5.2). The 9th leftmost bit corresponds to our leftmost bit in our second byte, but hey, our byte is the payload length! This means that all messages from our client will have payload length bytes equal to 0b10000000 = 0x80 + actual payload length. Therefore, in order to find out the actual payload length, we must subtract 0x80 or 128 or 0b10000000 (or any other number system you may prefer) from the payload length byte, our second byte in the frame.
Wow, okay .. that sounds complicated ... For you, "TL; DR" -guys, summary: subtract 0x80 from the second byte to get the payload length ...
public String reiceveMessage() throws IOException { //Read the first two bytes of the message, the frame type byte - and the payload length byte byte[] buf = readBytes(2); System.out.println("Headers:"); //Print them in nice hex to console convertAndPrint(buf); //And it with 00001111 to get four lower bits only, which is the opcode int opcode = buf[0] & 0x0F; //Opcode 8 is close connection if (opcode == 8) { //Client want to close connection! System.out.println("Client closed!"); socket.close(); System.exit(0); return null; } //Else I just assume it a single framed text message (opcode 1) else { final int payloadSize = getSizeOfPayload(buf[1]); System.out.println("Payloadsize: " + payloadSize); //Read the mask, which is 4 bytes, and than the payload buf = readBytes(MASK_SIZE + payloadSize); System.out.println("Payload:"); convertAndPrint(buf); //method continues below!
Now that we have read the entire message, it's time to expose it so that we can understand the meaning of the payload. To expose it, I created a method that takes a mask and a payload as arguments, and returns a decoded payload. Thus, the call is made using:
buf = unMask(Arrays.copyOfRange(buf, 0, 4), Arrays.copyOfRange(buf, 4, buf.length)); String message = new String(buf); return message; } }
Now the unMask method is pretty sweet and tiny
private byte[] unMask(byte[] mask, byte[] data) { for (int i = 0; i < data.length; i++) { data[i] = (byte) (data[i] ^ mask[i % mask.length]); } return data; }
The same goes for getSizeOfPayload:
private int getSizeOfPayload(byte b) {
It's all! You should now be able to communicate in both directions using clean sockets. I will fully add the Java class for completeness. He is able to receive and send messages with the client using web maps.
package javaapplication5; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import sun.misc.BASE64Encoder; /** * * @author * Anders */ public class WebsocketServer { public static final int MASK_SIZE = 4; public static final int SINGLE_FRAME_UNMASKED = 0x81; private ServerSocket serverSocket; private Socket socket; public WebsocketServer() throws IOException { serverSocket = new ServerSocket(2005); connect(); } private void connect() throws IOException { System.out.println("Listening"); socket = serverSocket.accept(); System.out.println("Got connection"); if(handshake()) { listenerThread(); } } private boolean handshake() throws IOException { PrintWriter out = new PrintWriter(socket.getOutputStream()); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); HashMap<String, String> keys = new HashMap<>(); String str; //Reading client handshake while (!(str = in.readLine()).equals("")) { String[] s = str.split(": "); System.out.println(); System.out.println(str); if (s.length == 2) { keys.put(s[0], s[1]); } } //Do what you want with the keys here, we will just use "Sec-WebSocket-Key" String hash; try { hash = new BASE64Encoder().encode(MessageDigest.getInstance("SHA-1").digest((keys.get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes())); } catch (NoSuchAlgorithmException ex) { ex.printStackTrace(); return false; } //Write handshake response out.write("HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + hash + "\r\n" + "\r\n"); out.flush(); return true; } private byte[] readBytes(int numOfBytes) throws IOException { byte[] b = new byte[numOfBytes]; socket.getInputStream().read(b); return b; } public void sendMessage(byte[] msg) throws IOException { System.out.println("Sending to client"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedOutputStream os = new BufferedOutputStream(socket.getOutputStream()); baos.write(SINGLE_FRAME_UNMASKED); baos.write(msg.length); baos.write(msg); baos.flush(); baos.close(); convertAndPrint(baos.toByteArray()); os.write(baos.toByteArray(), 0, baos.size()); os.flush(); } public void listenerThread() { Thread t = new Thread(new Runnable() { @Override public void run() { try { while (true) { System.out.println("Recieved from client: " + reiceveMessage()); } } catch (IOException ex) { ex.printStackTrace(); } } }); t.start(); } public String reiceveMessage() throws IOException { byte[] buf = readBytes(2); System.out.println("Headers:"); convertAndPrint(buf); int opcode = buf[0] & 0x0F; if (opcode == 8) { //Client want to close connection! System.out.println("Client closed!"); socket.close(); System.exit(0); return null; } else { final int payloadSize = getSizeOfPayload(buf[1]); System.out.println("Payloadsize: " + payloadSize); buf = readBytes(MASK_SIZE + payloadSize); System.out.println("Payload:"); convertAndPrint(buf); buf = unMask(Arrays.copyOfRange(buf, 0, 4), Arrays.copyOfRange(buf, 4, buf.length)); String message = new String(buf); return message; } } private int getSizeOfPayload(byte b) { //Must subtract 0x80 from masked frames return ((b & 0xFF) - 0x80); } private byte[] unMask(byte[] mask, byte[] data) { for (int i = 0; i < data.length; i++) { data[i] = (byte) (data[i] ^ mask[i % mask.length]); } return data; } private void convertAndPrint(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X ", b)); } System.out.println(sb.toString()); } public static void main(String[] args) throws IOException, InterruptedException, NoSuchAlgorithmException { WebsocketServer j = new WebsocketServer(); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("Write something to the client!"); j.sendMessage(br.readLine().getBytes()); } } }
And a simple client in html:
<!DOCTYPE HTML> <html> <body> <button type="button" onclick="connect();">Connect</button> <button type="button" onclick="connection.close()">Close</button> <form> <input type="text" id="msg" /> <button type="button" onclick="sayHello();">Say Hello!</button> <script> var connection; function connect() { console.log("connection"); connection = new WebSocket("ws://localhost:2005/"); </script> </body> </html>
Hope this clarifies something, and that I shed some light on him :)